Merge pull request #5915 from SandstromPL/fix/agc-single-sample-scan-multisampled-depth

fix(agc): rasterize a single-sample scan into a multisampled depth target at the pixel center
This commit is contained in:
Adrià authored and GitHub committed 2026-10-17 12:00:13 +02:00
commit d7262bd6d6
3 files changed
+143 -15

No files matched your search

@@ -384,6 +384,29 @@ constexpr std::array<std::array<std::int8_t, 2>, 2> StandardLocations2{{{4, 4},
constexpr std::array<std::array<std::int8_t, 2>, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}};
constexpr std::array<std::array<std::int8_t, 2>, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}};
std::string nonTriangleRejection(const QueueState& queue, const char* message, bool rectangles = false) {
const auto word = [](const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) -> std::optional<std::uint32_t> {
const auto it = find(registers, offset, bank);
if (it == registers.end()) return std::nullopt;
return it->second;
};
const auto& cx = queue.context;
const auto stages = word(cx, 0x2d5);
if (!stages) return {};
if ((*stages & 0x20u) != 0) {
if (const auto output = word(cx, 0x29b); output && ((*output & 0x80000000u) != 0 || (*output & 0x3fu) != 2u)) return zeroMessage(0x29b, *output, message);
} else if ((*stages & 4u) != 0) {
if (const auto parameters = word(cx, 0x2db)) {
const auto domain = *parameters & 3u;
const auto topology = (*parameters >> 5u) & 7u;
if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, message);
}
} else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u && !(rectangles && (*primitive == 7u || *primitive == 17u))) {
return zeroMessage(0x242, *primitive, message);
}
return {};
}
std::string singleSampleMsaaRejection(const QueueState& queue) {
const auto& cx = queue.context;
const auto word = [](const Registers& registers, std::uint32_t offset, RegisterBank bank = RegisterBank::Context) -> std::optional<std::uint32_t> {
@@ -397,20 +420,19 @@ std::string singleSampleMsaaRejection(const QueueState& queue) {
}
if (const auto alphaToMask = word(cx, 0x2dc); alphaToMask && (*alphaToMask & 1u) != 0 && !(*alphaToMask == AlphaToCoverageDithered && alphaToCoverageDisabled(cx))) return zeroMessage(0x2dc, *alphaToMask, "MSAA_ENABLE on a single-sample target with alpha-to-coverage");
if (const auto conservative = word(cx, 0x313); conservative && (*conservative & ~0x001f6000u) != 0) return zeroMessage(0x313, *conservative, "MSAA_ENABLE on a single-sample target with conservative rasterization");
constexpr const char* primitives = "MSAA_ENABLE on a single-sample target for primitives other than triangles";
const auto stages = word(cx, 0x2d5);
if (!stages) return {};
if ((*stages & 0x20u) != 0) {
if (const auto output = word(cx, 0x29b); output && ((*output & 0x80000000u) != 0 || (*output & 0x3fu) != 2u)) return zeroMessage(0x29b, *output, primitives);
} else if ((*stages & 4u) != 0) {
if (const auto parameters = word(cx, 0x2db)) {
const auto domain = *parameters & 3u;
const auto topology = (*parameters >> 5u) & 7u;
if ((domain != 1u && domain != 2u) || (topology != 2u && topology != 3u)) return zeroMessage(0x2db, *parameters, primitives);
}
} else if (const auto primitive = word(queue.userConfig, 0x242, RegisterBank::UserConfig); primitive && *primitive != 4u && *primitive != 5u && *primitive != 6u) {
return zeroMessage(0x242, *primitive, primitives);
}
return nonTriangleRejection(queue, "MSAA_ENABLE on a single-sample target for primitives other than triangles");
}
std::string singleSampleDepthScanRejection(const QueueState& queue) {
if (auto reason = nonTriangleRejection(queue, "a single-sample scan into a multisampled depth target for primitives other than triangles and rectangles", true); !reason.empty()) return reason;
const auto& cx = queue.context;
if (const auto eqaa = find(cx, 0x201); eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) return zeroMessage(0x201, eqaa->second, "a single-sample scan into a multisampled depth target with Z interpolation or overrasterization (DB_EQAA)");
if (PixelProgramSkipped(queue)) return {};
constexpr std::uint32_t sampleIndex = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
const auto ena = find(cx, 0x1b3);
const auto addr = find(cx, 0x1b4);
if (ena != cx.end() && addr != cx.end() && (ena->second & addr->second & sampleIndex) != 0) return zeroMessage(0x1b3, ena->second, "a single-sample scan into a multisampled depth target with a pixel program reading the sample index");
if (const auto control = find(cx, 0x203); control != cx.end() && (control->second & 0x100u) != 0) return zeroMessage(0x203, control->second, "a single-sample scan into a multisampled depth target with a sample-mask export");
return {};
}
@@ -1055,7 +1077,6 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
Require(!result.customSampleLocations, "dithered alpha-to-coverage with custom sample locations is unsupported");
}
const bool depthKeepsSampleZero = result.depth && result.depth->samples == 1 && result.samples > 1;
Require(!result.depth || depthKeepsSampleZero || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's");
zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode");
if (result.samples > 1 && (read(cx, 0x292) & 1u) == 0) {
const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
@@ -1074,6 +1095,14 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
Require(rasterizesSampleZeroOnly(cx, result), "the depth target's sample count differs from the rasterizer's");
rasterizeSampleZero(result);
}
if (result.depth && result.depth->samples > 1 && result.samples == 1 && (read(cx, 0x292) & 1u) == 0) {
Require(!result.alphaToCoverage, "a single-sample scan into a multisampled depth target with alpha-to-coverage is unsupported");
if (auto reason = singleSampleDepthScanRejection(queue); !reason.empty()) throw std::runtime_error(reason);
result.samples = result.depth->samples;
result.customSampleLocations = true;
result.sampleLocations.fill(0);
}
Require(!result.depth || depthKeepsSampleZero || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's");
zero(cx, 0x80, ~0u, "window offset");
zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits");
Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported");
@@ -3174,6 +3174,103 @@ void multisampleDccStateTests() {
}
}
void multisampledDepthTests() {
using AgcDriver::Graphics::DecodeState;
const auto singleSampleScan = [] {
auto queue = makeState();
queue.context[0x2f8] = 0;
queue.context[0x292] = 2;
queue.context[0x31d] = 0x12000;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
queue.context[0x000] = 0;
queue.context[0x002] = 0;
queue.context[0x007] = (3u << 16u) | 63u;
queue.context[0x00a] = 0;
queue.context[0x00b] = 0;
queue.context[0x010] = 0x2290098b;
queue.context[0x011] = 0x20000180;
for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100;
queue.context[0x200] = 0x16;
return queue;
};
const auto requireCentered = [](const AgcDriver::Graphics::State& state, const std::string& what) {
Require(state.samples == 4 && state.depth && state.depth->samples == 4 && state.customSampleLocations && !state.sampleLocationGrid && !state.perSampleShading, what + " did not rasterize the depth target's samples");
const auto locations = AgcDriver::Graphics::SampleLocations(state);
for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, what + " did not place every sample at the pixel center");
};
const auto depthOnly = [&] {
auto queue = singleSampleScan();
queue.context[0x8e] = 0;
queue.context[0x010] = 0xa290098b;
queue.context[0x200] = 0x777;
queue.context[0x201] = 0;
return queue;
};
auto queue = depthOnly();
auto state = DecodeState(queue);
Require(state.colors.empty(), "a depth-only single-sample scan decoded a color target");
requireCentered(state, "a single-sample scan into a 4x depth target (PPSA18216's registers)");
Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused a single-sample scan into a 4x depth target");
for (const auto primitive : {5u, 6u, 7u, 17u}) {
queue = depthOnly();
queue.userConfig[0x242] = primitive;
requireCentered(DecodeState(queue), "a single-sample scan of primitive type " + std::to_string(primitive) + " into a 4x depth target");
}
queue = depthOnly();
queue.context[0x000] = 1;
queue.context[0x00b] = std::bit_cast<std::uint32_t>(0.25f);
state = DecodeState(queue);
requireCentered(state, "a depth clear scanned single-sampled into a 4x depth target");
Require(state.depthWrite && state.depthCompare == VK_COMPARE_OP_ALWAYS, "a depth clear into a 4x depth target did not write every covered sample");
queue = depthOnly();
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
requireCentered(DecodeState(queue), "a single-sample scan whose pixel program is skipped");
queue = depthOnly();
queue.context[0x201] = 0x00040000u;
expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with Z interpolation or overrasterization (DB_EQAA)");
queue = singleSampleScan();
state = DecodeState(queue);
Require(state.colors.size() == 1 && state.colors[0].samples == 4, "a single-sample scan into 4x depth and color targets lost its color target");
requireCentered(state, "a single-sample scan into 4x depth and color targets");
queue.context[0x293] = 0x10000u;
Require(!DecodeState(queue).perSampleShading, "PS_ITER_SAMPLE requested sample shading for a single-sample scan");
queue = singleSampleScan();
queue.context[0x31d] = 0;
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
queue = singleSampleScan();
queue.context[0x010] = 0x22900983;
expectFailure([&] { DecodeState(queue); }, "a color target's sample count differs from the rasterizer's");
queue = singleSampleScan();
queue.context[0x2dc] = 0x0000aa01u;
expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with alpha-to-coverage");
for (const auto primitive : {2u, 3u}) {
queue = singleSampleScan();
queue.userConfig[0x242] = primitive;
expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target for primitives other than triangles and rectangles");
}
queue = singleSampleScan();
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary);
expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with a pixel program reading the sample index");
queue = singleSampleScan();
queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PerspectiveSample);
requireCentered(DecodeState(queue), "a single-sample scan with a pixel program reading sample-interpolated inputs");
queue = singleSampleScan();
queue.context[0x1c4] = 9;
queue.context[0x203] |= 0x100u;
expectFailure([&] { DecodeState(queue); }, "a single-sample scan into a multisampled depth target with a sample-mask export");
queue = singleSampleScan();
queue.context[0x292] = 3;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0;
expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's");
queue = singleSampleScan();
queue.context[0x2f8] = 0x00100001;
queue.context[0x292] = 3;
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44;
queue.context[0x31d] = 0x9000;
expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's");
}
void clipDistanceTests() {
auto queue = makeState();
queue.context[0x207] = 0x0040000f;
@@ -8059,6 +8156,7 @@ int main(int argc, char** argv) {
uint8x4TargetTests();
multisampleTests();
MsaaEnableSingleSampleTests();
multisampledDepthTests();
clipDistanceTests();
framebufferLayerTests();
UnwrittenSampleLocationTests();
+1
View File
@@ -122,6 +122,7 @@
- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted.
- [CB resolve sample count](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with CB_COLOR_CONTROL in resolve mode is a CB resolve when either PA_SC_AA_CONFIG or CB_COLOR0_ATTRIB.NUM_SAMPLES is multisampled, and takes its sample count from CB0. Inferred, not from documentation: PPSA36082 resolves its 4x targets with no sample count in PA_SC_AA_CONFIG, and radeonsi sets PA_SC_AA_CONFIG to 0 for a multisampled framebuffer when rasterizer multisampling and line smoothing are off.
- [Custom MSAA sample locations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support a uniform 1x1 pattern for 2, 4 or 8 samples through VK_EXT_sample_locations. Patterns varying across the guest's 2x2 pixel grid use a 2x2 sample location grid, anchored at the framebuffer origin like the guest's while the window offset is zero, on devices whose `maxSampleLocationGridSize` for the sample count is a multiple of 2x2, and throw on others. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. A multisampled rasterizer drawing into one-sample color targets, and a one-sample depth target if any, with MSAA_ENABLE set, no alpha-to-coverage and no coverage pixel input is drawn with one sample at sample 0's location in each pixel of the grid, as the targets keep only sample 0's coverage. Without PS_ITER_SAMPLE such a draw also refuses centroid inputs, and keeps PERSP_SAMPLE, LINEAR_SAMPLE and ANCILLARY: a pixel shaded once is taken to get sample 0's barycentrics and sample index 0. With PS_ITER_SAMPLE only sample 0's invocation reaches the targets, so its sample and centroid barycentrics are taken to be sample 0's and the draw is shaded once. Both are inferred, not measured. ANCILLARY with POS_X_FLOAT or POS_Y_FLOAT throws, since reading the sample index makes the host shade per sample, which moves FragCoord from the pixel center to sample 0's location. By the same Vulkan rule, PERSP_CENTER and LINEAR_CENTER inputs on this path are interpolated at sample 0's location on the host, where the console uses the pixel center; one-sample depth images are created with `SAMPLE_LOCATIONS_COMPATIBLE_DEPTH` when the device supports it, as multisampled ones already were; this is how VK_AMD_mixed_attachment_samples defines the case, and it is not measured on PS5 hardware. Other mixed sample counts throw. Depth images remain in GENERAL between draws, and render passes cannot continue across pattern changes. Guest offsets retain the existing decoder's signed four-bit, sixteenth-pixel interpretation; PS5 coverage and interpolation equivalence have not been measured on hardware.
- [Single-sample scan into a multisampled depth target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` = 0 and `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear into a multisampled depth target rasterizes the depth target's sample count with every sample at the pixel center, as a single-sample scan into a multisampled color target already does. radeonsi programs this `MSAA_ENABLE` and `PA_SC_AA_CONFIG` with the depth buffer's own sample count when GL multisampling is disabled on a multisampled framebuffer ([L1066-L1068](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L1066-L1068), [L3485-L3512](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3485-L3512)), although its comment lists Z samples as at most the coverage samples ([L3439](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/gallium/drivers/radeonsi/si_state.c#L3439)) and it sets `DB_EQAA.MAX_ANCHOR_SAMPLES` to the Z sample count, where PPSA18216 sets `DB_EQAA` to 0. GL then lets every sample take the single-sample depth ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190)) and the fragment be treated as in multisampling with full coverage ([L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)), which is what this draws: each depth and stencil sample is tested and written with the depth at the pixel center. GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, whether it instead evaluates the depth plane at each sample position, and how its occlusion queries count such a draw, is not measured on hardware. Points, lines, alpha-to-coverage, `DB_EQAA` Z interpolation or overrasterization, a pixel program reading the sample index, a sample-mask export and a single-sample color target next to the multisampled depth target throw
- [DCC display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only when all keys of the console's DCC extent agree on uncompressed or a fast-clear code, padding keys included; other keys, and register-clear keys over a pending image, throw.
- [Storage image write-back](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) stores the GPU results over a CPU-stamped range whose guest bytes equal the copy taken when the image's layers got their generation (images up to 1 MiB whose guest bytes were settled at that point, with no unit shadow, pending image results or recorded GPU store over them; other images keep the previous rule: the CPU's bytes win). A CPU store that rewrites the baseline value after the GPU changed it is not told apart from no store, and the GPU result wins there. The 1 MiB limit is a choice, not a console value.
- [16-bit float display buffers](../../../core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp) (libSceAgcDriver, libSceVideoOut) - pixel formats `0xC001000622000000` and `0xC001000600000000` are taken as R16G16B16A16 and B16G16R16A16 float, eight bytes per texel, as [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/59183dbfa81ed7efdff6069177fed50de0552725/src/graphics/host_gpu/renderer/image/imageInfo.h) maps them and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/VideoOut/VideoOutTypes.cs) names them (`Rgba16Float`, `Bgra16Float`); PPSA11386 registers 3840x2160 tiled buffers of the first. The tiled layout is assumed to be the 8-byte SW_64KB_R_X equation of `TextureSwizzleEquations.hpp` applied to full coordinates, as addrlib applies it, by analogy with the 32-bit scanout; `ColorTargetLayout` applies the equations to in-block coordinates, which for 8-byte texels differs in every odd 64-row block row (address bit 11 takes y bit 6) and for 16-byte texels also differs, so a float display buffer written through `WriteColorTarget` and presented from guest memory would show those rows misplaced. The values are assumed to be linear light and are encoded with the sRGB curve for the 8-bit output. Bits 32-39 of these formats are `0x06`, as in the 10-bit formats SharpProspero lists without sRGB transfer, while its sRGB formats have `0x00`, its 10-bit PQ formats `0x04` (with `0x07` in bits 40-47) and its float BT.2100 PQ formats `0x07`: they show only that these formats are not the sRGB ones. NaN and negative values give 0, values of 1 and above give 255, alpha is linear. Neither the layout nor the transfer is verified against console or title frames, and HDR output is not modelled. DCC metadata on a float buffer throws.