fix(agc): fall back to pixel (0,0)'s sample locations where the device has no 2x2 grid

A draw whose sample locations vary between the pixels of the guest's
2x2 pattern threw "sample locations varying between pixels require a
2x2 sample location grid for the draw's sample count" on a device
whose maxSampleLocationGridSize for that sample count is not a
multiple of 2x2. An RTX 3070 Laptop GPU reports 1x1 for every count,
so Resident Evil Village's 2x checkerboard pattern stopped there
before its title.

The pipeline now decides the grid when it is created: such a pattern
is drawn through a 1x1 grid with pixel (0,0)'s locations, the first
registers of the pattern and the pixel the guest's grid is anchored
at, and the first such pipeline of each sample count logs it. The
render pass begin takes the pipeline's grid, so a sample-location-
compatible depth image is begun with the locations it is drawn with.
Devices with the grid keep it, and uniform patterns are unchanged.
This commit is contained in:
Yunus Shaikh committed 2026-10-15 14:36:22 +00:00
1 parent f633042dfd
commit 668ecaeca2
5 files changed
+177 -6

No files matched your search

@@ -1924,9 +1924,12 @@ if(BUILD_TESTING)
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)
add_test(NAME agc_driver_sample_location_grid_fallback COMMAND agc_driver_graphics_tests --sample-location-grid-fallback)
set_tests_properties(agc_driver_sample_location_grid_fallback 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>")
set_tests_properties(agc_driver_sample_location_grid_fallback 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)
@@ -91,6 +91,7 @@ private:
VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
std::vector<std::uint32_t> colorSlots;
bool sampleLocationsEnabled = false;
VkExtent2D sampleLocationGrid{1, 1};
std::vector<CachedFramebuffer> framebuffers;
};
@@ -7,6 +7,7 @@
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdio>
@@ -117,7 +118,6 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp
bool ValidateSampleLocations(const Context& context, const State& state) {
const bool extensionSupportsSamples = (context.sampleLocationSampleCounts & state.samples) != 0;
Require(!state.customSampleLocations || extensionSupportsSamples, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count");
Require(!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0, "sample locations varying between pixels require a 2x2 sample location grid for the draw's sample count");
const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, HostDepthFormat(context, state.depth->format), state.depth->samples));
Require(!state.customSampleLocations || enabled, "custom sample locations require a compatible depth format and sample count");
return enabled;
@@ -181,6 +181,13 @@ VkShaderStageFlags pushStagesOf(const Context& context) {
return PreRasterizationPushStages(context.meshShader, context.tessellationShader, context.geometryShader);
}
VkExtent2D hostSampleLocationGrid(const Context& context, const State& state) {
if (!state.sampleLocationGrid || (context.sampleLocationGridSampleCounts & state.samples) != 0) return SampleLocationGrid(state);
static std::atomic<std::uint32_t> reported{0};
if ((reported.fetch_or(state.samples, std::memory_order_relaxed) & state.samples) == 0) std::fprintf(stderr, "[gpu] no 2x2 sample location grid at sample count %u on this device: sample locations varying between pixels take pixel (0,0)'s in every pixel\n", state.samples);
return {1, 1};
}
template<typename TValue>
void appendKey(std::vector<std::byte>& key, const TValue& value) {
static_assert(std::is_trivially_copyable_v<TValue>);
@@ -290,6 +297,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) {
PerformanceTimer timing("Vulkan.GraphicsPipeline");
sampleLocationsEnabled = ValidateSampleLocations(context, state);
sampleLocationGrid = hostSampleLocationGrid(context, state);
// A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep
// the buffer pool, which is reset with the device, alive past it.
this->context.bufferPool.reset();
@@ -466,7 +474,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
const auto locations = SampleLocations(state);
VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT};
sampleLocations.sampleLocationsEnable = VK_TRUE;
const auto grid = SampleLocationGrid(state);
const auto grid = sampleLocationGrid;
sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, grid, state.samples * grid.width * grid.height, locations.data()};
if (sampleLocationsEnabled) samples.pNext = &sampleLocations;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
@@ -696,7 +704,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V
begin.framebuffer = framebuffer.Handle();
begin.renderArea = {{0, 0}, extent};
const auto locations = SampleLocations(state);
const auto grid = SampleLocationGrid(state);
const auto grid = sampleLocationGrid;
const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast<VkSampleCountFlagBits>(state.samples), grid, state.samples * grid.width * grid.height, locations.data()};
const VkAttachmentSampleLocationsEXT initial{static_cast<std::uint32_t>(state.colors.size()), info};
const VkSubpassSampleLocationsEXT post{0, info};
@@ -2624,9 +2624,9 @@ void multisampleTests() {
for (std::size_t index = 0; index < expectedCheckerboard.size(); ++index) Require(checkerboard[index].x == expectedCheckerboard[index].x && checkerboard[index].y == expectedCheckerboard[index].y, "grid sample locations are not in Vulkan's (x + y * width) * samples + sample order");
AgcDriver::Graphics::Context gridContext{};
gridContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT;
expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid");
Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a pattern varying between pixels was refused on a device without a 2x2 sample location grid");
gridContext.sampleLocationGridSampleCounts = VK_SAMPLE_COUNT_4_BIT;
expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(gridContext, state); }, "2x2 sample location grid");
Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a 2x pattern varying between pixels was refused on a device with a 2x2 grid for 4 samples only");
gridContext.sampleLocationGridSampleCounts |= VK_SAMPLE_COUNT_2_BIT;
Require(AgcDriver::Graphics::ValidateSampleLocations(gridContext, state), "a supported 2x2 sample location grid was refused");
queue = multisampled();
@@ -4958,6 +4958,159 @@ std::vector<std::uint32_t> makeModule(const ModuleShape& shape) {
return words;
}
struct SampleLocationsUse {
VkExtent2D grid{};
std::vector<VkSampleLocationEXT> locations;
};
std::vector<SampleLocationsUse> pipelineSampleLocations;
std::vector<SampleLocationsUse> passSampleLocations;
void recordSampleLocations(std::vector<SampleLocationsUse>& uses, const VkSampleLocationsInfoEXT& info) {
uses.push_back({info.sampleLocationGridSize, {info.pSampleLocations, info.pSampleLocations + info.sampleLocationsCount}});
}
VKAPI_ATTR VkResult VKAPI_CALL sampleLocationCreateGraphicsPipelines(VkDevice, VkPipelineCache, std::uint32_t count, const VkGraphicsPipelineCreateInfo* infos, const VkAllocationCallbacks*, VkPipeline* pipelines) {
Require(count == 1, "the sample location mock expects one pipeline per call");
const auto* locations = infos[0].pMultisampleState != nullptr ? static_cast<const VkPipelineSampleLocationsStateCreateInfoEXT*>(infos[0].pMultisampleState->pNext) : nullptr;
if (locations != nullptr && locations->sType == VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT && locations->sampleLocationsEnable) recordSampleLocations(pipelineSampleLocations, locations->sampleLocationsInfo);
*pipelines = makeHandle<VkPipeline>();
++mock.live;
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL sampleLocationCreateRenderPass(VkDevice, const VkRenderPassCreateInfo*, const VkAllocationCallbacks*, VkRenderPass* pass) {
*pass = makeHandle<VkRenderPass>();
++mock.live;
return VK_SUCCESS;
}
VKAPI_ATTR VkResult VKAPI_CALL sampleLocationCreateFramebuffer(VkDevice, const VkFramebufferCreateInfo*, const VkAllocationCallbacks*, VkFramebuffer* framebuffer) {
*framebuffer = makeHandle<VkFramebuffer>();
++mock.live;
return VK_SUCCESS;
}
VKAPI_ATTR void VKAPI_CALL sampleLocationDestroyRenderPass(VkDevice, VkRenderPass, const VkAllocationCallbacks*) { --mock.live; }
VKAPI_ATTR void VKAPI_CALL sampleLocationDestroyFramebuffer(VkDevice, VkFramebuffer, const VkAllocationCallbacks*) { --mock.live; }
VKAPI_ATTR void VKAPI_CALL sampleLocationSetViewport(VkCommandBuffer, std::uint32_t, std::uint32_t, const VkViewport*) {}
VKAPI_ATTR void VKAPI_CALL sampleLocationSetScissor(VkCommandBuffer, std::uint32_t, std::uint32_t, const VkRect2D*) {}
VKAPI_ATTR void VKAPI_CALL sampleLocationBeginRenderPass(VkCommandBuffer, const VkRenderPassBeginInfo* begin, VkSubpassContents) {
const auto* locations = static_cast<const VkRenderPassSampleLocationsBeginInfoEXT*>(begin->pNext);
if (locations == nullptr || locations->sType != VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT) return;
for (std::uint32_t index = 0; index < locations->attachmentInitialSampleLocationsCount; ++index) recordSampleLocations(passSampleLocations, locations->pAttachmentInitialSampleLocations[index].sampleLocationsInfo);
for (std::uint32_t index = 0; index < locations->postSubpassSampleLocationsCount; ++index) recordSampleLocations(passSampleLocations, locations->pPostSubpassSampleLocations[index].sampleLocationsInfo);
}
PFN_vkVoidFunction VKAPI_CALL sampleLocationProc(VkDevice device, const char* name) {
static const std::map<std::string_view, PFN_vkVoidFunction> table{
{"vkCreateGraphicsPipelines", reinterpret_cast<PFN_vkVoidFunction>(sampleLocationCreateGraphicsPipelines)},
{"vkCreateRenderPass", reinterpret_cast<PFN_vkVoidFunction>(sampleLocationCreateRenderPass)},
{"vkDestroyRenderPass", reinterpret_cast<PFN_vkVoidFunction>(sampleLocationDestroyRenderPass)},
{"vkCreateFramebuffer", reinterpret_cast<PFN_vkVoidFunction>(sampleLocationCreateFramebuffer)},
{"vkDestroyFramebuffer", reinterpret_cast<PFN_vkVoidFunction>(sampleLocationDestroyFramebuffer)},
{"vkCmdBeginRenderPass", reinterpret_cast<PFN_vkVoidFunction>(sampleLocationBeginRenderPass)},
{"vkCmdSetViewport", reinterpret_cast<PFN_vkVoidFunction>(sampleLocationSetViewport)},
{"vkCmdSetScissor", reinterpret_cast<PFN_vkVoidFunction>(sampleLocationSetScissor)}
};
const auto it = table.find(name);
return it != table.end() ? it->second : mockProc(device, name);
}
void sampleLocationGridFallbackTests() {
using namespace AgcDriver::Graphics;
auto queue = makeState();
queue.context[0x2f8] = 0x00100001;
queue.context[0x292] = 3;
queue.context[0x3b8] = 0x0dc00000u;
queue.context[0x31d] = 0x9000;
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
for (const auto [pixel, word] : {std::pair{0u, 0x0c04u}, std::pair{1u, 0x0c04u}, std::pair{2u, 0x040cu}, std::pair{3u, 0x040cu}}) queue.context[0x2fe + pixel * 4u] = word;
const auto checkerboard = DecodeState(queue);
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x0c04u;
const auto uniform = DecodeState(queue);
Require(checkerboard.samples == 2 && checkerboard.customSampleLocations && checkerboard.sampleLocationGrid && uniform.samples == 2 && uniform.customSampleLocations && !uniform.sampleLocationGrid, "the 2x checkerboard (PPSA01556) or the uniform pattern of its pixel (0,0) did not decode");
auto sampleZero = checkerboard;
sampleZero.samples = 1;
sampleZero.sampleLocations = {};
for (std::uint32_t pixel = 0; pixel < 4; ++pixel) sampleZero.sampleLocations[pixel * 8u] = checkerboard.sampleLocations[pixel * 8u];
const auto previousDepthSamples = depthSampleCounts;
depthSampleCounts = VK_SAMPLE_COUNT_2_BIT;
ShaderRecompiler::RecompileResult vertex, fragment;
vertex.spirv = makeModule({});
fragment.spirv = makeModule({.fragment = true});
vertex.variantId = 1;
fragment.variantId = 2;
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
const std::array<std::uint32_t, 1> mainKey{41};
const auto draw = [&](const State& decoded, VkSampleCountFlags gridSamples, bool libraries, bool depth) {
mock = MockVulkan{};
pipelineSampleLocations.clear();
passSampleLocations.clear();
auto context = mockContext();
context.deviceProc = sampleLocationProc;
context.device = makeHandle<VkDevice>();
context.graphicsPipelineLibrary = libraries;
context.sampleLocationSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT;
context.sampleLocationGridSampleCounts = gridSamples;
context.imageFormatProperties = sampleLocationDepthProperties;
context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT;
context.limits.framebufferDepthSampleCounts = VK_SAMPLE_COUNT_2_BIT;
context.limits.maxFramebufferWidth = 64;
context.limits.maxFramebufferHeight = 64;
context.limits.maxFramebufferLayers = 1;
State state{};
state.samples = decoded.samples;
state.sampleLocations = decoded.sampleLocations;
state.customSampleLocations = decoded.customSampleLocations;
state.sampleLocationGrid = decoded.sampleLocationGrid;
if (depth) {
state.depth = DepthTarget{0, 0, {64, 64}, VK_FORMAT_D32_SFLOAT, 1.0f, 0};
state.depth->samples = state.samples;
state.depthTest = true;
state.depthCompare = VK_COMPARE_OP_LESS;
}
const PipelineSetLayouts resources{makeHandle<VkDescriptorSetLayout>(), VK_NULL_HANDLE, false, mainKey, {}};
if (libraries) {
Require(Pipeline::Prebuild(context, state, VertexInputLayout{}, resources, shaders), "the sample location pipeline's libraries were not prebuilt");
} else {
Pipeline pipeline(context, state, VertexInputLayout{}, resources, shaders, VK_IMAGE_LAYOUT_GENERAL);
const std::array<VkImageView, 1> depthView{makeHandle<VkImageView>()};
const auto framebuffer = pipeline.AcquireFramebuffer(std::span(depthView).first(depth ? 1u : 0u), {}, {64, 64});
pipeline.Begin(makeHandle<VkCommandBuffer>(), *framebuffer, {64, 64}, state);
}
ClearPipelineLibraries(context.device);
Require(mock.live == 0, "sample location grid fallback tests leaked Vulkan objects");
Require(pipelineSampleLocations.size() == (libraries ? 2u : 1u) && passSampleLocations.size() == (libraries ? 0u : depth ? 2u : 1u), "the pipeline or render pass did not take its sample locations once per library or attachment");
auto uses = pipelineSampleLocations;
uses.insert(uses.end(), passSampleLocations.begin(), passSampleLocations.end());
return uses;
};
const auto expect = [](const std::vector<SampleLocationsUse>& uses, VkExtent2D grid, std::span<const VkSampleLocationEXT> locations, const std::string& what) {
for (const auto& use : uses) {
Require(use.grid.width == grid.width && use.grid.height == grid.height && use.locations.size() == locations.size(), what + " took the wrong sample location grid");
for (std::size_t index = 0; index < locations.size(); ++index) Require(use.locations[index].x == locations[index].x && use.locations[index].y == locations[index].y, what + " took the wrong sample locations");
}
};
const std::array<VkSampleLocationEXT, 2> pixelZero{{{0.75f, 0.5f}, {0.25f, 0.5f}}};
const std::array<VkSampleLocationEXT, 8> grid{{{0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.75f, 0.5f}}};
const std::array<VkSampleLocationEXT, 1> sampleZeroPixelZero{{{0.75f, 0.5f}}};
const std::array<VkSampleLocationEXT, 4> sampleZeroGrid{{{0.75f, 0.5f}, {0.75f, 0.5f}, {0.25f, 0.5f}, {0.25f, 0.5f}}};
for (const bool libraries : {true, false}) {
for (const bool depth : {false, true}) {
for (const VkSampleCountFlags gridSamples : {VkSampleCountFlags{0}, VkSampleCountFlags{VK_SAMPLE_COUNT_4_BIT}}) expect(draw(checkerboard, gridSamples, libraries, depth), {1, 1}, pixelZero, "a 2x checkerboard on a device without a 2x2 grid for 2 samples");
expect(draw(checkerboard, VK_SAMPLE_COUNT_2_BIT | VK_SAMPLE_COUNT_4_BIT, libraries, depth), {2, 2}, grid, "a 2x checkerboard on a device with a 2x2 grid for 2 samples");
for (const VkSampleCountFlags gridSamples : {VkSampleCountFlags{0}, VkSampleCountFlags{VK_SAMPLE_COUNT_2_BIT}}) expect(draw(uniform, gridSamples, libraries, depth), {1, 1}, pixelZero, "a uniform 2x pattern");
}
expect(draw(sampleZero, VK_SAMPLE_COUNT_2_BIT, libraries, false), {1, 1}, sampleZeroPixelZero, "one sample at sample 0's checkerboard locations on a device without a one-sample grid");
expect(draw(sampleZero, VK_SAMPLE_COUNT_1_BIT | VK_SAMPLE_COUNT_2_BIT, libraries, false), {2, 2}, sampleZeroGrid, "one sample at sample 0's checkerboard locations on a device with a one-sample grid");
}
depthSampleCounts = previousDepthSamples;
mock = MockVulkan{};
}
void pipelineLibrarySetTests() {
using namespace AgcDriver::Graphics;
for (const auto mask : {0u, 1u, 2u, 3u}) {
@@ -7223,6 +7376,11 @@ int main(int argc, char** argv) {
bufferBudgetPlacementTests();
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--sample-location-grid-fallback") {
sampleLocationGridFallbackTests();
std::cout << "Sample location grid fallback checks passed\n";
return 0;
}
{
AgcDriver::Graphics::PipelineCacheSynchronization synchronization;
auto pipelineUse = synchronization.LockForPipelineUse();
@@ -7355,6 +7513,7 @@ int main(int argc, char** argv) {
clipDistanceTests();
framebufferLayerTests();
UnwrittenSampleLocationTests();
sampleLocationGridFallbackTests();
quadPixelMaskTests();
ShaderStageTests();
NggVertexWorkgroupTests();
+1 -1
View File
@@ -106,7 +106,7 @@
- [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.
- [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. 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.
- [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. On other devices every pixel takes the pattern's pixel (0,0) locations (`PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_*`, its first registers and the corner the grid is anchored at), and the first such pipeline of each sample count logs `[gpu] no 2x2 sample location grid`. The RTX 5070 reports 2x4 for 2 samples, 2x2 for 4 and 1x2 for 8; an AMD Radeon integrated GPU reports 2x2 for each; an RTX 3070 Laptop GPU (driver 617.42) reports 1x1 for every count. With that stand-in the top-left pixel of every 2x2 quad matches the console and the other three use its positions. A pattern that only reorders one set of positions between pixels, as the 2x checkerboard of PPSA01556 and PPSA27836 puts its samples at x = +4/16 and -4/16 and swaps them on odd rows, keeps every position and the number of samples a primitive covers in each pixel, so a resolve that averages a pixel's samples, or takes their minimum or maximum, sees the console's values. Sample i of an odd row then holds what the console stores in sample 1 - i: a pass that reads the samples by index, as checkerboard reconstruction does, exchanges the two half-pixels of odd rows, which can show as combing along vertical edges, and per-sample shading, sample-mask exports and coverage inputs see the swapped indices too. A pattern whose pixels hold different positions draws pixel (0,0)'s positions in each pixel whose own differ. A device's pipelines and render pass begins take the same stand-in for a given pattern, so a sample-location-compatible depth image is not drawn with one set of locations and transitioned or tested with another. Devices without the required sample count, four subpixel bits or the full guest coordinate range refuse custom patterns. 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.
- [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.