From a48b39e9339ba9855d6726f2109d66bab973d35b Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Thu, 8 Oct 2026 15:27:34 -0500 Subject: [PATCH 1/4] feat(agc): run unregistered NGG vertex programs that need a workgroup on the mesh path Co-Authored-By: Claude Opus 5.5 --- .../include/Driver/Shaders/ShaderRegistry.hpp | 2 ++ .../Execution/src/Driver/Draw/DrawDecode.cpp | 11 ++++++++++- .../src/Driver/Shaders/ShaderRegistry.cpp | 16 ++++++++++++++++ .../libSceAgcDriver/Graphics/include/State.hpp | 4 ++-- .../prx/libSceAgcDriver/Graphics/src/State.cpp | 18 ++++++++++-------- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 297686f5e..ffec875b5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -81,6 +81,7 @@ struct ShaderSnapshot { std::vector header; std::shared_ptr prepared = std::make_shared(); std::shared_ptr registeredState; + std::shared_ptr> workgroupUse = std::make_shared>(0u); }; @@ -88,6 +89,7 @@ std::shared_ptr ReadRawShader(std::uint64_t address); std::shared_ptr ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list types); void NoteRawGraphicsDraw(); bool RawGraphicsDrawn(); +bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset); std::shared_ptr PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index be0f1794d..1a8180fd9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -109,7 +109,16 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Submission& submission) { auto product = std::make_shared(); - product->state = Graphics::DecodeState(queue); + bool vertexWorkgroup = false; + const auto stages = queue.context.find(0x2d5); + const auto low = queue.shader.find(0xc8); + const auto high = queue.shader.find(0xc9); + if (stages != queue.context.end() && (stages->second & 0x02002024u) == 0x2000u && low != queue.shader.end() && high != queue.shader.end()) { + const auto address = (static_cast(low->second) << 8u) | (static_cast(high->second & 0xffu) << 40u); + const auto front = ProgramSnapshot(*submission.shaders, address, {2}); + vertexWorkgroup = front->header.empty() && ProgramUsesWorkgroup(*front, static_cast((address - front->codeAddress) / sizeof(std::uint32_t))); + } + product->state = Graphics::DecodeState(queue, vertexWorkgroup); DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true); product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue)); product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); 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 75fb76947..2d564856c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -131,6 +132,21 @@ void AwaitRegisteredPreparation(PreparedShaders& prepared, std::unique_lockload(std::memory_order_acquire); + if (cached != Unknown) return cached == Used; + } + const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(std::span(snapshot.code).subspan(codeOffset)); + const bool used = std::any_of(decoded.instructions.begin(), decoded.instructions.end(), [](const auto& instruction) { + return instruction.family == ShaderRecompiler::RdnaInstructionFamily::DS || instruction.op == ShaderRecompiler::RdnaOpcode::SBarrier; + }); + if (codeOffset == 0) snapshot.workgroupUse->store(used ? Used : Unused, std::memory_order_release); + return used; +} + + struct ShaderPreparationTransaction::State { struct Change { std::shared_ptr destination; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 05cb27663..b3a5e2970 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -109,8 +109,8 @@ struct State { std::array blendConstants; }; -ShaderStages DecodeShaderStages(const QueueState& queue); -State DecodeState(const QueueState& queue); +ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); +State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index f29320922..6098e17bd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -477,7 +477,7 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse } -ShaderStages DecodeShaderStages(const QueueState& queue) { +ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto value = read(queue.context, 0x2d5); const auto validate = [&](bool condition, const char* reason) { if (condition) return; @@ -489,7 +489,8 @@ ShaderStages DecodeShaderStages(const QueueState& queue) { validate((value & 3u) != 3u && ((value >> 3u) & 3u) != 3u && ((value >> 6u) & 3u) != 3u, "reserved LS_EN, ES_EN or VS_EN encoding"); const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig); const bool tessellation = primitive == 9; - const bool geometry = (value & 0x20u) != 0; + validate(!vertexWorkgroup || ((value & 0x02002020u) == 0x2000u && !tessellation), "a vertex program that needs a workgroup requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation"); + const bool geometry = (value & 0x20u) != 0 || vertexWorkgroup; validate(tessellation == ((value & 4u) != 0), "Patch topology and HS_EN disagree"); validate(!tessellation || !geometry, "combined tessellation and geometry is unsupported by the reference path"); const auto path = tessellation ? ShaderPath::Tessellation : geometry ? ShaderPath::Geometry : ShaderPath::Vertex; @@ -513,15 +514,16 @@ ShaderStages DecodeShaderStages(const QueueState& queue) { const auto vertices = (group >> 9u) & 0x1ffu; const auto primitives = group & 0x1ffu; const auto maxVertices = read(queue.context, 0x1ff); - const auto verticesPerPrimitive = read(queue.context, 0x2ce); + const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce); validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && read(queue.context, 0x29b) == 2 && verticesPerPrimitive >= 3, "unsupported geometry input or output assembly"); const auto inputSize = primitive == 1 ? 1u : primitive == 2 ? 2u : 3u; - validate(vertices >= inputSize && maxVertices != 0 && maxVertices <= 256 && verticesPerPrimitive <= 256, "invalid geometry subgroup output"); + validate(vertices >= inputSize && maxVertices != 0 && (!vertexWorkgroup || maxVertices >= inputSize) && maxVertices <= 256 && verticesPerPrimitive <= 256, "invalid geometry subgroup output"); const auto inputStep = primitive == 5 || primitive == 6 ? 1u : inputSize; - const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, maxVertices / verticesPerPrimitive}); + const auto outputLimit = vertexWorkgroup ? (maxVertices - inputSize) / inputStep + 1u : maxVertices / verticesPerPrimitive; + const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit}); validate(groupPrimitives != 0, "geometry subgroup contains no primitives"); const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader); - validate(((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3, "unsupported geometry VGPR allocation"); + validate((vertexWorkgroup || ((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3) && ((resources >> 16u) & 3u) == 3, "unsupported geometry VGPR allocation"); const auto esgsItemSize = read(queue.context, 0x2ab); validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)}); @@ -539,12 +541,12 @@ std::string DepthMaintenanceRejection(const QueueState& queue) { return zeroMessage(0x000, control->second, "DB_RENDER_CONTROL depth copy, resummarize or decompress"); } -State DecodeState(const QueueState& queue) { +State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (auto reason = DepthMaintenanceRejection(queue); !reason.empty()) throw std::runtime_error(reason); const auto& cx = queue.context; State result{}; result.conservativeRasterization = decodeConservativeRasterization(queue); - result.stages = DecodeShaderStages(queue); + result.stages = DecodeShaderStages(queue, vertexWorkgroup); const auto primitive = read(queue.userConfig, 0x242, RegisterBank::UserConfig); APS5_LOG_OUT_DEBUG("DecodeState primitive=%u path=%u vertexWave=%u", primitive, static_cast(result.stages.path), result.stages.vertexWaveSize); switch (primitive) { From 0a1b31f5c4e53015abd56696e4e01b4e0ea37d49 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Thu, 8 Oct 2026 15:31:07 -0500 Subject: [PATCH 2/4] test(agc): cover NGG vertex programs routed to the mesh path Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 31 +++++++++++++++++++ .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 16 ++++++++++ 2 files changed, 47 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7deea9485..516c3b165 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -468,6 +468,36 @@ void ShaderStageTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void NggVertexWorkgroupTests() { + auto queue = makeState(); + queue.context[0x2d5] = 0x12010; + queue.userConfig[0x242] = 4; + queue.userConfig[0x25b] = 0x10080; + queue.context[0x1ff] = 0x80; + queue.context[0x2ce] = 0; + queue.context[0x29b] = 2; + queue.context[0x2ab] = 1; + queue.shader[0x8a] = 0x222c0009; + queue.shader[0x8b] = 0x43001e; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + Require(AgcDriver::Graphics::DecodeState(queue).stages.path == AgcDriver::Graphics::ShaderPath::Vertex, "an NGG vertex program without a workgroup left the vertex path"); + auto state = AgcDriver::Graphics::DecodeState(queue, true); + Require(state.stages.path == AgcDriver::Graphics::ShaderPath::Geometry && state.stages.mesh, "an NGG vertex program that needs a workgroup did not take the mesh path"); + auto mesh = *state.stages.mesh; + Require(mesh.inputPrimitive == 4 && mesh.primitivesPerGroup == 42 && mesh.verticesPerGroup == 126 && mesh.maxVertices == 128 && mesh.maxPrimitives == 128 && mesh.threadsPerGroup == 128 && mesh.ldsSizeDwords == 1024 && mesh.esgsItemSize == 1, "the NGG vertex subgroup of a triangle list changed"); + queue.userConfig[0x242] = 6; + mesh = *AgcDriver::Graphics::DecodeState(queue, true).stages.mesh; + Require(mesh.primitivesPerGroup == 126 && mesh.verticesPerGroup == 128 && mesh.maxPrimitives == 128, "the NGG vertex subgroup of a triangle strip is not bounded by its vertex budget"); + queue.context[0x1ff] = 2; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "invalid geometry subgroup output"); + queue.context[0x1ff] = 0x80; + for (const auto stages : {0x02012010u, 0x00012030u, 0x00010010u}) { + queue.context[0x2d5] = stages; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "requires PRIMGEN_EN without GS_EN, PRIMGEN_PASSTHRU_EN or tessellation"); + } +} + constexpr std::array IdentityExports{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}; std::vector pixelBuiltinsRead(std::uint32_t ena, std::uint32_t addr, std::uint32_t source) { @@ -4043,6 +4073,7 @@ int main() { uint16ExportTests(); uint8x4TargetTests(); ShaderStageTests(); + NggVertexWorkgroupTests(); TuningFieldTests(); PixelInputLayoutTests(); ColorPipeBankXorTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 4137e06b3..e6a9d0384 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -602,6 +602,21 @@ void testProgramSnapshots() { check(AgcDriver::DriverDetail::DecodeProgramsCurrent(decode, shaders), "a decode with the rewritten program was not current"); } +void testProgramWorkgroupUse() { + const auto snapshot = [](std::vector code) { + AgcDriver::DriverDetail::ShaderSnapshot result{0x1000, 0, 2, std::move(code), {}}; + return result; + }; + const auto plain = snapshot({0xbf800000, 0xbf810000}); + check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(plain, 0) && !AgcDriver::DriverDetail::ProgramUsesWorkgroup(plain, 0), "a program without LDS or barriers needs a workgroup"); + const auto barrier = snapshot({0xbf8a0000, 0xbf810000}); + check(AgcDriver::DriverDetail::ProgramUsesWorkgroup(barrier, 0), "an s_barrier did not need a workgroup"); + const auto lds = snapshot({0xd8340000, 0x00000100, 0xbf810000}); + check(AgcDriver::DriverDetail::ProgramUsesWorkgroup(lds, 0) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(lds, 0), "an LDS write did not need a workgroup"); + const auto entry = snapshot({0xbf8a0000, 0xbf810000, 0xbf800000, 0xbf810000}); + check(!AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 2) && AgcDriver::DriverDetail::ProgramUsesWorkgroup(entry, 0), "the workgroup check did not start at the program's entry"); +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -694,6 +709,7 @@ int main() { check(!unterminated.empty() && bounded->code.size() == 64, "raw compute crossed inaccessible memory or missed its last instruction"); #endif testProgramSnapshots(); + testProgramWorkgroupUse(); testEvents(); testValidation(); testClearState(); From 0e3449129e1047d64c47a8b0858f23becda9c1e3 Mon Sep 17 00:00:00 2001 From: Scott Schuster Date: Thu, 8 Oct 2026 22:32:58 -0500 Subject: [PATCH 3/4] fix(agc): accept any ES VGPR count for NGG vertex programs on the mesh path vkQuake's NGG culling vertex shaders load only the vertex ID (SPI_SHADER_PGM_RSRC2_GS ES_VGPR_COMP_CNT 0), and DecodeShaderStages still required 3 for a program routed to the mesh path. The merged ES inputs keep their positions whatever the count, and the mesh entry loads all of them, so the count is now checked only for real geometry programs, as the GS VGPR count already was. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 6098e17bd..2785bf7bf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -523,7 +523,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto groupPrimitives = std::min({primitives, (vertices - inputSize) / inputStep + 1u, outputLimit}); validate(groupPrimitives != 0, "geometry subgroup contains no primitives"); const auto resources = read(queue.shader, 0x8b, RegisterBank::Shader); - validate((vertexWorkgroup || ((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3) && ((resources >> 16u) & 3u) == 3, "unsupported geometry VGPR allocation"); + validate(vertexWorkgroup || (((read(queue.shader, 0x8a, RegisterBank::Shader) >> 29u) & 3u) == 3 && ((resources >> 16u) & 3u) == 3), "unsupported geometry VGPR allocation"); const auto esgsItemSize = read(queue.context, 0x2ab); validate(esgsItemSize != 0 && esgsItemSize * vertices <= 0xffffu, "invalid VGT_ESGS_RING_ITEMSIZE"); const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, primitives * (verticesPerPrimitive - 2u)}); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 516c3b165..59ecfc305 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -489,6 +489,11 @@ void NggVertexWorkgroupTests() { queue.userConfig[0x242] = 6; mesh = *AgcDriver::Graphics::DecodeState(queue, true).stages.mesh; Require(mesh.primitivesPerGroup == 126 && mesh.verticesPerGroup == 128 && mesh.maxPrimitives == 128, "the NGG vertex subgroup of a triangle strip is not bounded by its vertex budget"); + queue.shader[0x8a] = 0x222c0005; + queue.shader[0x8b] = 0x400030; + Require(AgcDriver::Graphics::DecodeState(queue, true).stages.mesh.has_value(), "an NGG vertex program that loads only its vertex ID was rejected"); + queue.shader[0x8a] = 0x222c0009; + queue.shader[0x8b] = 0x43001e; queue.context[0x1ff] = 2; expectFailure([&] { AgcDriver::Graphics::DecodeState(queue, true); }, "invalid geometry subgroup output"); queue.context[0x1ff] = 0x80; From fdc0b5268be96fbe279907bf0f1c533eef30edb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 07:56:42 +0200 Subject: [PATCH 4/4] docs(technical-debt): record the NGG vertex mesh routing assumptions --- docs/dev/TechnicalDebt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8dae145c5..eb9495555 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -126,6 +126,7 @@ Throughout the project, every function at every stage either **does exactly what - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws - [Vertex entry merged wave info](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) - `s3` of every vertex-path shader is set to `waveSize | waveSize << 8` (vertex and primitive count of a full wave, wave index 0). Layout taken from the prologue Mesa's ACO emits for NGG vertex shaders (`s_bfe_u64 exec` from `s3`), not measured on hardware. The driver gives every graphics program user data from `s8`, so the condition does not tell NGG from legacy vertex shaders: they just never read `s3`. A draw whose last wave is partial still reports a full wave. +- [NGG vertex programs on the mesh path](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - an unregistered vertex program with `PRIMGEN_EN` that uses LDS or `s_barrier` runs on the geometry (mesh) path. Each input primitive is assumed to yield at most one output primitive, the subgroup is bounded by `GE_MAX_OUTPUT_PER_SUBGROUP` as a vertex budget, and the GS/ES VGPR count checks are skipped, because the merged inputs keep their positions. Taken from the code Mesa's ACO emits for NGG culling and from PPSA59322 and PPSA99010 draws, not measured on hardware. Registered programs keep the vertex path - [s_barrier in vertex-path programs](../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Programs with LDS, GDS or memory writes throw. Not measured on hardware - [Strided buffer overhang](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a strided buffer descriptor that ends less than one stride past the end of the readable registered allocation holding its base is bound only up to that allocation's end, even when another readable allocation starts right there. This assumes the bytes past the end are the rounded-up `num_records` of the last record (RADV rounds it up), which no fetch inside the buffer reads; a guest that really keeps a vertex split across two adjacent allocations would read zeros for the cut tail. Taken from the RetroArch Dolphin core case in #2912, not measured on hardware. - [Zero-stride vertex fetch range](../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - for a stride-0 V# with OOB_SELECT 2 the whole fetch reads memory unless NUM_RECORDS is 0; with OOB_SELECT 3 it reads zero when NUM_RECORDS is 0 and throws when the fetch only partly fits. Rules taken from the GFX10 OOB_SELECT table in Mesa [ac_descriptors.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/ac_descriptors.c), not measured on hardware. Whether OOB_SELECT 3 checks each dword of a multi-dword fetch separately is unknown. OOB_SELECT 0 and 1 keep the structured record checks.