fix(agc-driver): move indices far past every vertex fetch's records to the first zero vertex

Goat Simulator 3 draws a triangle list whose indices are all 0xffffffff
with primitive restart off. Each fetch past a descriptor's records reads
zeros (out-of-bounds select 0 or 1), but the zero-padded vertex copies
were sized to the highest index: 160 GiB, which vkAllocateMemory refused.
When the highest index is at least 64Ki vertices past the largest
per-vertex record count, the draw now uses a copy of its indices with
every index past that count moved to it. Each attribute reads zeros
there as at the original index; only the vertex index the shader sees
changes, so indices closer to the records are left as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Tyler Westerberg 2026-10-10 20:53:13 -04:00
parent 436e310ddb
commit a3cba2d1ff
4 changed files with 78 additions and 0 deletions

View File

@ -227,6 +227,45 @@ inline VertexCopyPlan PlanVertexCopies(std::span<const VertexFetch> fetches) {
return plan;
}
inline std::optional<std::uint32_t> ZeroFetchVertex(std::span<const ShaderRecompiler::VertexAttribute> attributes) {
std::uint32_t zeroFrom = 0;
for (const auto& attribute : attributes) {
const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu;
if (NullVertexDescriptor(attribute) || attribute.fetchIndex != 0 || stride == 0) continue;
if (VertexBufferOutOfBoundsSelect(attribute) > 1u) return std::nullopt;
zeroFrom = std::max(zeroFrom, attribute.resource.fields[2]);
}
return zeroFrom;
}
inline std::optional<std::uint32_t> ClampDrawIndices(std::span<std::byte> indices, std::uint32_t indexSize, bool skipRestart, std::uint32_t limit) {
Require(indexSize == 2 || indexSize == 4, "unsupported index size");
const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu;
std::optional<std::uint32_t> highest;
for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) {
std::uint32_t index = 0;
if (indexSize == 2) {
std::uint16_t value = 0;
std::memcpy(&value, indices.data() + offset, sizeof(value));
index = value;
} else {
std::memcpy(&index, indices.data() + offset, sizeof(index));
}
if (skipRestart && index == restartIndex) continue;
if (index > limit) {
index = limit;
if (indexSize == 2) {
const auto value = static_cast<std::uint16_t>(index);
std::memcpy(indices.data() + offset, &value, sizeof(value));
} else {
std::memcpy(indices.data() + offset, &index, sizeof(index));
}
}
highest = std::max(highest.value_or(0u), index);
}
return highest;
}
inline std::optional<std::uint32_t> HighestDrawIndex(std::span<const std::byte> indices, std::uint32_t indexSize, bool skipRestart) {
Require(indexSize == 2 || indexSize == 4, "unsupported index size");
const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu;

View File

@ -1115,6 +1115,24 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
inputs.nothing = true;
return inputs;
}
constexpr std::uint32_t ZeroFetchSlack = 0x10000u;
if (const auto zeroFrom = state.stages.mesh ? std::nullopt : ZeroFetchVertex(shaders.front().program->vertexAttributes)) {
const auto limit = *zeroFrom > draw.firstVertex ? *zeroFrom - draw.firstVertex : 0u;
if (*highest >= limit && *highest - limit >= ZeroFetchSlack) {
GuestMemory::FlushGpuWrites(draw.indexAddress, static_cast<std::size_t>(indexBytes));
auto clamped = std::make_shared<Buffer>(context, static_cast<std::size_t>(indexBytes), VK_BUFFER_USAGE_INDEX_BUFFER_BIT);
const auto bytes = clamped->Bytes().first(static_cast<std::size_t>(indexBytes));
GuestMemory::Read(draw.indexAddress, bytes, draw.indexSize);
highest = ClampDrawIndices(bytes, draw.indexSize, skipRestart, limit);
import = nullptr;
copy = DrawInputCopy{};
copy.buffer = std::move(clamped);
if (!highest) {
inputs.nothing = true;
return inputs;
}
}
}
Require(*highest <= context.limits.maxDrawIndexedIndexValue, "index exceeds the device's indexed draw limit");
Require(!fanGeometry || !state.primitiveRestart || *highest != (draw.indexSize == 2 ? 0xffffu : 0xffffffffu), "primitive restart in a triangle fan geometry draw is unsupported");
inputs.maxIndex = *highest;

View File

