feat(shader): carry the shader float mode into the recompile request

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Adrià Franch 2026-10-08 22:17:07 +02:00
parent 4b6ab6bb73
commit 0cbc9586bc
11 changed files with 126 additions and 8 deletions

View File

@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <array>
#include <optional>
#include <atomic>
#include <cstddef>
#include <cstdint>
@ -70,6 +71,7 @@ struct ShaderSnapshot {
std::shared_ptr<const ShaderSnapshot> ReadRawComputeShader(std::uint64_t address);
std::uint64_t NullPixelProgramAddress();
std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const ShaderSnapshot& snapshot);
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target);

View File

@ -67,7 +67,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
}
ShaderRecompiler::RecompileRequest request{
{ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header},
{(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory},
{(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory, RegisteredFloatMode(snapshot)},
localDevice->ComputeTarget((packet[4] & 0x8000u) != 0 ? 32u : 64u),
{0, 0, 0, 128}
};

View File

@ -18,7 +18,7 @@ ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, st
const auto waveSize = program.binary.stage == Stage::Fragment ? graphics.stages.fragmentWaveSize : graphics.stages.vertexWaveSize;
ShaderRecompiler::RecompileRequest request{
program.binary,
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory},
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)},
localDevice->Target(),
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushConstantBytes) - pushOffset},
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}}

View File

@ -235,6 +235,23 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s
}
throw std::runtime_error("AGC driver: prepared shader artifact is missing for the requested static ABI: address=" + std::to_string(request.shader.codeAddress) + " stage=" + std::to_string(static_cast<std::uint32_t>(request.shader.stage)) + " wave=" + std::to_string(request.context.waveSize) + " pushOffset=" + std::to_string(request.layout.pushConstantOffsetBytes) + " pushCapacity=" + std::to_string(request.layout.pushConstantSizeBytes) + " preparedLayouts=" + layouts);
}
std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const ShaderSnapshot& snapshot) {
if (snapshot.registeredState == nullptr) return std::nullopt;
std::uint32_t rsrc1;
std::uint32_t fp16OverflowBit;
switch (snapshot.type) {
case 0: rsrc1 = 0x212; fp16OverflowBit = 26; break;
case 1: rsrc1 = 0x00a; fp16OverflowBit = 29; break;
case 2: case 4: case 6: rsrc1 = 0x08a; fp16OverflowBit = 31; break;
case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break;
default: return std::nullopt;
}
const auto found = snapshot.registeredState->shader.find(rsrc1);
if (found == snapshot.registeredState->shader.end()) return std::nullopt;
const auto value = found->second;
return ShaderRecompiler::ShaderFloatMode{(value >> 12u) & 0xffu, ((value >> 21u) & 1u) != 0u, ((value >> 23u) & 1u) != 0u, ((value >> fp16OverflowBit) & 1u) != 0u};
}
namespace {
template<typename TValue>
@ -352,7 +369,7 @@ std::vector<PreparedShaders::Entry> PrepareRegistered(const ShaderSnapshot& snap
auto graph = ShaderRecompiler::GraphBuilder{}.Build(decoded, &swappc);
ShaderRecompiler::Structurizer{}.Structurize(graph);
const std::array<ShaderRecompiler::MemoryRegion, 2> memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}};
ShaderRecompiler::RecompileRequest request{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, userData, compute, pixel, vertex, memory}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics};
ShaderRecompiler::RecompileRequest request{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, userData, compute, pixel, vertex, memory, RegisteredFloatMode(snapshot)}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics};
if (graphics && graphics->mesh) request.layout.pushConstantSizeBytes = ShaderRecompiler::MeshDrawPushOffsetBytes;
std::vector<PreparedShaders::Entry> entries;
const auto append = [&] {
@ -404,7 +421,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize;
std::optional<ShaderRecompiler::ShaderVertexStageInfo> vertex;
if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true);
ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory}, target, {0, 0, pushOffset, capacity - pushOffset}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}};
ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory, RegisteredFloatMode(*program.snapshot)}, target, {0, 0, pushOffset, capacity - pushOffset}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}};
std::vector<std::uint64_t> key;
ShaderRecompiler::BuildPreparedShaderKey(request, key);
std::shared_ptr<const ShaderRecompiler::SourceHandle> handle;

