From 75720e0deb0bbe7a329514c74a642f33dbc60032 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Mon, 19 Oct 2026 23:50:30 -0300 Subject: [PATCH 1/3] fix(agc): decode a fast-launch draw as one point per subgroup whatever its input type --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 11 ++++++----- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 10 +++++++--- core/libs/prx/libSceAgcDriver/tests/Submit.cpp | 2 ++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 090a68abc1fb..2c20485866de 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1009,16 +1009,17 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto group = read(queue.userConfig, 0x25b, RegisterBank::UserConfig); const auto vertices = (group >> 9u) & 0x1ffu; const auto primitives = group & 0x1ffu; - validate(!fastLaunch || (!vertexWorkgroup && primitive == 1 && vertices == 1 && primitives == 1), "fast launch other than one point per subgroup is unsupported"); + validate(!fastLaunch || (!vertexWorkgroup && vertices == 1 && primitives == 1), "fast launch other than one vertex and one primitive per subgroup is unsupported"); + const auto inputPrimitive = fastLaunch ? 1u : primitive; const auto maxVertices = read(queue.context, 0x1ff); const auto verticesPerPrimitive = vertexWorkgroup ? 3u : read(queue.context, 0x2ce); const auto outputType = read(queue.context, 0x29b); validate(outputType == 2 || (outputType == 1 && !vertexWorkgroup), "VGT_GS_OUT_PRIM_TYPE other than a triangle strip, or a line strip from a geometry program, is unsupported"); const auto outputSize = outputType == 1 ? 2u : 3u; - validate((primitive == 1 || primitive == 2 || primitive == 4 || primitive == 5 || primitive == 6) && verticesPerPrimitive >= outputSize, "unsupported geometry input or output assembly"); - const auto inputSize = primitive == 1 ? 1u : primitive == 2 ? 2u : 3u; + validate((inputPrimitive == 1 || inputPrimitive == 2 || inputPrimitive == 4 || inputPrimitive == 5 || inputPrimitive == 6) && verticesPerPrimitive >= outputSize, "unsupported geometry input or output assembly"); + const auto inputSize = inputPrimitive == 1 ? 1u : inputPrimitive == 2 ? 2u : 3u; 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 inputStep = inputPrimitive == 5 || inputPrimitive == 6 ? 1u : inputSize; 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"); @@ -1031,7 +1032,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto threads = std::max({(groupPrimitives - 1u) * inputStep + inputSize, primitives, maxVertices, maxPrimitives}); const auto raster = find(queue.context, 0x205); const auto provokingVertex = raster != queue.context.end() && (raster->second & (1u << 19u)) != 0 ? outputSize - 1u : 0u; - result.mesh = ShaderRecompiler::MeshConfiguration{primitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, provokingVertex, esgsItemSize, outputSize}; + result.mesh = ShaderRecompiler::MeshConfiguration{inputPrimitive, groupPrimitives, (groupPrimitives - 1u) * inputStep + inputSize, maxVertices, maxPrimitives, ((threads + result.vertexWaveSize - 1u) / result.vertexWaveSize) * result.vertexWaveSize, ((resources >> 19u) & 0xffu) * 128u, provokingVertex, esgsItemSize, outputSize}; } return result; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 6e1f5d23ee2f..d06fb630ebd1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1031,10 +1031,14 @@ void ShaderStageTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "unsupported geometry routing, fast launch or wave-ID state"); fast.context[0x2d5] = 0x482030; fast.userConfig[0x25b] = 0x402; - expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one vertex and one primitive per subgroup"); fast.userConfig[0x25b] = 0x201; - fast.userConfig[0x242] = 4; - expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one point per subgroup"); + for (const std::uint32_t type : {2u, 3u, 4u, 5u, 6u}) { + fast.userConfig[0x242] = type; + const auto typed = AgcDriver::Graphics::DecodeState(fast); + const auto& typedMesh = *typed.stages.mesh; + Require(typedMesh.inputPrimitive == 1 && typedMesh.primitivesPerGroup == 1 && typedMesh.verticesPerGroup == 1 && typedMesh.maxPrimitives == 64, "a fast-launch draw with a line or triangle input type did not decode as one point per subgroup"); + } fast.userConfig[0x242] = 1; fast.context[0x2d3] = 0; expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "amplifies to no primitives"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index d97862941b77..38f39742eda4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -889,6 +889,8 @@ void testRegisteredFastLaunchWorkgroupVertex() { }}; const std::array primitive{{{0x242u, 1u}, {0x25bu, 0x201u}}}; AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); + const std::array strip{{{0x242u, 6u}, {0x25bu, 0x201u}}}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, strip); } void testWorkerFailure() { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6ec4e45f5cc5..c123c2008ac1 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -21,7 +21,7 @@ - [sceAgcDriverSetSubmitValidationMode, sceAgcDriverGetSubmitValidationMode, sceAgcDriverSetSubmitValidationConfig, sceAgcDriverGetSubmitValidationConfig, sceAgcDriverSetValidationErrorOutputFrequency, sceAgcDriverGetShaderDebuggingStatus](../../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp) (libSceAgcDriver) - the five validation calls return `0x8A6C1000` without reading their arguments or writing caller memory, and `sceAgcDriverGetShaderDebuggingStatus` returns 1, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L250-L291) reads them from Sony's libSceAgcDriver.sprx: each validation export is `mov eax, 0x8a6c1000; ret` in the copy Dragon Quest VII Reimagined bundles (0x7bb0-0x7bf0), and the debugging status is 1 in the firmware 5.50 library (0x6740). The parameters (a 32-bit mode or frequency, a mode output, a configuration pointer) follow OpenAGC and are never read. Whether later retail firmware enables submit validation or shader debugging is unverified; no title in this repository is known to call them - [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 - [Depth clear draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a draw with `DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE` is taken to write `DB_DEPTH_CLEAR` to every pixel it covers whatever the depth test, depth bounds and depth bias say: it is drawn with an always-passing depth write and the viewport depth range pinned to the clear value. The draw is rasterized as submitted, so its coverage is the draw's own. Whether the DB still applies the depth test or HiZ to such a draw is not measured -- [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. +- [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup whatever `VGT_PRIMITIVE_TYPE` holds, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, a point list whose vertex id gives the workgroup index), not measured on the console; for a line or triangle type (PPSA34547 uses a triangle strip) one subgroup per vertex is assumed. Fast launch 2, other group sizes and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. - [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots, because Mesa's lowering exports one primitive, possibly null, per emitted vertex; whether the console's compiler does the same is not known, and triangle strips keep `- 2` per input. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` as for triangles. Line output counts as lines for the polygon offset decode, as on the vertex path. The lines are drawn 1 pixel wide with Vulkan's default line rasterization, like lines on the vertex path: `PA_SU_LINE_CNTL` is not read, and neither its width nor the console's line rules were measured. Points, rectangle lists, per-stream types and line strips from a vertex program on the mesh path still throw. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. - [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. `PS_ITER_SAMPLE`, an `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) or overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throws. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. From f4a27403bfb7e648b7b10d9ec87a659bc2992588 Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Mon, 26 Oct 2026 20:38:08 -0300 Subject: [PATCH 2/3] test(agc): check what a fast-launch triangle strip prepares The agc_driver case that resolves the registered fast-launch program with a triangle strip passed with main's State.cpp too: the program is already deferred at registration, so the resolve defers the stage it cannot decode without logging anything. Resolve the strip first and require the point list to find the artifact the strip prepared (the same prepared entry hashes). With main's State.cpp the strip prepares nothing and the point list then prepares the program, so the case fails. A device that prepares the program for neither type has nothing to compare. --- core/libs/prx/libSceAgcDriver/tests/Submit.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 1b2ed4831ff7..50f216dd7fd5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -3,6 +3,7 @@ #include "execution/VulkanTestDevice.hpp" #include "prx/libSceAgcDriver/Execution/include/QueueState.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" @@ -884,10 +885,13 @@ void testRegisteredFastLaunchWorkgroupVertex() { const std::array context{{ {0x2d5u, 0x00482030u}, {0x1ffu, 32u}, {0x2ceu, 32u}, {0x2d3u, 64u}, {0x29bu, 2u}, {0x2abu, 4u}, }}; - const std::array primitive{{{0x242u, 1u}, {0x25bu, 0x201u}}}; - AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); + auto& driver = AgcDriver::DriverDetail::Driver::Get(); const std::array strip{{{0x242u, 6u}, {0x25bu, 0x201u}}}; AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, strip); + const auto prepared = driver.PreparedEntryHashes(&header.shader); + const std::array primitive{{{0x242u, 1u}, {0x25bu, 0x201u}}}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); + check(driver.PreparedEntryHashes(&header.shader) == prepared, "a registered fast-launch program resolved with a triangle strip was not prepared as for a point list"); } void testWorkerFailure() { From c65b4eff5f04f9587d9d220b8e87a654edff093c Mon Sep 17 00:00:00 2001 From: hugodearaujo Date: Thu, 29 Oct 2026 10:25:09 -0300 Subject: [PATCH 3/3] test(agc): decode a fast-launch draw of the polygon type as one point per subgroup --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 1381ef662e42..eeeed135ab50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1184,7 +1184,7 @@ void ShaderStageTests() { fast.userConfig[0x25b] = 0x402; expectFailure([&] { AgcDriver::Graphics::DecodeState(fast); }, "fast launch other than one vertex and one primitive per subgroup"); fast.userConfig[0x25b] = 0x201; - for (const std::uint32_t type : {2u, 3u, 4u, 5u, 6u}) { + for (const std::uint32_t type : {2u, 3u, 4u, 5u, 6u, 21u}) { fast.userConfig[0x242] = type; const auto typed = AgcDriver::Graphics::DecodeState(fast); const auto& typedMesh = *typed.stages.mesh;