Merge pull request #7016 from Amoenus/codex/fix-native-vertex-after6590-main661

fix(agc): refuse native MSAA vertex buffer reads
This commit is contained in:
Dean authored and GitHub committed 2026-10-18 00:57:37 +13:00
commit c9e6c45de9
4 files changed
+212 -5

No files matched your search

@@ -1442,6 +1442,13 @@ if(BUILD_TESTING)
endif()
if(BUILD_TESTING)
add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input)
set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state)
add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state)
set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES
@@ -1048,6 +1048,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
if (draw.indexed) GuestMemory::CheckRange(reinterpret_cast<const void*>(draw.indexAddress), static_cast<std::size_t>(indexBytes), draw.indexSize);
APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK");
Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target");
if (draw.indexed) RequireNoMultisampleTarget(draw.indexAddress, static_cast<std::size_t>(indexBytes), "an index buffer read");
APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders");
if (recipe != nullptr) {
inputs.fragmentOutputs = recipe->fragmentOutputs;
@@ -1197,6 +1198,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
const auto [begin, end] = plan.copies[range];
const auto bytes = static_cast<std::size_t>(end - begin);
GuestMemory::CheckRange(reinterpret_cast<const void*>(begin), bytes, 1);
RequireNoMultisampleTarget(begin, bytes, "a vertex buffer read");
if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range], &inputs.vertexInputBehindGpuWrite)) {
rangeHandles.push_back(import->buffer);
rangeOffsets.push_back(begin - import->base);
@@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
@@ -25,9 +26,20 @@
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <span>
#include <string>
#include <string_view>
#include <vector>
#include <mutex>
namespace AgcDriver {
struct VulkanDeviceTestAccess {
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
};
}
namespace {
@@ -156,7 +168,7 @@ AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& c
return state;
}
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state) {
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::uint64_t indexAddress = 0, std::uint32_t indexSize = 2u) {
const auto target = device.Target();
constexpr std::uint32_t waveSize = 64;
std::vector<std::uint32_t> vertexUserData(4, 0u);
@@ -195,7 +207,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta
{ShaderStage::Vertex, &vertexResult, 0},
{ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)}
}};
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
const AgcDriver::Pm4::DrawParameters draw{indexAddress, static_cast<std::uint32_t>(Triangle.size()), indexAddress != 0 ? indexSize : 0u, 1, 0, indexAddress != 0};
device.Draw(state, draw, shaders);
}
@@ -363,6 +375,69 @@ void CheckUniform(const std::vector<std::array<float, 4>>& loaded, const std::ar
}
}
void CheckNativeIndices(AgcDriver::VulkanDevice& device, const Block& source, const Block& destination) {
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
if (context.hostImportAlignment == 0) {
std::puts("native index alias checks not run: host imports unavailable");
return;
}
const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(DecodeTarget(source));
Require(colorBytes <= source.bytes && source.bytes - colorBytes >= BlockBytes, "the native-index fixture has no neighboring block");
const auto neighbor = source.Address() + colorBytes;
AgcDriver::Graphics::RequireNoMultisampleTarget(neighbor, 12u, "an index neighbor control");
std::string guarded;
try {
AgcDriver::Graphics::RequireNoMultisampleTarget(source.Address(), 48u, "a draw-input control");
} catch (const std::exception& error) {
guarded = error.what();
}
Require(guarded.find("whose samples are only on the host") != std::string::npos, "the draw-input source is not guarded as a native target");
{
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
const auto* native = AgcDriver::Graphics::HostImportFor(context, source.Address(), 12u);
const auto* adjacent = AgcDriver::Graphics::HostImportFor(context, neighbor, 12u);
Require(native != nullptr && native == adjacent, "the native indices and their neighbor do not share a host import");
}
const auto state = TargetState(DecodeTarget(destination));
AgcDriver::Graphics::NoteColorMetadataFill(source.Address(), static_cast<std::size_t>(colorBytes), FillOnes);
std::uint32_t missed = 0;
for (const std::uint32_t indexSize : {2u, 4u}) {
if (indexSize == 2u) {
const std::array<std::uint16_t, 3> indices{0u, 1u, 2u};
std::memcpy(source.data + colorBytes, indices.data(), sizeof(indices));
} else {
const std::array<std::uint32_t, 3> indices{0u, 1u, 2u};
std::memcpy(source.data + colorBytes, indices.data(), sizeof(indices));
}
{
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
AgcDriver::Graphics::NoteColorMetadataFill(destination.Address(), static_cast<std::size_t>(AgcDriver::Graphics::MultisampledColorBytes(state.color)), 0u);
AgcDriver::Graphics::MaterializeMultisampleTargetClear(context, state.color);
}
CheckUniform(LoadSamples(device, destination), {}, "before the neighboring indexed draw");
Draw(device, state, neighbor, indexSize);
device.WaitIdle();
CheckSamples(state, LoadSamples(device, destination), {});
for (const bool edge : {false, true}) {
const auto address = edge ? neighbor - indexSize : source.Address();
std::string refusal;
try {
Draw(device, state, address, indexSize);
device.WaitIdle();
} catch (const std::exception& error) {
refusal = error.what();
}
const bool refused = refusal.find("whose samples are only on the host") != std::string::npos;
std::printf("native index %u %s: refused=%u neighbor=passed detail=%s\n", indexSize * 8u, edge ? "edge" : "base", refused ? 1u : 0u, refusal.c_str());
Require(refusal.empty() || refused, "the native index input failed for another reason: " + refusal);
missed += refused ? 0u : 1u;
}
}
CheckUniform(LoadSamples(device, source), Ones, "after native draw-input alias attempts");
std::puts("native index controls: shared-import=1 source-samples=1024 neighbor-triangle=passed");
Require(missed == 0, "native indices missed " + std::to_string(missed) + " resident-image refusals");
}
void StoreWords(const Block& block, std::uint64_t bytes, std::uint32_t word) {
for (std::uint64_t offset = 0; offset < bytes; offset += sizeof(word)) std::memcpy(block.data + offset, &word, sizeof(word));
}
@@ -381,12 +456,129 @@ void RequireRefused(AgcDriver::VulkanDevice& device, const std::array<std::uint3
Require(refusal.find(expected) != std::string::npos, what + " was not refused: " + refusal);
}
alignas(256) std::array<std::uint32_t, 12> VertexValues{};
void DrawVertexInput(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, std::uint64_t address, std::uint32_t stride) {
alignas(256) static constexpr std::array<std::uint32_t, 7> code{
0xbe822100u, 0x34020a84u, 0xe0781000u, 0x80010801u, 0xf80008cfu, 0x0b0a0908u, 0xbf810000u,
};
VertexValues.fill(0xdeadbeefu);
const auto output = reinterpret_cast<std::uintptr_t>(VertexValues.data());
std::vector<std::uint32_t> userData(12u, 0u);
userData[0] = 0x10000000u;
userData[1] = 1u;
userData[4] = static_cast<std::uint32_t>(output);
userData[5] = static_cast<std::uint32_t>((output >> 32u) & 0xffffu);
userData[6] = static_cast<std::uint32_t>(sizeof(VertexValues));
userData[7] = 0x31016facu;
ShaderRecompiler::ShaderVertexStageInfo info{};
info.resourcesNum = 1u;
info.resources[0].fields = {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), stride == 0u ? 16u : 3u, 0x3104dfacu};
info.resourcesDst[0] = {8, 4, 0, 0};
info.fetchAttribReg = 8u;
info.fetchBufferReg = 10u;
info.fetchEmbedded = true;
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(std::span(code))}}};
ShaderRecompiler::RecompileRequest vertexRequest{
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
{64u, 0, userData, std::nullopt, std::nullopt, info, vertexMemory},
device.Target(), {0, 0, 0, 64}
};
vertexRequest.useCache = false;
const auto vertex = ShaderRecompiler::Recompile(vertexRequest);
Require(vertex.vertexAttributes.size() == 1u, "the native vertex probe did not create a fixed-function attribute");
const auto vertexPush = static_cast<std::uint32_t>(vertex.pushConstants.size());
const auto pixelAddress = reinterpret_cast<std::uintptr_t>(PixelCode.data());
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{pixelAddress, std::as_bytes(std::span(PixelCode))}}};
const AgcDriver::Registers registers{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}};
ShaderRecompiler::RecompileRequest pixelRequest{
{ShaderStage::Fragment, pixelAddress, PixelCode, 0, {}},
{64u, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(registers, AgcDriver::Graphics::ExportMappings(state)), std::nullopt, pixelMemory},
device.Target(), PixelPushLayout(vertexPush, device.Target())
};
pixelRequest.useCache = false;
const auto pixel = ShaderRecompiler::Recompile(pixelRequest);
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(vertexPush, device.Target())}}};
const AgcDriver::Pm4::DrawParameters draw{0u, 3u, 0u, 1u, 0u, false};
device.Draw(state, draw, shaders);
device.WaitIdle();
}
int main() {
void CheckVertexValues(std::uint64_t address, std::uint32_t stride) {
std::array<std::uint32_t, 4> expected{};
for (std::size_t vertex = 0; vertex < 3u; ++vertex) {
std::memcpy(expected.data(), reinterpret_cast<const void*>(address + vertex * stride), sizeof(expected));
for (std::size_t component = 0; component < expected.size(); ++component) {
Require(VertexValues[vertex * 4u + component] == expected[component], "the fixed-function vertex probe did not observe the original guest bytes");
}
}
}
int NativeVertexInput(std::unique_ptr<AgcDriver::VulkanDevice>& device) {
Block source(4u * BlockBytes);
Block destination(BlockBytes);
struct ReleaseDevice {
std::unique_ptr<AgcDriver::VulkanDevice>& value;
~ReleaseDevice() { value.reset(); }
} releaseDevice{device};
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device);
if (context.hostImportAlignment == 0u) {
std::puts("native vertex input not tested: host imports unavailable");
return VulkanTestSkipped;
}
const auto color = DecodeTarget(source);
const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color);
Require(colorBytes >= 48u && colorBytes + 48u <= source.bytes, "the native vertex probe has no neighboring range");
constexpr std::array<float, 4> element{0.25f, -2.0f, 7.5f, 1.0f};
for (std::size_t vertex = 0; vertex < 3u; ++vertex) {
std::memcpy(source.data + vertex * sizeof(element), element.data(), sizeof(element));
std::memcpy(source.data + colorBytes + vertex * sizeof(element), element.data(), sizeof(element));
}
std::memcpy(source.data + colorBytes - 8u, element.data(), 8u);
Draw(*device, TargetState(color));
AgcDriver::Graphics::NoteColorMetadataFill(source.Address(), static_cast<std::size_t>(colorBytes), FillOnes);
CheckUniform(LoadSamples(*device, source), Ones, "before the fixed-function vertex probe");
const auto neighbor = source.Address() + colorBytes;
{
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
const auto* native = AgcDriver::Graphics::HostImportFor(context, source.Address(), 48u);
const auto* adjacent = AgcDriver::Graphics::HostImportFor(context, neighbor, 48u);
Require(native != nullptr && native == adjacent, "the native and neighboring vertices do not share an import");
}
AgcDriver::Graphics::RequireNoMultisampleTarget(neighbor, 48u, "the vertex neighbor control");
const auto state = TargetState(DecodeTarget(destination));
std::uint32_t missed = 0u;
for (const auto stride : {0u, 16u}) {
for (const bool edge : {false, true}) {
DrawVertexInput(*device, state, neighbor, stride);
CheckVertexValues(neighbor, stride);
const auto address = edge ? neighbor - 8u : source.Address();
std::string refusal;
try {
DrawVertexInput(*device, state, address, stride);
CheckVertexValues(address, stride);
} catch (const std::exception& error) {
refusal = error.what();
}
const bool refused = refusal.find("whose samples are only on the host") != std::string::npos;
Require(refusal.empty() || refused, "the native vertex input failed for another reason: " + refusal);
std::printf("native vertex stride %u %s: refused=%u neighbor=passed original-guest-values=%u detail=%s\n", stride, edge ? "edge" : "base", refused ? 1u : 0u, refused ? 0u : 12u, refusal.c_str());
missed += refused ? 0u : 1u;
}
}
CheckUniform(LoadSamples(*device, source), Ones, "after fixed-function vertex inputs");
std::puts("native vertex controls: shared-import=1 source-samples=1024 neighbor-values=passed");
Require(missed == 0u, "native vertex inputs missed " + std::to_string(missed) + " resident-image refusals");
return 0;
}
}
int main(int argc, char** argv) {
try {
const auto device = OpenVulkanTestDevice();
auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
if (argc == 2 && std::string_view(argv[1]) == "--native-vertex-input") return NativeVertexInput(device);
Block block(4u * BlockBytes);
Block other(BlockBytes);
const auto color = DecodeTarget(block);
@@ -425,6 +617,12 @@ int main() {
RequireStoreRefused(*device, TargetDescriptor(mixedStore, SampleShift), "writing the 16x16 multisampled texture", "storing to a multisampled texture no draw rendered to whose color memory is not uniform");
std::puts("stores to one sample of each pixel of a drawn 4x target and to samples of zeroed color memory no draw rendered to reach exactly those samples, and a store to non-uniform color memory no draw rendered to is refused");
std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw");
Block destination(BlockBytes);
struct ReleaseIndexDevice {
std::unique_ptr<AgcDriver::VulkanDevice>& device;
~ReleaseIndexDevice() { device.reset(); }
} releaseIndexDevice{device};
CheckNativeIndices(*device, block, destination);
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
+1 -1
View File
@@ -119,7 +119,7 @@
- [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill.
- [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host.
- [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared.
- [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.
- [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer 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