From 687f73c9515d63083f00a976783d837255b1e3f4 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Sun, 11 Oct 2026 11:09:58 +0200 Subject: [PATCH] fix(agc): select the registered back half for a fused hull header sceAgcFuseShaderHalves returns a fused hull as a copy of its back half (type 7) that is not registered: type 3, no user data, and shader registers copied to scratch memory with the checksum, RSRC1/RSRC2 and the LS program address rewritten. sceAgcCreatePrimState accepts only that type-3 header, and graphics ABI resolution refused it as a replaced shader header. SameHeader now also takes a type-3 header as the registered type-7 header for the same code when every other field is equal and its shader registers differ only in the fused ones. Graphics ABI resolution then applies the bound header's shader registers over the registered state. PPSA26344 fuses its hull halves during its loading screen. Co-Authored-By: Claude Opus 5.5 --- .../include/Driver/Shaders/ShaderRegistry.hpp | 3 ++ .../src/Driver/Shaders/ShaderRegistry.cpp | 28 ++++++++++- .../tests/execution/PreparedPublication.cpp | 50 +++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index ffec875b5..7ccb5d690 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -18,6 +18,8 @@ #include #include +struct Shader; + namespace ShaderRecompiler { class ShaderPreparationContext; } @@ -96,6 +98,7 @@ std::shared_ptr PrepareShaderWithDiagnosti std::uint64_t NullPixelProgramAddress(); ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device); std::optional RegisteredFloatMode(const ShaderSnapshot& snapshot); +std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader); std::shared_ptr RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list types); void PublishRegisteredShader(std::shared_ptr& registry, const std::shared_ptr& snapshot); void SubmitRegistrationPreparation(std::function task); 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 9a42f6534..9b46369ce 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -230,6 +230,27 @@ void ShaderPreparationTransaction::Commit() { committed = true; } +bool FusedHullHeader(const ShaderSnapshot& snapshot, Shader used, const Shader& registered) { + if (used.type != 3u || registered.type != 7u) return false; + const auto* bound = used.sh_registers; + used.type = registered.type; + used.sh_registers = registered.sh_registers; + if (std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) != 0) return false; + const auto address = reinterpret_cast(registered.sh_registers); + const std::size_t bytes = registered.num_sh_registers * sizeof(ShaderRegister); + if (bytes == 0 || bound == nullptr || address < snapshot.headerAddress || address - snapshot.headerAddress > snapshot.header.size() || snapshot.header.size() - (address - snapshot.headerAddress) < bytes) return false; + GuestMemory::CheckRange(bound, bytes, alignof(ShaderRegister)); + for (std::size_t index = 0; index < registered.num_sh_registers; ++index) { + ShaderRegister expected; + std::memcpy(&expected, snapshot.header.data() + (address - snapshot.headerAddress) + index * sizeof(ShaderRegister), sizeof(ShaderRegister)); + const auto reg = bound[index]; + if (reg.offset != expected.offset) return false; + const bool fused = reg.offset == 0x100u || reg.offset == 0x10au || reg.offset == 0x10bu || reg.offset == 0x148u || reg.offset == 0x149u; + if (!fused && reg.value != expected.value) return false; + } + return true; +} + bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { if (snapshot.headerAddress == reinterpret_cast(shader)) return true; if (snapshot.header.size() < sizeof(Shader)) return false; @@ -239,7 +260,7 @@ bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) { std::memcpy(&used, static_cast(shader), sizeof(Shader)); std::memcpy(®istered, snapshot.header.data(), sizeof(Shader)); used.user_data = registered.user_data; - return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0; + return std::memcmp(&used, ®istered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || FusedHullHeader(snapshot, used, registered); } std::shared_ptr RegisteredHeader(const ShaderRegistry& registry, const Shader* shader) { @@ -836,6 +857,11 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std registered.shader.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.shader.insert_or_assign(offset, value); }); registered.context.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.context.insert_or_assign(offset, value); }); registered.userConfig.ForEach([&](std::uint32_t offset, std::uint32_t value) { state.userConfig.insert_or_assign(offset, value); }); + if (shader->type != snapshot.type) { + const std::size_t count = shader->num_sh_registers; + GuestMemory::CheckRange(shader->sh_registers, count * sizeof(ShaderRegister), alignof(ShaderRegister)); + for (std::size_t index = 0; index < count; ++index) state.shader.insert_or_assign(shader->sh_registers[index].offset, shader->sh_registers[index].value); + } } } for (const auto reg : context) state.context.insert_or_assign(reg.offset, reg.value); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp index 5c5260c34..b6abe53ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "Optimization/ResourceProgram.hpp" +#include #include #include #include @@ -147,6 +148,54 @@ void HeaderSelection() { Reject([&] { static_cast(RegisteredProgram(*registry, 0x10000, {2, 4})); }); } +void FusedHullHeaderSelection() { + struct Hull { + Shader shader{}; + std::array registers{{{0x100, 0x11}, {0x108, 0x100}, {0x109, 0}, {0x10a, 0x2}, {0x10b, 0x10}, {0x148, 0}, {0x149, 0}}}; + ShaderSpecialRegs specials{}; + } back; + back.shader.file_header = 0x34333231u; + back.shader.version = 0x18; + back.shader.header_size = sizeof(Hull); + back.shader.shader_size = 4; + back.shader.code = reinterpret_cast(std::uintptr_t{0x50000}); + back.shader.type = 7; + back.shader.sh_registers = back.registers.data(); + back.shader.num_sh_registers = back.registers.size(); + back.shader.specials = &back.specials; + auto snapshot = std::make_shared(); + snapshot->codeAddress = 0x50000; + snapshot->headerAddress = reinterpret_cast(&back); + snapshot->type = 7; + snapshot->code = {0xbf810000u}; + snapshot->header.resize(sizeof(Hull)); + std::memcpy(snapshot->header.data(), &back, sizeof(Hull)); + std::shared_ptr registry; + PublishRegisteredShader(registry, snapshot); + auto registers = back.registers; + registers[0].value = 0x22; + registers[3].value = 0x3; + registers[4].value = 0x1e; + registers[5].value = 0x600; + registers[6].value = 0x1; + Shader fused = back.shader; + fused.type = 3; + fused.sh_registers = registers.data(); + fused.user_data = nullptr; + Require(RegisteredHeader(*registry, &fused) == snapshot, "a fused hull header did not select its registered back half"); + registers[1].value = 0x200; + Require(RegisteredHeader(*registry, &fused) == nullptr, "a fused hull header with another hull program selected the back half"); + registers[1].value = 0x100; + registers[2].offset = 0x10c; + Require(RegisteredHeader(*registry, &fused) == nullptr, "a fused hull header with other registers selected the back half"); + registers[2].offset = 0x109; + fused.target ^= 1u; + Require(RegisteredHeader(*registry, &fused) == nullptr, "a fused hull header with another target selected the back half"); + fused.target ^= 1u; + fused.type = 2; + Require(RegisteredHeader(*registry, &fused) == nullptr, "a vertex header selected a registered hull back half"); +} + void Registration(AgcDriver::VulkanDevice& device) { const std::array code{0xbf810000u}; RecompileRequest request{{ShaderStage::Compute, 0x10000, code, 0, {}}, {32, 0, {}, ShaderComputeStageInfo{{1, 1, 1}, 0, {false, false, false}, false, 0, {}}, {}, {}, {}}, device.ComputeTarget(32), {0, 0, 0, 128}}; @@ -234,6 +283,7 @@ int main() { try { Transactions(); HeaderSelection(); + FusedHullHeaderSelection(); auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; Registration(*device); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 37557e4a0..0f72550b6 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -81,7 +81,7 @@ flowchart LR - `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. - 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 shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. +- The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - 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 compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry.