From 5822c26b59a1a75327390ad5bc148f9db81ea992 Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Wed, 7 Oct 2026 15:18:10 +0200 Subject: [PATCH] fix(agc): bind a null index buffer to non-indexed mesh draws A non-indexed mesh draw (DRAW_INDEX_AUTO, or a non-indexed indirect record) gave the mesh program's hidden index buffer V# (user words 4..7) a 4-byte range at the program's own code address, so the descriptor had a guest address although the prologue never loads through it (the load is predicated on the draw's index size). That range is bound from live guest memory. When the program also reads guest memory by address, the address build captures the program's code as an immutable snapshot, and the two overlapped: the draw failed with "writable guest memory overlaps an immutable snapshot", or, when the live bytes at the code address had changed since the capture, "guest snapshot differs from the writable memory it overlaps". MeshIndexBufferDescriptor now returns a null V# (base and size 0, same format word) for a non-indexed draw, which ShaderResources binds to its empty placeholder buffer, and takes no unread address any more. PPSA14632 (LEGO Horizon Adventures) hit this on its layered mesh-path draws. Co-Authored-By: Claude Opus 5.5 --- .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/include/Draw.hpp | 2 +- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 9 +++++---- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 11 +++++++++++ core/libs/prx/libSceAgcDriver/tests/Mesh.cpp | 2 +- 5 files changed, 19 insertions(+), 7 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 5634d788a..97c2905da 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -91,7 +91,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe if (!graphics.stages.mesh) return; auto& words = programs.front().userData; require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words"); - const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters, programs.front().binary.codeAddress); + const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters); std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord); }; if (!drawParameters.indirect) setMeshIndexBuffer(drawParameters); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index e8fc9a7c2..078e05bb2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -28,7 +28,7 @@ struct DrawInputCopy { DrawInputCopy CopyDrawInput(const Context& context, Recorder* recorder, std::uint64_t address, std::size_t bytes, std::size_t alignment, Recorder::SnapshotUse use); void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived); -std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress); +std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw); struct MeshArguments { std::uint32_t groups; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index cd8095a8f..2ddc3a9a2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -601,11 +601,12 @@ void CheckBufferAliases(std::span shaders, const ColorTarg } -std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress) { - const auto address = draw.indexed ? draw.indexAddress : unreadAddress; - const auto bytes = draw.indexed ? (static_cast(draw.indexCount) * draw.indexSize + 3u) & ~std::uint64_t{3} : 4u; - Require(address != 0 && bytes != 0 && bytes <= 0xffffffffu && (address >> 48u) == 0, "invalid mesh index buffer range"); +std::array MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw) { constexpr std::uint32_t RawWord3 = 0x31016facu; + if (!draw.indexed) return {0u, 0u, 0u, RawWord3}; + const auto address = draw.indexAddress; + const auto bytes = (static_cast(draw.indexCount) * draw.indexSize + 3u) & ~std::uint64_t{3}; + Require(address != 0 && bytes != 0 && bytes <= 0xffffffffu && (address >> 48u) == 0, "invalid mesh index buffer range"); return {static_cast(address), static_cast(address >> 32u) & 0xffffu, static_cast(bytes), RawWord3}; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 2d45534e2..c70920772 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1977,6 +1977,16 @@ bool recompilesDebugBranch(std::uint32_t opcode) { return !ShaderRecompiler::Recompile(request).spirv.Words().empty(); } +void meshIndexBufferTests() { + const AgcDriver::Pm4::DrawParameters automatic{0, 96, 0, 1, 0, false}; + const auto unindexed = AgcDriver::Graphics::MeshIndexBufferDescriptor(automatic); + Require(unindexed[0] == 0 && unindexed[1] == 0 && unindexed[2] == 0 && unindexed[3] == 0x31016facu, "a non-indexed mesh draw did not get a null index buffer V#"); + const AgcDriver::Pm4::DrawParameters indexed{0x123456789a00ull, 5, 2, 1, 0, true}; + const auto words = AgcDriver::Graphics::MeshIndexBufferDescriptor(indexed); + Require(words[0] == 0x56789a00u && words[1] == 0x1234u && words[2] == 12u && words[3] == 0x31016facu, "an indexed mesh draw's index buffer V# changed"); + expectFailure([] { static_cast(AgcDriver::Graphics::MeshIndexBufferDescriptor(AgcDriver::Pm4::DrawParameters{0, 3, 2, 1, 0, true})); }, "invalid mesh index buffer range"); +} + void meshArgumentTests() { using AgcDriver::Graphics::MeshArguments; using AgcDriver::Graphics::ResolveMeshArguments; @@ -2105,6 +2115,7 @@ int main() { misalignedShaderDataTests(); debugBranchTests(); meshArgumentTests(); + meshIndexBufferTests(); validationTests(); vertexCopyTests(); pixelParameterSlotTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp index 025817a38..3257e4d73 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Mesh.cpp @@ -122,7 +122,7 @@ struct MeshDraw { void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) { const auto target = device.Target(); std::vector userData(12, 0u); - const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw, reinterpret_cast(GeometryCode.data())); + const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw); std::copy(index.begin(), index.end(), userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord); std::copy(setup.vertexBuffer.begin(), setup.vertexBuffer.end(), userData.begin() + 8); const std::array geometryMemory{{{reinterpret_cast(GeometryCode.data()), std::as_bytes(std::span(GeometryCode))}}};