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 <noreply@anthropic.com>
This commit is contained in:
alfalfa-star 2026-10-07 15:18:10 +02:00
parent e79c971f99
commit 5822c26b59
5 changed files with 19 additions and 7 deletions

View File

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

View File

@ -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<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress);
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw);
struct MeshArguments {
std::uint32_t groups;

View File

@ -601,11 +601,12 @@ void CheckBufferAliases(std::span<const CompiledShader> shaders, const ColorTarg
}
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress) {
const auto address = draw.indexed ? draw.indexAddress : unreadAddress;
const auto bytes = draw.indexed ? (static_cast<std::uint64_t>(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<std::uint32_t, 4> 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<std::uint64_t>(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<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, static_cast<std::uint32_t>(bytes), RawWord3};
}

View File

@ -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<void>(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();

View File

@ -122,7 +122,7 @@ struct MeshDraw {
void DrawMesh(AgcDriver::VulkanDevice& device, const MeshDraw& setup) {
const auto target = device.Target();
std::vector<std::uint32_t> userData(12, 0u);
const auto index = AgcDriver::Graphics::MeshIndexBufferDescriptor(setup.draw, reinterpret_cast<std::uintptr_t>(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<ShaderRecompiler::MemoryRegion, 1> geometryMemory{{{reinterpret_cast<std::uintptr_t>(GeometryCode.data()), std::as_bytes(std::span(GeometryCode))}}};