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 <noreply@anthropic.com>
This commit is contained in:
Mouad-Scriptz-0 2026-10-11 13:13:01 +00:00
parent a11c8f7f70
commit b63996f1ff
5 changed files with 50 additions and 9 deletions

View File

@ -144,7 +144,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> 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);

View File

@ -618,7 +618,7 @@ std::unique_ptr<RegisteredPreparation> 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<PreparedGraphicsStage> 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<ShaderRecompiler::ShaderVertexStageInfo> 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;

View File

@ -18,7 +18,7 @@ struct DecodeRead {
std::uint64_t address;
std::vector<std::byte> bytes;
};
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads = nullptr, bool staticAbi = false);
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads = nullptr, bool staticAbi = false, std::uint32_t hiddenUserWords = 0);
}

View File

@ -162,7 +162,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
};
}
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads, bool staticAbi) {
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* 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<const st
}
if (vertexAttribReg < 0) return info;
if (vertexBufferReg < 0) throw std::runtime_error("AGC graphics: vertex attribute table requires a vertex buffer table");
if (static_cast<std::uint32_t>(vertexBufferReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex buffer table pointer exceeds the user-SGPR domain");
if (static_cast<std::uint32_t>(vertexAttribReg) + 1u >= userData.size()) throw std::runtime_error("AGC graphics: vertex attribute table pointer exceeds the user-SGPR domain");
const auto bufferWord = static_cast<std::uint32_t>(vertexBufferReg) + hiddenUserWords;
const auto attribWord = static_cast<std::uint32_t>(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<ShaderSemantic, ShaderRecompiler::ShaderVertexStageInfo::MaxResources> semantics{};
_readHeaderArray(header, headerAddress, shader.input_semantics, shader.num_input_semantics, semantics.data());
const auto attribTableAddr = static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexAttribReg)]) | (static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexAttribReg) + 1u]) << 32u);
const auto bufferTableAddr = static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexBufferReg)]) | (static_cast<std::uint64_t>(userData[static_cast<std::uint32_t>(vertexBufferReg) + 1u]) << 32u);
const auto attribTableAddr = static_cast<std::uint64_t>(userData[attribWord]) | (static_cast<std::uint64_t>(userData[attribWord + 1u]) << 32u);
const auto bufferTableAddr = static_cast<std::uint64_t>(userData[bufferWord]) | (static_cast<std::uint64_t>(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;

View File

@ -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<std::byte> header(semanticOffset + sizeof(ShaderSemantic));
static std::array<std::uint32_t, 4> bufferTable{0x1000u, 0x20000u, 64u, 0x3000u};
static std::array<std::uint32_t, 1> attribTable{(0x2cu << 5u)};
std::array<std::uint16_t, ShaderRegs::AGC_DIRECT_RESOURCE_TYPE_COUNT> direct{};
direct.fill(ShaderRegs::AGC_ILLEGAL_DIRECT_OFFSET);
direct[static_cast<std::size_t>(ShaderRegs::AgcDirectResourceType::PtrVertexBufferTable)] = 4;
direct[static_cast<std::size_t>(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<std::uint16_t*>(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<ShaderUserData*>(header.data() + userDataOffset);
shader.input_semantics = reinterpret_cast<ShaderSemantic*>(header.data() + semanticOffset);
shader.num_input_semantics = 1;
std::memcpy(header.data(), &shader, sizeof(shader));
const auto headerAddress = reinterpret_cast<std::uintptr_t>(header.data());
const auto low = [](const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer)); };
const auto high = [](const void* pointer) { return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer) >> 32u); };
const std::array<std::uint32_t, 8> registers{0x31016facu, 0x7efff390u, 0x100002u, 0x49u, low(bufferTable.data()), high(bufferTable.data()), low(attribTable.data()), high(attribTable.data())};
std::vector<std::uint32_t> 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<void>(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<std::uint32_t, 4> 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();