Merge pull request #4556 from Amoenus/codex/fix-inactive-covered-centroid

fix(agc): admit inactive covered-centroid mode
This commit is contained in:
Adrià 2026-10-13 16:43:05 +02:00 committed by GitHub
commit b4d76ba609
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 72 additions and 2 deletions

View File

@ -1560,8 +1560,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:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)

View File

@ -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");
@ -699,6 +710,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);
@ -1262,7 +1274,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) {

View File

@ -2102,6 +2102,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<AgcDriver::Graphics::RegisterRead> 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");
@ -5505,6 +5553,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{};

View File

@ -34,6 +34,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