From 44c4b3f8bf6c42c6d8c0ecb73e69ca79c71c4341 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Mon, 12 Oct 2026 18:34:17 +0300 Subject: [PATCH] fix(agc): admit inactive covered-centroid mode Admit PA_SC_AA_CONFIG bit29 only without active perspective or linear centroid inputs, or with a skipped pixel program. Active/unspecified centroid inputs and unrelated AA fields remain refused. Source: pinned PS5_Mesa 0b2d6d1a GFX10.3 schema/RADV producer; existing ENA/ADDR intersection. Console covered-centroid interpolation remains unmeasured. Full build/all 124 runtime libraries, conventions and registration pass. Final Windows suite: 965 pass, zero fail, seven skips of 972. Isolated state test passes 20 repeats and fails 5/5 with main State.cpp; four guard mutants fail 5/5 each. Original two title register captures reproduce the main refusal and pass this check with candidate, five times each. NVIDIA relevant groups pass; AMD draw-cache failure also occurs on unchanged main and is reported in issue4435. AI-assisted: Codex. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 ++ .../libSceAgcDriver/Graphics/src/State.cpp | 17 +++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 53 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d0bf61b2a..d9fe1ee1b 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1503,8 +1503,11 @@ if(BUILD_TESTING) configure_windows_unwind(agc_compute_pipeline_capture_tests) add_test(NAME agc_compute_descriptor_layout_flags COMMAND agc_driver_graphics_tests --descriptor-layout-flags) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_covered_centroid_state COMMAND agc_driver_graphics_tests --covered-centroid-state) + set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 404f0ccef..9cffc2ccd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -354,9 +354,20 @@ std::string singleSampleMsaaRejection(const QueueState& queue) { return {}; } +constexpr std::uint32_t CoveredCentroidIsCenter = 1u << 29u; +constexpr std::uint32_t CentroidInputs = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveCentroid) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::LinearCentroid); + +bool coveredCentroidSupported(const QueueState& queue) { + const auto config = find(queue.context, 0x2f8); + if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true; + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0; +} + std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); - zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); + zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); const auto log2 = config & 7u; if (log2 == 0) { Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported"); @@ -698,6 +709,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(std::popcount(read(cx, 0x207) & 0xffffu) <= 8, "more than eight clip and cull distances are unsupported"); result.paClVsOutCntl = read(cx, 0x207); result.samples = decodeSamples(cx); + Require(coveredCentroidSupported(queue), "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"); decodeSampleLocations(cx, result); { const auto depthControl = read(cx, 0x200); @@ -1261,7 +1273,8 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) { if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; } - if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; + if (!coveredCentroidSupported(queue)) return "AGC graphics: COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs is not implemented"; const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0; if (auto reason = nonzero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); !reason.empty()) return reason; if (!multisampled && value(cx, 0x292, word) && (word & 1u) != 0) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index a6c4230d0..b3db26deb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2100,6 +2100,54 @@ void multisampleTests() { state = DecodeState(queue); Require(state.samples == 8 && !state.customSampleLocations && state.colors.size() == 1 && state.colors[0].samples == 8, "the standard 8x pattern (sample 7 at (7, -7), PPSA99902's registers) did not decode as standard"); AgcDriver::Graphics::ValidateSampleLocations({}, state); + for (const auto log2 : {0u, 1u, 2u, 3u}) { + queue = log2 == 0 ? makeState() : multisampled(); + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u; + queue.context[0x2f8] = (log2 << 20u) | log2 | 0x20000000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = log2 == 3 ? 0xbd153fd1u : log2 == 2 ? 0x622ae6aeu : 0xcc44u; + queue.context[0x2ff + pixel * 4u] = log2 == 3 ? 0x9773f95bu : 0u; + } + std::vector sampleReads; + AgcDriver::Graphics::RegisterReadLog() = &sampleReads; + const auto coveredCenter = DecodeState(queue); + const auto coveredRejection = AgcDriver::Graphics::DrawRejection(queue, false); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + Require(coveredCenter.samples == (1u << log2) && !coveredCenter.customSampleLocations, "covered-centroid mode changed the sample count or locations without centroid inputs"); + for (const auto read : sampleReads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a covered-centroid register"); + Require(coveredRejection.empty(), "covered-centroid mode without centroid inputs was refused: " + coveredRejection); + for (const auto centroid : {4u, 64u, 68u}) { + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted active covered-centroid interpolation"); + queue.context[0x1b4] = 2u; + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "an unaddressed centroid input changed covered-centroid mode"); + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u | centroid; + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a disabled centroid input changed covered-centroid mode"); + } + queue.context[0x1b3] = 2u; + queue.context[0x1b4] = 2u; + for (const auto offset : {0x1b3u, 0x1b4u}) { + queue.context.erase(offset); + expectFailure([&] { DecodeState(queue); }, "COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("COVERED_CENTROID_IS_CENTER with active or unspecified centroid inputs") != std::string::npos, "the precheck accepted unspecified centroid inputs"); + queue.context[offset] = 2u; + } + const auto withProgram = queue; + queue.shader[0x008] = 0; + queue.context[0x8e] = 0; + queue.context.erase(0x1b3); + queue.context.erase(0x1b4); + Require(DecodeState(queue).samples == (1u << log2) && AgcDriver::Graphics::DrawRejection(queue, false).empty(), "covered-centroid mode was refused with no pixel program"); + queue = withProgram; + queue.context[0x2f8] |= 0x10u; + expectFailure([&] { DecodeState(queue); }, "coverage conversion or nonstandard multisampling"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).find("coverage conversion or nonstandard multisampling") != std::string::npos, "covered-centroid handling admitted unrelated AA bits"); + } queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); @@ -5464,6 +5512,11 @@ int main(int argc, char** argv) { std::cerr << "descriptor pool reset failure did not terminate\n"; return 1; } + if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") { + multisampleTests(); + std::cout << "Covered-centroid and multisample state checks passed\n"; + return 0; + } { const AgcDriver::Graphics::Context context{}; const AgcDriver::Graphics::State state{}; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c9e77d176..566f4a91e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -33,6 +33,7 @@ - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. +- [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. - [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline