From 1232fd6cdf7ef183e40da5e6358220eed9f7807e Mon Sep 17 00:00:00 2001 From: XairRrR Date: Sat, 10 Oct 2026 13:33:14 +0200 Subject: [PATCH] feat(agc-driver): prepare shader variants registration did not predict Registration prepares the variants a shader's registered state predicts; a draw or dispatch whose state needs another one threw "prepared shader artifact is missing for the requested static ABI". Titles that link their own libSceAgc build that state themselves and never call the driver's static ABI resolution, so their draws differ from the registered prediction. The missing artifact is now prepared at use, logged, and kept with the snapshot like the deferred and null pixel cases. SourceHandleFor now checks that the request refers to the registered code, as InvocationFor already did. Uncharted: Legacy of Thieves Collection (PPSA05684) still misses on main 197ffac9 (after the fixed push slots of #2661) for its own pixel shaders: "prepared shader artifact is missing for the requested static ABI: address=85899411712 stage=5 wave=64 pushOffset=128 pushCapacity=128". --- .../src/Driver/Shaders/ShaderRegistry.cpp | 52 +++++++------------ .../tests/execution/PreparedGraphics.cpp | 8 ++- .../tests/execution/PreparedShaders.cpp | 43 +++++++++++---- docs/dev/ARCHITECTURE.md | 1 + 4 files changed, 60 insertions(+), 44 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 52a46d03b..af6348610 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -227,20 +227,24 @@ void PublishRegisteredShader(std::shared_ptr& registry, const st namespace { -bool PreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler::RecompileRequest& request) { +void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler::RecompileRequest& request) { if (snapshot.codeAddress == NullPixelProgramAddress() && request.shader.stage == ShaderRecompiler::ShaderStage::Fragment && request.context.waveSize == 32u) { APS5_LOG_ERR("The null pixel program has no wave%u artifact for this draw; preparing it at draw", request.context.waveSize); - return true; + return; + } + if (!snapshot.header.empty()) { + if (!snapshot.prepared->deferred) APS5_LOG_ERR("Shader 0x%llx stage %u has no artifact prepared at registration for the state of this draw or dispatch; preparing it at use", static_cast(request.shader.codeAddress), static_cast(request.shader.stage)); + return; } - if (!snapshot.header.empty()) return snapshot.prepared->deferred; if (snapshot.type != 0 || request.shader.stage != ShaderRecompiler::ShaderStage::Compute) throw std::runtime_error("AGC driver: unregistered program is not a compute shader"); APS5_LOG_ERR("Compute shader 0x%llx was not registered; preparing its artifact at dispatch", static_cast(snapshot.codeAddress)); - return true; } } std::shared_ptr SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request) { + require(codeOffset < snapshot.code.size(), "prepared shader code offset is outside the snapshot"); + require(std::ranges::equal(request.shader.code, std::span(snapshot.code).subspan(codeOffset)), "prepared shader request does not refer to registered code"); struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(request, key); @@ -249,18 +253,10 @@ std::shared_ptr SourceHandleFor(const Shad for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) return entry.handle; } - if (PreparedAtUse(snapshot, request)) { - auto handle = PrepareShaderWithDiagnostics(request); - snapshot.prepared->entries.push_back({codeOffset, handle}); - return handle; - } - std::string layouts; - for (const auto& entry : snapshot.prepared->entries) { - if (entry.codeOffset != codeOffset || entry.handle == nullptr || entry.handle->artifact == nullptr) continue; - const auto& layout = entry.handle->artifact->layout; - layouts += " [" + std::to_string(layout.pushConstantOffsetBytes) + "," + std::to_string(layout.pushConstantSizeBytes) + "]"; - } - 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(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); + ReportPreparedAtUse(snapshot, request); + auto handle = PrepareShaderWithDiagnostics(request); + snapshot.prepared->entries.push_back({codeOffset, handle}); + return handle; } ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapshot, std::uint64_t vertexId, std::uint64_t fragmentId) { @@ -300,22 +296,14 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) return std::move(*invocation); } - if (PreparedAtUse(snapshot, request)) { - auto handle = PrepareShaderWithDiagnostics(request); - invocationRequest = request; - invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*handle); - auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, handle, key); - if (!invocation.has_value()) throw std::runtime_error("AGC driver: raw compute artifact does not match its invocation"); - snapshot.prepared->entries.push_back({codeOffset, std::move(handle)}); - return std::move(*invocation); - } - std::string layouts; - for (const auto& entry : snapshot.prepared->entries) { - if (entry.codeOffset != codeOffset || entry.handle == nullptr || entry.handle->artifact == nullptr) continue; - const auto& layout = entry.handle->artifact->layout; - layouts += " [" + std::to_string(layout.pushConstantOffsetBytes) + "," + std::to_string(layout.pushConstantSizeBytes) + "]"; - } - 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(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); + ReportPreparedAtUse(snapshot, request); + auto handle = PrepareShaderWithDiagnostics(request); + invocationRequest = request; + invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*handle); + auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, handle, key); + if (!invocation.has_value()) throw std::runtime_error("AGC driver: an artifact prepared at use does not match its invocation"); + snapshot.prepared->entries.push_back({codeOffset, std::move(handle)}); + return std::move(*invocation); } std::optional RegisteredFloatMode(const ShaderSnapshot& snapshot) { if (snapshot.registeredState == nullptr) return std::nullopt; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp index eb1a511a1..db2a20d9e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedGraphics.cpp @@ -231,13 +231,17 @@ void Check(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ShaderPath path request.layout.pushConstantSizeBytes -= 4; static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); request.layout.pushConstantOffsetBytes = 2; - Reject([&] { static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); }, "artifact is missing"); + Reject([&] { static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); }, "not dword-aligned"); request.layout = {0, 0, 0, mesh ? MeshDrawPushOffsetBytes : 128u}; if (pixel) request.context.pixel->interpolatorSettings[0] ^= 1u; else if (tessellation) ++request.graphics->tessellation->outputControlPoints; else if (mesh) ++request.graphics->mesh->maxVertices; else ++request.context.userDataBaseRegister; - Reject([&] { static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); }, "artifact is missing"); + const auto predicted = program.snapshot->prepared->entries.size(); + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(program.snapshot->prepared->entries.size() == predicted + 1, "a draw state that registration did not predict was not prepared"); + static_cast(InvocationFor(*program.snapshot, program.codeOffset, request)); + Require(program.snapshot->prepared->entries.size() == predicted + 1, "an artifact prepared at a draw was not reused"); } ShaderRegistry invalidRegistry; DrawDecode invalid{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 695192b8d..1a60e4153 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -92,12 +94,18 @@ void Run(AgcDriver::VulkanDevice& device) { const ShaderRecompiler::ShaderComputeStageInfo compute{{1, 1, 1}, 0, {false, false, false}, false, 1, {}}; ShaderRecompiler::RecompileRequest request{{ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, {32, 0, users, compute, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}}; RunUnregistered(device, request); + { + AgcDriver::DriverDetail::ShaderSnapshot unpredicted{request.shader.codeAddress, 0, 0, {code.begin(), code.end()}, {}}; + unpredicted.header.resize(sizeof(Shader)); + const auto prepared = AgcDriver::DriverDetail::SourceHandleFor(unpredicted, 0, request); + Require(unpredicted.prepared->entries.size() == 1 && unpredicted.prepared->entries.front().handle == prepared, "a registered shader with no matching artifact was not prepared"); + Require(AgcDriver::DriverDetail::SourceHandleFor(unpredicted, 0, request) == prepared && unpredicted.prepared->entries.size() == 1, "an artifact prepared at a dispatch was not reused"); + } AgcDriver::DriverDetail::ShaderSnapshot snapshot{request.shader.codeAddress, 0, 0, {code.begin(), code.end()}, {}}; snapshot.header.resize(sizeof(Shader)); - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "artifact is missing"); const auto handle = ShaderRecompiler::PrepareShader(request); snapshot.prepared->entries.push_back({0, handle}); - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 1, request)); }, "artifact is missing"); + ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 1, request)); }, "outside the snapshot"); const auto& artifact = ShaderRecompiler::GetPreparedArtifact(*handle); for (std::uint32_t value = 0; value < 8; ++value) { users.fill(value); @@ -118,7 +126,10 @@ void Run(AgcDriver::VulkanDevice& device) { ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request)); }, "does not refer to registered code"); ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 1, registeredRequest)); }, "outside the snapshot"); registeredRequest.context.compute->numThreads[0] = 2; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, registeredRequest)); }, "artifact is missing"); + static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, registeredRequest)); + Require(snapshot.prepared->entries.size() == 2, "a dispatch state that registration did not predict was not prepared"); + static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, registeredRequest)); + Require(snapshot.prepared->entries.size() == 2, "an artifact prepared at a dispatch was not reused"); ShaderRecompiler::SrtRuntime preparedRuntime{}; preparedRuntime.userData = users; const auto preparedCapture = invocation.Capture(preparedRuntime); @@ -144,12 +155,13 @@ void Run(AgcDriver::VulkanDevice& device) { replacement->Dispatch(*replacementResult, 1, 1, 1); replacement->WaitIdle(); replacementRequest.target.nonConstantImageOffsets = !replacementRequest.target.nonConstantImageOffsets; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, replacementRequest)); }, "artifact is missing"); + static_cast(AgcDriver::DriverDetail::InvocationFor(snapshot, 0, replacementRequest)); + Require(snapshot.prepared->entries.size() == 3, "a target that registration did not prepare for was not prepared"); ShaderRecompiler::SrtRuntime runtime{}; runtime.userData = users; const auto capture = ShaderRecompiler::CaptureResources(request, runtime, *handle); request.context.compute->numThreads[0] = 2; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "artifact is missing"); + Require(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request) == snapshot.prepared->entries[1].handle, "a dispatch did not find the artifact prepared for its state"); ExpectFailure([&] { static_cast(ShaderRecompiler::CaptureResources(request, runtime, *handle)); }, "does not match the static ABI"); ExpectFailure([&] { static_cast(ShaderRecompiler::MaterializeShader(request, *capture, *handle)); }, "does not match the static ABI"); const auto otherHandle = ShaderRecompiler::PrepareShader(request); @@ -160,10 +172,10 @@ void Run(AgcDriver::VulkanDevice& device) { request.layout.pushConstantSizeBytes = 124; Require(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request) == handle, "compatible push constant capacity discarded the prepared artifact"); request.layout.pushConstantSizeBytes = 126; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "artifact is missing"); + ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "not dword-aligned"); request.layout.pushConstantSizeBytes = 128; code[0] = 0xffffffffu; - ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "artifact is missing"); + ExpectFailure([&] { static_cast(AgcDriver::DriverDetail::SourceHandleFor(snapshot, 0, request)); }, "does not refer to registered code"); } void FailureCapture(AgcDriver::VulkanDevice& device) { @@ -434,9 +446,20 @@ void Registration(bool indirect) { commands.insert(commands.end(), {0xc0033700u, 0x00100200u, static_cast(destinationAddress), static_cast(destinationAddress >> 32u), 7}); packet = Packet{commands.data(), static_cast(commands.size()), 0, {}}; sceAgcDriverSubmitAcb(0x20, &packet); - ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "artifact is missing"); - ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, "artifact is missing"); - ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "artifact is missing"); + AgcDriverWaitIdle_nid_postfix(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (std::atomic_ref(destination).load() != 7 && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(destination == 7, "a dispatch whose state registration did not predict did not run"); + destination = 0; + alignas(256) static const std::array invalid{0xffffffffu}; + const auto invalidAddress = reinterpret_cast(invalid.data()); + commands[2] = static_cast(invalidAddress >> 8u); + commands[5] = static_cast(invalidAddress >> 40u); + packet = Packet{commands.data(), static_cast(commands.size()), 0, {}}; + sceAgcDriverSubmitAcb(0x20, &packet); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, ""); + ExpectFailure([] { AgcDriverWaitIdle_nid_postfix(); }, ""); + ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, ""); Require(destination == 0, "failed dispatch executed a subsequent memory write"); } diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 3713b767c..4e3165c6a 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -70,6 +70,7 @@ flowchart LR - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). - The snapshot is published as pending. `SourceHandleFor`, `InvocationFor` and `ShaderPreparationTransaction::Edit` / `Read` wait for the pending job before reading `PreparedShaders`, and rethrow its failure. `APS5_SYNC_SHADER_PREPARE=1` prepares on the registering thread instead. +- A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. ### Shader MODE register access