@ -4215,6 +4215,26 @@ void validationTests() {
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "metadata disagrees");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 2, 1) == 80, "incorrect strided vertex range");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 3, 1) == 112, "an index past the records still spans its fetch");
{
auto wider = attribute;
wider.resource.fields[2] = 9;
auto instanced = attribute;
instanced.fetchIndex = 1;
instanced.resource.fields[2] = 1000;
auto constant = attribute;
constant.resource.fields[1] = 0;
const std::array attributes{attribute, wider, instanced, constant};
Require(AgcDriver::Graphics::ZeroFetchVertex(attributes) == 9u, "the first vertex reading zeros is not past the largest per-vertex record count");
auto clamped = attribute;
clamped.resource.fields[3] |= 2u << 28u;
const std::array selective{attribute, clamped};
Require(!AgcDriver::Graphics::ZeroFetchVertex(selective).has_value(), "an attribute with out-of-bounds select 2 was taken to read zeros past its records");
std::array<std::uint32_t, 4> indices{0xffffffffu, 2u, 0x12345u, 0xffffffffu};
Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, true, 9u) == 9u && indices == std::array<std::uint32_t, 4>{0xffffffffu, 2u, 9u, 0xffffffffu}, "clamping kept an index past the limit or moved a restart index");
Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(indices)), 4, false, 9u) == 9u && indices == std::array<std::uint32_t, 4>{9u, 2u, 9u, 9u}, "clamping without restart kept a 0xffffffff index");
std::array<std::uint16_t, 2> narrow{0xffffu, 1u};
Require(AgcDriver::Graphics::ClampDrawIndices(std::as_writable_bytes(std::span(narrow)), 2, false, 4u) == 4u && narrow[0] == 4u && narrow[1] == 1u, "clamping 16-bit indices failed");
}
attribute.fetchIndex = 1;
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2) == 48, "instance attributes used the vertex index");
Require(AgcDriver::Graphics::VertexBufferReadSize(attribute, 100, 2, 1) == 80, "first instance was ignored");

View File

@ -61,6 +61,7 @@ Throughout the project, every function at every stage either **does exactly what
- [kern.sdk_version](../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` reports the system software version as the 4-byte value `0x01000000` (1.00), the value `sceKernelGetSystemSwVersion` reports; a write returns -1 with `EPERM`. The major version is byte 3 and the minor version byte 2, in hex digits that read as decimal (13.60 is `0x1360xxxx`), as [Capy Mobile](https://github.com/khalifa007/CapyMobile/blob/2758c016/src/ps5/platform.c#L64-L71) (PPSA92280), [remote_lua_loader](https://github.com/n0llptr/remote_lua_loader/blob/7c2840e475/savedata/misc.lua#L195-L209) and the [lapse payload](https://github.com/NetflixNHack/Netflix-N-Hack/blob/605eb93ba5/PS4/payloads/lapse.js#L436-L450) read it. Capy Mobile only displays it. The console's firmware version is not modeled and the low two bytes are unknown
- PPSA01530, PPSA23827 and PPSA13356 imports declared without parameters, signatures unknown, added without an implementation because the titles fail to load without them: [sceNetShowIfconfig](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet), [sce::Json::Value::setNullAccessCallBack](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2), [kqueue and kevent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel), and [scePadUnknown_n3kSX62fgNo](../../core/libs/prx/libScePad/Export.cpp) (libScePad), whose real name is unknown. [_sceUltWaitingQueueResourcePoolCreate, _sceUltSemaphoreCreate, _sceUltMutexOptParamInitialize, _sceUltMutexCreate, _sceUltQueueDataResourcePoolCreate and _sceUltQueueCreate](../../core/libs/prx/libSceUlt/Export.cpp) (libSceUlt) forward to the exports without the underscore; that their argument lists match is inferred from the other underscore Ult exports, not measured.
- [sceVoiceQoSSetConnectionAttribute](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS, NID `KCGOxg8iX7s`) is a throwing stub declared without parameters, like the other endpoint calls of the library: PPSA04159 fails to load without it and its signature is unknown. The name hashes to the imported NID (`tools/nid_names.py`)
- [Indices far past every vertex fetch](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - when a draw's highest index is at least 64Ki vertices past the largest per-vertex record count, every index past that count is moved to it in a copy of the indices, so the zero-padded vertex copies are not sized to the original index (PPSA04159 draws a list of `0xffffffff` with primitive restart off, which asked for 160 GiB). Each attribute fetched by vertex index reads zeros there as at the original index (out-of-bounds select 0 or 1); a draw with another out-of-bounds select keeps its indices. The vertex index the shader sees changes for the moved indices, which a shader that reads it directly would notice
- [sceVideoOutGetOutputStatusWithVrr](../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - the name hashes to the imported NID. PPSA26344 version 02.002.000 passes a VideoOut handle and an output pointer at three call sites. It compares a 32-bit value at byte 4 with 2, tests bit 0x10 at byte 16, and, when set, compares low 16-bit values at bytes 48, 50 and 52 with 48, 60 and 120. The full output layout, size, field meanings and console errors are unknown. Every call throws without reading or writing caller memory.
- [sceHttp2WebSocketSetPingInterval](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2, NID `YfaXPnc8PJE`) - imported by `libdiscord_partner_sdk.prx` of PPSA23126 (issue 2648). The parameters, the unit of the interval and the console errors are unknown. Every call throws.
- [sceKernelGetProsperoSystemSwVersion](../../core/libs/prx/libkernel/System/src/SoftwareVersion.cpp) (libkernel) reports system software version `0x01000000` ("01.000.000"); the console's firmware version is not modeled. The 0x28-byte block (a 64-bit size at 0, the version string at 8 and the packed version at 0x24) follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs), which Prospero Multi Tools (PPSA99110) and Prospero Explorer (PPSA99109) are built with and which sets the size to 40, and the same fields in [etaHEN](https://github.com/etaHEN/etaHEN/blob/main/Source%20Code/daemon/include/ipc.hpp) and [shsrv](https://github.com/ps5-payload-dev/shsrv/blob/master/sh.c). A size other than 0x28 returns `SCE_KERNEL_ERROR_EINVAL` without writing; whether the console checks the size is unverified, since etaHEN and shsrv declare a 0x30-byte block and do not set it