View File

@ -6,6 +6,7 @@
#include "Optimization/ResourceProgram.hpp"
#include "Optimization/DescriptorBindingBuilder.hpp"
#include "ShaderCacheDirectory.hpp"
#include "ControlFlow/RequestSerializer.hpp"
#include <algorithm>
#include <array>
#include <chrono>
@ -394,6 +395,21 @@ void verifyKeySensitivity() {
changes("the first binding", [](SampleRequest& sample) { sample.request.layout.firstBinding = 1; });
changes("the push constant offset", [](SampleRequest& sample) { sample.request.layout.pushConstantOffsetBytes = 16; });
changes("the push constant size", [](SampleRequest& sample) { sample.request.layout.pushConstantSizeBytes = 64; });
changes("a float mode", [](SampleRequest& sample) { sample.request.context.floatMode = ShaderFloatMode{}; });
const ShaderFloatMode astroMode{0xc0u, true, false, false};
const auto withMode = [&](const ShaderFloatMode& mode) {
SampleRequest sample;
sample.request.context.floatMode = mode;
return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)};
};
const auto astro = withMode(astroMode);
require(withMode(astroMode) == astro, "the float mode key is not deterministic");
for (const auto& [what, mode] : std::initializer_list<std::pair<const char*, ShaderFloatMode>>{
{"FLOAT_MODE", {0x00u, true, false, false}}, {"DX10_CLAMP", {0xc0u, false, false, false}},
{"IEEE_MODE", {0xc0u, true, true, false}}, {"FP16_OVFL", {0xc0u, true, false, true}}}) {
const auto changed = withMode(mode);
require(changed.first != astro.first && changed.second != astro.second, std::string("the key ignores ") + what);
}
SampleRequest moved;
moved.userData[0] ^= 0x10000u;
@ -445,6 +461,16 @@ void verifyKeySensitivity() {
require(fragment.Key() != fragmentKey, "integer fragment output reused a float interface");
}
void verifyFloatModeSerialization() {
const RequestSerializer serializer;
SampleRequest sample;
require(!serializer.Deserialize(serializer.Serialize(sample.request)).request.context.floatMode.has_value(), "an unknown float mode came back from serialization");
const ShaderFloatMode mode{0x04u, false, true, true};
sample.request.context.floatMode = mode;
const auto back = serializer.Deserialize(serializer.Serialize(sample.request));
require(back.request.context.floatMode == mode, "the float mode did not survive serialization");
}
void verifyStore() {
require(ShaderDiskCache::Enabled(), "the disk cache is not enabled");
SampleRequest sample;
@ -996,6 +1022,7 @@ int main(int argc, char** argv) {
verifyEntryRoundTrip();
verifyArtifactStorageIsolation();
verifyKeySensitivity();
verifyFloatModeSerialization();
verifyStore();
verifyAcrossProcesses(argv[0]);
verifyEmissionFailureMemo();

View File

@ -916,12 +916,12 @@ void verifyPixelRequestSerialization() {
minimal.context.waveSize = 64;
minimal.context.pixel = ShaderPixelStageInfo{};
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQQwAAAA=", "new requests did not use serialization version 12");
require(requestPrefix(encoded, 8u) == "NVNQQQ0AAAA=", "new requests did not use serialization version 13");
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u;
for (std::size_t bytes = 0; bytes < 8u; ++bytes) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-12 pixel mapping was accepted");
}
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQ0AAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQ4AAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
}

View File

@ -16,6 +16,7 @@
#include <cstdio>
#include <cstring>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <thread>
@ -480,6 +481,41 @@ void testShaderHeaderAlignment() {
refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, truncated)); }), "smaller than its fixed fields");
}
void testRegisteredFloatMode() {
using AgcDriver::DriverDetail::RegisteredFloatMode;
using AgcDriver::DriverDetail::ShaderSnapshot;
using AgcDriver::DriverDetail::RegisteredShaderState;
struct Stage {
std::uint8_t type;
std::uint32_t rsrc1;
std::uint32_t fp16OverflowBit;
};
constexpr std::array<Stage, 7> stages{{{0, 0x212, 26}, {1, 0x00a, 29}, {2, 0x08a, 31}, {4, 0x08a, 31}, {6, 0x08a, 31}, {5, 0x10a, 30}, {7, 0x10a, 30}}};
constexpr std::array<std::uint32_t, 4> otherBits{26, 29, 30, 31};
for (const auto& stage : stages) {
const auto mode = [&](std::uint32_t value) {
ShaderSnapshot snapshot{0x20000, 0, stage.type, {}, {}};
auto state = std::make_shared<RegisteredShaderState>();
state->shader.emplace(stage.rsrc1, value);
snapshot.registeredState = state;
return RegisteredFloatMode(snapshot);
};
const auto name = "stage type " + std::to_string(stage.type);
const auto astro = mode((0xc0u << 12u) | (1u << 21u) | 0x3fu);
check(astro.has_value() && astro->floatMode == 0xc0u && astro->dx10Clamp && !astro->ieeeMode && !astro->fp16Overflow, (name + ": FLOAT_MODE 0xc0 with DX10_CLAMP decoded wrong").c_str());
const auto ieee = mode(1u << 23u);
check(ieee && ieee->ieeeMode && ieee->floatMode == 0u && !ieee->dx10Clamp, (name + ": IEEE_MODE decoded wrong").c_str());
check(mode(1u << stage.fp16OverflowBit)->fp16Overflow, (name + ": FP16_OVFL not read from bit " + std::to_string(stage.fp16OverflowBit)).c_str());
for (const auto bit : otherBits) {
if (bit != stage.fp16OverflowBit) check(!mode(1u << bit)->fp16Overflow, (name + ": bit " + std::to_string(bit) + " read as FP16_OVFL").c_str());
}
ShaderSnapshot missing{0x20000, 0, stage.type, {}, {}};
check(!RegisteredFloatMode(missing).has_value(), (name + ": a snapshot without registered state has a float mode").c_str());
missing.registeredState = std::make_shared<RegisteredShaderState>();
check(!RegisteredFloatMode(missing).has_value(), (name + ": a missing RSRC1 has a float mode").c_str());
}
}
void testWorkerFailure() {
std::array<std::uint32_t, 5> words{0xc0031500, 1, 1, 1, 0x41};
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
@ -580,6 +616,7 @@ int main() {
testWaitFreeSubmissionTheCpuWaitsFor();
testMultiSubmissions();
testShaderHeaderAlignment();
testRegisteredFloatMode();
testWorkerFailure();
check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure");
std::puts("AGC driver submit tests passed");

View File

@ -51,6 +51,7 @@ public:
append(key, request.context.compute);
append(key, request.context.pixel);
append(key, request.context.vertex);
append(key, request.context.floatMode);
appendMesh(key, request);
appendTessellation(key, request);
std::uint64_t hash = 0xcbf29ce484222325ull;
@ -82,6 +83,7 @@ private:
append(key, request.context.compute);
append(key, request.context.pixel);
append(key, request.context.vertex);
append(key, request.context.floatMode);
appendMesh(key, request);
appendTessellation(key, request);
append(key, request.target);
@ -133,6 +135,13 @@ private:
for (const unsigned char byte : value) append(key, byte);
}
static void append(std::vector<std::uint64_t>& key, const ShaderFloatMode& value) {
append(key, value.floatMode);
append(key, value.dx10Clamp);
append(key, value.ieeeMode);
append(key, value.fp16Overflow);
}
static void append(std::vector<std::uint64_t>& key, const ShaderComputeStageInfo& value) {
append(key, value.numThreads);
append(key, value.ldsSizeDwords);

View File

@ -519,6 +519,13 @@ void writeGuestContext(Writer& writer, const GuestContext& context) {
writer.WriteU64(region.guestAddress);
writer.WriteBytes(region.bytes);
}
writer.WriteBool(context.floatMode.has_value());
if (context.floatMode.has_value()) {
writer.WriteU32(context.floatMode->floatMode);
writer.WriteBool(context.floatMode->dx10Clamp);
writer.WriteBool(context.floatMode->ieeeMode);
writer.WriteBool(context.floatMode->fp16Overflow);
}
}
GuestContext readGuestContext(Reader& reader, DeserializedRequest& result, std::uint32_t version) {
@ -551,6 +558,14 @@ GuestContext readGuestContext(Reader& reader, DeserializedRequest& result, std::
result.memory.push_back(region);
}
context.memory = result.memory;
if (version >= 13u && reader.ReadBool()) {
ShaderFloatMode mode;
mode.floatMode = reader.ReadU32();
mode.dx10Clamp = reader.ReadBool();
mode.ieeeMode = reader.ReadBool();
mode.fp16Overflow = reader.ReadBool();
context.floatMode = mode;
}
return context;
}
@ -699,7 +714,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const
std::string buffer;
Writer writer(buffer);
writer.WriteU32(0x41505335u);
writer.WriteU32(12u);
writer.WriteU32(13u);
writeShaderBinary(writer, request.shader);
writeGuestContext(writer, request.context);
writeSpirvTarget(writer, request.target);
@ -726,7 +741,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const
Reader reader(decoded);
if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature");
const auto version = reader.ReadU32();
if (version < 1u || version > 12u) throw std::runtime_error("unsupported recompile request serialization version");
if (version < 1u || version > 13u) throw std::runtime_error("unsupported recompile request serialization version");
DeserializedRequest result{};
result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader);
result.request.context = readGuestContext(reader, result, version);

