From b63996f1ff9b6a17cfee3c48fd81bdc6d38ec253 Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 13:13:01 +0000 Subject: [PATCH] fix(agc): read a merged program's vertex tables past its hidden user words A merged (NGG geometry path) program's user data starts with eight hidden words, so the vertex buffer and attribute table offsets of its AGC header, which count from the first user SGPR, were read eight words too early and found zeros. Killzone: Liberation (PPSA11142) stopped on `AGC graphics: null vertex buffer table address` at the first frame of its PSP game. The decoder now adds the hidden word count to the table reads; the registers reported to the recompiler are unchanged, as it already maps them from the first user SGPR. `agc_driver_graphics` gains a merged and a plain layout case. Co-Authored-By: Claude Fable 5.1 --- .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../src/Driver/Shaders/ShaderRegistry.cpp | 4 +- .../Graphics/include/ShaderInputState.hpp | 2 +- .../Graphics/src/ShaderInputState.cpp | 12 +++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 39 +++++++++++++++++++ 5 files changed, 50 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index fe033b378..bc0349ddb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -144,7 +144,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe const auto& program = programs[i]; if (program.binary.stage == Stage::Fragment || roles[i] == Role::GeometryBack) return; decodeReads[i].clear(); - vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i]); + vertexInfos[i] = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, &decodeReads[i], false, 8u - program.firstUserSgpr); }; if (!registerKey) { for (std::size_t i = 0; i < programs.size(); ++i) decodeVertexInfo(i); 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 9b46369ce..b0a111de6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -618,7 +618,7 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap if (firstUser == 0) userCount += 8; } plan->userData.resize(userCount); - if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true); + if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, plan->userData, nullptr, true, 8u - firstUser); plan->swappc = {vertex.has_value(), firstUser, userCount}; if (deferred != nullptr) { if (const auto counter = ShaderRecompiler::UnresolvableSwappcTarget(plan->decoded, &plan->swappc)) { @@ -805,7 +805,7 @@ std::vector PrepareGraphicsStages(const DrawDecode& decod const bool fragment = program.binary.stage == ShaderRecompiler::ShaderStage::Fragment; const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize; std::optional vertex; - if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true); + if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true, 8u - program.firstUserSgpr); pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots); const auto offset = pushOffset; const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 671937306..d1202c5fa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -18,7 +18,7 @@ struct DecodeRead { std::uint64_t address; std::vector bytes; }; -ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads = nullptr, bool staticAbi = false); +ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads = nullptr, bool staticAbi = false, std::uint32_t hiddenUserWords = 0); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 486df0e35..8c20d1a06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -162,7 +162,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con }; } -ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi) { +ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) { if (header.empty()) return {}; if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header"); Shader shader; @@ -185,14 +185,16 @@ ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span(vertexBufferReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); - if (static_cast(vertexAttribReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); + const auto bufferWord = static_cast(vertexBufferReg) + hiddenUserWords; + const auto attribWord = static_cast(vertexAttribReg) + hiddenUserWords; + if (bufferWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain"); + if (attribWord + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain"); if (shader.num_input_semantics == 0 || shader.num_input_semantics > ShaderRecompiler::ShaderVertexStageInfo::MaxResources) throw std::runtime_error("AGC graphics: vertex semantic count is outside the supported domain"); if (shader.input_semantics == nullptr) throw std::runtime_error("AGC graphics: missing vertex input semantics"); std::array semantics{}; _readHeaderArray(header, headerAddress, shader.input_semantics, shader.num_input_semantics, semantics.data()); - const auto attribTableAddr = static_cast(userData[static_cast(vertexAttribReg)]) | (static_cast(userData[static_cast(vertexAttribReg) + 1u]) << 32u); - const auto bufferTableAddr = static_cast(userData[static_cast(vertexBufferReg)]) | (static_cast(userData[static_cast(vertexBufferReg) + 1u]) << 32u); + const auto attribTableAddr = static_cast(userData[attribWord]) | (static_cast(userData[attribWord + 1u]) << 32u); + const auto bufferTableAddr = static_cast(userData[bufferWord]) | (static_cast(userData[bufferWord + 1u]) << 32u); if (!staticAbi && attribTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex attribute table address"); if (!staticAbi && bufferTableAddr == 0) throw std::runtime_error("AGC graphics: null vertex buffer table address"); info.fetchEmbedded = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 78a2e9a03..7dc8b270b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -13,6 +13,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include "SceShaders.hpp" +#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp" #include "ControlFlow/RequestSerializer.hpp" #include "CacheKey.hpp" #include "Optimization/ResourceProgram.hpp" @@ -723,6 +724,43 @@ void ColorPipeBankXorTests() { } } +void HiddenUserWordVertexTablesTests() { + constexpr std::size_t userDataOffset = 288; + constexpr std::size_t directOffset = 352; + constexpr std::size_t semanticOffset = 384; + static std::vector header(semanticOffset + sizeof(ShaderSemantic)); + static std::array bufferTable{0x1000u, 0x20000u, 64u, 0x3000u}; + static std::array attribTable{(0x2cu << 5u)}; + std::array direct{}; + direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET); + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 4; + direct[static_cast(ShaderRegs::AgcDirectResourceType::PtrVertexAttribDescTable)] = 6; + std::memcpy(header.data() + directOffset, direct.data(), sizeof(direct)); + ShaderSemantic semantic{}; + semantic.size_in_elements = 1; + std::memcpy(header.data() + semanticOffset, &semantic, sizeof(semantic)); + ShaderUserData userData{}; + userData.direct_resource_offset = reinterpret_cast(header.data() + directOffset); + userData.direct_resource_count = ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT; + std::memcpy(header.data() + userDataOffset, &userData, sizeof(userData)); + Shader shader{}; + shader.user_data = reinterpret_cast(header.data() + userDataOffset); + shader.input_semantics = reinterpret_cast(header.data() + semanticOffset); + shader.num_input_semantics = 1; + std::memcpy(header.data(), &shader, sizeof(shader)); + const auto headerAddress = reinterpret_cast(header.data()); + const auto low = [](const void* pointer) { return static_cast(reinterpret_cast(pointer)); }; + const auto high = [](const void* pointer) { return static_cast(reinterpret_cast(pointer) >> 32u); }; + const std::array registers{0x31016facu, 0x7efff390u, 0x100002u, 0x49u, low(bufferTable.data()), high(bufferTable.data()), low(attribTable.data()), high(attribTable.data())}; + std::vector merged(8, 0u); + merged.insert(merged.end(), registers.begin(), registers.end()); + const auto info = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 8); + Require(info.fetchEmbedded && info.fetchBufferReg == 4u && info.fetchAttribReg == 6u && info.resourcesNum == 1 && info.resources[0].fields[0] == 0x1000u && info.resources[0].fields[2] == 64u, "a merged program's vertex tables were not read past its eight hidden user words"); + expectFailure([&] { static_cast(AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, merged, nullptr, false, 0)); }, "null vertex attribute table"); + const auto plain = AgcDriver::Graphics::DecodeVertexStageInfo(header, headerAddress, registers, nullptr, false, 0); + Require(plain.fetchEmbedded && plain.fetchBufferReg == 4u && plain.resourcesNum == 1 && plain.resources[0].fields[0] == 0x1000u, "a vertex program's tables moved"); +} + void HeaderlessVertexStageTests() { const std::array userData{1, 2, 3, 4}; const auto info = AgcDriver::Graphics::DecodeVertexStageInfo({}, 0, userData); @@ -4316,6 +4354,7 @@ int main(int argc, char** argv) { PixelInputLayoutTests(); ColorPipeBankXorTests(); HeaderlessVertexStageTests(); + HiddenUserWordVertexTablesTests(); ComputeScratchTests(); shaderUserDataTailPaddingTests(); InitialContextTests();