View File

@ -155,6 +155,15 @@ struct ShaderVertexStageInfo {
bool fetchEmbedded;
};
struct ShaderFloatMode {
std::uint32_t floatMode = 0;
bool dx10Clamp = false;
bool ieeeMode = false;
bool fp16Overflow = false;
bool operator==(const ShaderFloatMode&) const = default;
};
struct GuestContext {
std::uint32_t waveSize;
std::uint32_t userDataBaseRegister;
@ -163,6 +172,7 @@ struct GuestContext {
std::optional<ShaderPixelStageInfo> pixel;
std::optional<ShaderVertexStageInfo> vertex;
std::span<const MemoryRegion> memory;
std::optional<ShaderFloatMode> floatMode;
};
struct MeshTargetLimits {

View File

@ -284,6 +284,7 @@ Throughout the project, every function at every stage either **does exactly what
- A [COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown.
- A [COND_INDIRECT_BUFFER](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords.
- [SET_*_REG_INDIRECT](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write.
- The [shader float mode](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (`FLOAT_MODE`, `DX10_CLAMP`, `IEEE_MODE` and `FP16_OVFL` of `COMPUTE_PGM_RSRC1` and `SPI_SHADER_PGM_RSRC1_PS/GS/HS`, bit positions from Mesa's `gfx10.json` and `gfx103.json`) is decoded into `GuestContext::floatMode` and is part of the shader cache keys and the serialized request, but no translation reads it yet (#981). Every float semantic still follows the single mode it was measured or written in, as the entries below record. The mode comes from the registered shader header's `sh_registers`, the same source at registration and at draw/dispatch so the prepared artifact keys match. A title that rewrites RSRC1 at run time, or a raw compute program with no header, compiles with that mode or none.
- [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted, a positive denormal src2 counts as positive): the recompiler does not read MODE, so other IEEE/denormal settings are not modelled.
- [v_fma_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140) is assumed to be fused, which has not been measured on hardware. LLVM decodes this encoding as `v_mad_legacy_f32` for gfx1013, and `v_mac_legacy_f32` (VOP2 0x06) was measured to round its product before the add, so 0x140 may do the same.
- The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability.