From c9e48e5ae2e0f4ae900ca22ae7cd67b9e53e7d30 Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 03:29:36 -0700 Subject: [PATCH 1/6] feat(agc): support uniform custom MSAA sample locations --- .../Execution/src/VulkanDevice.cpp | 17 +++++++++ .../Graphics/include/Context.hpp | 1 + .../Graphics/include/Pipeline.hpp | 1 + .../Graphics/include/State.hpp | 3 ++ .../Graphics/src/DepthSurface.cpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 ++++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 22 ++++++++++++ .../libSceAgcDriver/Graphics/src/State.cpp | 33 +++++++++++++++-- .../prx/libSceAgcDriver/tests/Graphics.cpp | 35 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 10 files changed, 117 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 8a1600be3..970ed7f62 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -284,6 +284,7 @@ struct VulkanDevice::State { bool maintenance8 = false; std::uint32_t srgbDecodeFormats = 0; bool storageImageWithoutFormat = false; + VkSampleCountFlags sampleLocationSampleCounts = 0; bool depthClamp = false; bool depthBounds = false; bool depthBiasClamp = false; @@ -1150,6 +1151,21 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->spirvExtensions.push_back("SPV_KHR_physical_storage_buffer"); state->depthRangeUnrestricted = hasExtension(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); if (state->depthRangeUnrestricted) deviceExtensions.push_back(VK_EXT_DEPTH_RANGE_UNRESTRICTED_EXTENSION_NAME); + if (hasExtension(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME)) { + VkPhysicalDeviceSampleLocationsPropertiesEXT locations{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLE_LOCATIONS_PROPERTIES_EXT}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &locations}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) { + const auto multisampleProperties = state->InstanceFunction("vkGetPhysicalDeviceMultisamplePropertiesEXT"); + for (const auto samples : {VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) { + if ((locations.sampleLocationSampleCounts & samples) == 0) continue; + VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT}; + multisampleProperties(selected, samples, &multisample); + if (multisample.maxSampleLocationGridSize.width != 0 && multisample.maxSampleLocationGridSize.height != 0) state->sampleLocationSampleCounts |= samples; + } + } + if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME); + } // Guest dispatches cover whole thread groups past the edge of small images; robust image access // drops those writes instead of faulting the device. const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME); @@ -3019,6 +3035,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.srgbDecodeFormats = state->srgbDecodeFormats; + context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; context.singlePassStorage = state->storageImageWithoutFormat; return context; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 38049d9e6..0fa6d5c8b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -170,6 +170,7 @@ struct Context { bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; + VkSampleCountFlags sampleLocationSampleCounts = 0; template TFunction Function(const char* name) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index bb817d437..0db0b87eb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -106,6 +106,7 @@ bool HasStencil(VkFormat format); void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); +void ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index bb18791e6..80959a49e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -116,10 +116,13 @@ struct State { VkPipelineColorBlendAttachmentState blend; std::array blendConstants; std::uint32_t samples = 1; + std::array sampleLocations{}; + bool customSampleLocations = false; }; ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); State DecodeState(const QueueState& queue, bool vertexWorkgroup = false); +std::array SampleLocations(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 0b5d5eb79..56005f036 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -34,6 +34,7 @@ public: const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + if ((context.sampleLocationSampleCounts & target.samples) != 0) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b1fe81c22..a957824c0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -50,6 +50,11 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: } mix(state.renderExtent.width); mix(state.renderExtent.height); + mix(state.samples); + mix(state.customSampleLocations); + if (state.customSampleLocations) { + for (const auto location : state.sampleLocations) mix(location); + } if (!context.provokingVertexModePerPipeline) mix(state.provokingVertexMode); return key; } @@ -1065,6 +1070,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. ValidateViewport(context, state.viewport); ValidateDepthBounds(context, state); + ValidateSampleLocations(context, state); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; if (draw.indexed) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 27bfd7ee9..ad1caed7f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -106,6 +106,10 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } } +void ValidateSampleLocations(const Context& context, const State& state) { + Require(!state.customSampleLocations || (context.sampleLocationSampleCounts & state.samples) != 0, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); +} + void ValidateViewport(const Context& context, const VkViewport& viewport) { Require(std::isfinite(viewport.minDepth) && std::isfinite(viewport.maxDepth), "non-finite viewport depth range"); Require(context.depthRangeUnrestricted || (viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1), "viewport depth outside [0, 1] requires VK_EXT_depth_range_unrestricted"); @@ -206,6 +210,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span 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"); + ValidateSampleLocations(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(); @@ -367,6 +372,11 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; samples.rasterizationSamples = static_cast(state.samples); + const auto locations = SampleLocations(state); + VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; + sampleLocations.sampleLocationsEnable = VK_TRUE; + sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, {1, 1}, state.samples, locations.data()}; + if ((context.sampleLocationSampleCounts & state.samples) != 0) samples.pNext = &sampleLocations; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -563,6 +573,16 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V begin.renderPass = renderPass; begin.framebuffer = framebuffer.Handle(); begin.renderArea = {{0, 0}, extent}; + const auto locations = SampleLocations(state); + const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(state.samples), {1, 1}, state.samples, locations.data()}; + const VkAttachmentSampleLocationsEXT initial{static_cast(state.colors.size()), info}; + const VkSubpassSampleLocationsEXT post{0, info}; + VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; + sampleLocations.attachmentInitialSampleLocationsCount = state.depth ? 1 : 0; + sampleLocations.pAttachmentInitialSampleLocations = state.depth ? &initial : nullptr; + sampleLocations.postSubpassSampleLocationsCount = 1; + sampleLocations.pPostSubpassSampleLocations = &post; + if ((context.sampleLocationSampleCounts & state.samples) != 0) begin.pNext = &sampleLocations; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); Continue(commands, state); } @@ -687,6 +707,8 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto& blend : state.blends) append(key, blend); for (const auto value : state.blendConstants) append(key, value); append(key, state.samples); + append(key, state.customSampleLocations); + if (state.customSampleLocations) append(key, state.sampleLocations); append(key, state.colors.size()); for (const auto& color : state.colors) append(key, color.format); if (state.blends.size() != state.colors.size()) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 3316fd778..6a089940e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -327,7 +327,12 @@ std::uint32_t decodeSamples(const Registers& cx) { } Require(log2 <= 3u, "more than eight samples are unsupported"); Require(((config >> 20u) & 7u) == log2, "exposing fewer samples than are rasterized is unsupported"); - const auto samples = 1u << log2; + return 1u << log2; +} + +void decodeSampleLocations(const Registers& cx, State& state) { + const auto samples = state.samples; + if (samples == 1) return; const std::span> standard = samples == 2 ? std::span>(StandardLocations2) : samples == 4 ? std::span>(StandardLocations4) : std::span>(StandardLocations8); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { for (std::uint32_t sample = 0; sample < samples; ++sample) { @@ -335,10 +340,14 @@ std::uint32_t decodeSamples(const Registers& cx) { const auto bits = (word >> (8u * (sample % 4u))) & 0xffu; const auto x = static_cast(static_cast(bits << 4u)) >> 4; const auto y = static_cast(static_cast(bits & 0xf0u)) >> 4; - Require(x == standard[sample][0] && y == standard[sample][1], "nonstandard sample locations are unsupported"); + if (pixel == 0) { + state.sampleLocations[sample] = static_cast(bits); + state.customSampleLocations |= x != standard[sample][0] || y != standard[sample][1]; + } else { + Require(bits == state.sampleLocations[sample], "nonstandard sample locations varying between pixels are unsupported"); + } } } - return samples; } bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { @@ -519,6 +528,23 @@ void intersect(VkRect2D& result, const Registers& registers, std::uint32_t offse } +std::array SampleLocations(const State& state) { + Require(state.samples == 1 || state.samples == 2 || state.samples == 4 || state.samples == 8, "invalid sample count"); + std::array locations{}; + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + std::array position{}; + if (state.customSampleLocations) { + const auto bits = state.sampleLocations[sample]; + position = {static_cast(bits & 7u) - static_cast(bits & 8u), static_cast((bits >> 4u) & 7u) - static_cast((bits >> 4u) & 8u)}; + } else if (state.samples != 1) { + const auto& standard = state.samples == 2 ? StandardLocations2[sample] : state.samples == 4 ? StandardLocations4[sample] : StandardLocations8[sample]; + position = {standard[0], standard[1]}; + } + locations[sample] = {0.5f + position[0] / 16.0f, 0.5f + position[1] / 16.0f}; + } + return locations; +} + ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { const auto value = read(queue.context, 0x2d5); const auto validate = [&](bool condition, const char* reason) { @@ -619,6 +645,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); result.samples = decodeSamples(cx); + decodeSampleLocations(cx, result); { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 0103b5aec..21c8ab46d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1752,6 +1752,41 @@ void multisampleTests() { } state = DecodeState(queue); Require(state.samples == 8 && 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"); + queue.context[0x2f8] = 0x00200002; + queue.context[0x31d] = 0x12000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x22eea66a; + state = DecodeState(queue); + Require(state.samples == 4 && state.customSampleLocations, "uniform custom sample locations were not decoded"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + const std::array expected{{{0.125f, 0.875f}, {0.875f, 0.125f}, {0.375f, 0.375f}, {0.625f, 0.625f}}}; + for (std::size_t sample = 0; sample < expected.size(); ++sample) Require(locations[sample].x == expected[sample].x && locations[sample].y == expected[sample].y, "custom sample coordinates or indices changed"); + AgcDriver::Graphics::Context sampleContext{}; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "VK_EXT_sample_locations"); + sampleContext.sampleLocationSampleCounts = VK_SAMPLE_COUNT_2_BIT; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "sample count"); + sampleContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; + AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; + state = DecodeState(queue); + Require(!state.customSampleLocations, "standard sample locations require the extension"); + AgcDriver::Graphics::ValidateSampleLocations({}, state); + for (const auto samples : {2u, 8u}) { + queue = multisampled(); + const auto log2 = samples == 2 ? 1u : 3u; + queue.context[0x2f8] = (log2 << 20u) | log2; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0x77887788; + queue.context[0x2ff + pixel * 4u] = 0x77887788; + } + state = DecodeState(queue); + Require(state.customSampleLocations && state.samples == samples, "a custom 2x or 8x pattern did not decode"); + const auto endpoints = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto expected = sample % 2u == 0 ? 0.0f : 15.0f / 16.0f; + Require(endpoints[sample].x == expected && endpoints[sample].y == expected, "signed sample coordinate endpoints changed"); + } + } queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 51f08cc9e..8a389c755 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -555,6 +555,7 @@ Throughout the project, every function at every stage either **does exactly what - [Multisampled CMASK state](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [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 is modeled. 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) is taken as `max(CB bytes * samples, width * height * element bytes * samples)` from its address, an upper bound on the sample planes of a target laid out in 64 KiB blocks (inferred: the single-sample layout size times the sample count; the exact fragment-aware footprint of addrlib is not computed, so a few bytes past the real footprint can throw too). Targets are kept per device for the device's lifetime, one per surface address, format, extent and sample count, and are not evicted. +- [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 throw. 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. From e28dcc0c1263f4c68e559b60bbbd526f6e96b056 Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:13:44 -0700 Subject: [PATCH 2/6] fix(agc): validate custom sample location depth formats --- .../Graphics/include/DepthSurface.hpp | 1 + .../Graphics/include/Pipeline.hpp | 3 +- .../Graphics/src/DepthSurface.cpp | 12 +++++- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 - .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 13 ++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 37 +++++++++++++++++++ 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp index f5b6cad4a..3d493123d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp @@ -13,6 +13,7 @@ namespace AgcDriver::Graphics { class Texture; VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target); +bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples); std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel); void ClearDepthSurfaces(VkDevice device); bool DepthSurfaceAt(std::uint64_t address); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 0db0b87eb..cfeb4ba77 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -87,6 +87,7 @@ private: VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_UNDEFINED; std::vector colorSlots; + bool sampleLocationsEnabled = false; std::vector framebuffers; }; @@ -106,7 +107,7 @@ bool HasStencil(VkFormat format); void ValidateViewport(const Context& context, const VkViewport& viewport); void ValidateDepthBounds(const Context& context, const State& state); void ValidateProvokingVertex(const Context& context, const State& state, std::span shaders = {}); -void ValidateSampleLocations(const Context& context, const State& state); +bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 56005f036..6b0383da7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -34,7 +34,7 @@ public: const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (target.stencilAddress != 0 ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); try { VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - if ((context.sampleLocationSampleCounts & target.samples) != 0) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; + if (SampleLocationsCompatibleDepth(context, target.format, target.samples)) info.flags = VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT; info.imageType = VK_IMAGE_TYPE_2D; info.format = target.format; info.extent = {target.extent.width, target.extent.height, 1}; @@ -188,6 +188,16 @@ std::uint64_t DepthSliceBytes(VkExtent2D extent, std::uint32_t bytesPerTexel) { return width * height * bytesPerTexel; } +bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples) { + if (samples <= 1 || (context.sampleLocationSampleCounts & samples) == 0) return false; + VkImageFormatProperties properties{}; + const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, + VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, &properties); + if (result == VK_ERROR_FORMAT_NOT_SUPPORTED) return false; + Check(result, "vkGetPhysicalDeviceImageFormatProperties sample locations depth"); + return (properties.sampleCounts & samples) != 0; +} + VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target) { std::lock_guard lock(surfacesMutex()); const bool cleared = target.htileAddress != 0 && clearedHtiles().erase(target.htileAddress) != 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index a957824c0..5c1b7f82a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1070,7 +1070,6 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. ValidateViewport(context, state.viewport); ValidateDepthBounds(context, state); - ValidateSampleLocations(context, state); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; if (draw.indexed) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index ad1caed7f..e61ba99f4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include @@ -106,8 +107,10 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } } -void ValidateSampleLocations(const Context& context, const State& state) { - Require(!state.customSampleLocations || (context.sampleLocationSampleCounts & state.samples) != 0, "custom sample locations require VK_EXT_sample_locations support for the draw's sample count"); +bool ValidateSampleLocations(const Context& context, const State& state) { + const bool enabled = (context.sampleLocationSampleCounts & state.samples) != 0 && (!state.depth || SampleLocationsCompatibleDepth(context, state.depth->format, state.depth->samples)); + Require(!state.customSampleLocations || enabled, "custom sample locations require a compatible depth format and sample count"); + return enabled; } void ValidateViewport(const Context& context, const VkViewport& viewport) { @@ -210,7 +213,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span 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"); - ValidateSampleLocations(context, state); + sampleLocationsEnabled = ValidateSampleLocations(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(); @@ -376,7 +379,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput VkPipelineSampleLocationsStateCreateInfoEXT sampleLocations{VK_STRUCTURE_TYPE_PIPELINE_SAMPLE_LOCATIONS_STATE_CREATE_INFO_EXT}; sampleLocations.sampleLocationsEnable = VK_TRUE; sampleLocations.sampleLocationsInfo = {VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, samples.rasterizationSamples, {1, 1}, state.samples, locations.data()}; - if ((context.sampleLocationSampleCounts & state.samples) != 0) samples.pNext = &sampleLocations; + if (sampleLocationsEnabled) samples.pNext = &sampleLocations; VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -582,7 +585,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V sampleLocations.pAttachmentInitialSampleLocations = state.depth ? &initial : nullptr; sampleLocations.postSubpassSampleLocationsCount = 1; sampleLocations.pPostSubpassSampleLocations = &post; - if ((context.sampleLocationSampleCounts & state.samples) != 0) begin.pNext = &sampleLocations; + if (sampleLocationsEnabled) begin.pNext = &sampleLocations; context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); Continue(commands, state); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 21c8ab46d..d0f00ee39 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1721,6 +1721,18 @@ void uint16ExportTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } +VkSampleCountFlags depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; +VkResult depthSampleResult = VK_SUCCESS; +std::uint32_t depthSampleQueries = 0; + +VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, VkFormat format, VkImageType type, VkImageTiling tiling, VkImageUsageFlags usage, VkImageCreateFlags flags, VkImageFormatProperties* properties) { + Require(format == VK_FORMAT_D32_SFLOAT && type == VK_IMAGE_TYPE_2D && tiling == VK_IMAGE_TILING_OPTIMAL, "sample location query changed the depth image format or type"); + Require(usage == (VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT) && flags == VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, "sample location query differs from depth image creation"); + ++depthSampleQueries; + properties->sampleCounts = depthSampleCounts; + return depthSampleResult; +} + void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorMetadataPass; @@ -1766,9 +1778,34 @@ void multisampleTests() { expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "sample count"); sampleContext.sampleLocationSampleCounts |= VK_SAMPLE_COUNT_4_BIT; AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); + sampleContext.imageFormatProperties = sampleLocationDepthProperties; + using AgcDriver::Graphics::SampleLocationsCompatibleDepth; + Require(SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "supported custom sample depth format was refused"); + state.depth = AgcDriver::Graphics::DepthTarget{}; + state.depth->format = VK_FORMAT_D32_SFLOAT; + state.depth->samples = 4; + Require(AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "supported custom depth sample locations were disabled"); + depthSampleCounts = VK_SAMPLE_COUNT_2_BIT; + Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth count was accepted"); + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); + depthSampleCounts = VK_SAMPLE_COUNT_4_BIT; + depthSampleResult = VK_ERROR_FORMAT_NOT_SUPPORTED; + Require(!SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4), "unsupported custom sample depth format was accepted"); + expectFailure([&] { AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state); }, "compatible depth format and sample count"); + state.customSampleLocations = false; + Require(!AgcDriver::Graphics::ValidateSampleLocations(sampleContext, state), "standard locations did not fall back for an incompatible depth format"); + depthSampleResult = VK_ERROR_OUT_OF_HOST_MEMORY; + expectFailure([&] { SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 4); }, "Vulkan result"); + depthSampleResult = VK_SUCCESS; + const auto queries = depthSampleQueries; + Require(!SampleLocationsCompatibleDepth({}, VK_FORMAT_D32_SFLOAT, 4) && !SampleLocationsCompatibleDepth(sampleContext, VK_FORMAT_D32_SFLOAT, 1), "sample location depth compatibility requires the extension and multisampling"); + Require(depthSampleQueries == queries, "ineligible sample location depth formats were queried"); for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0x622ae6ae; state = DecodeState(queue); Require(!state.customSampleLocations, "standard sample locations require the extension"); + const auto standard = AgcDriver::Graphics::SampleLocations(state); + const std::array expectedStandard{{{0.375f, 0.125f}, {0.875f, 0.375f}, {0.125f, 0.625f}, {0.625f, 0.875f}}}; + for (std::size_t sample = 0; sample < expectedStandard.size(); ++sample) Require(standard[sample].x == expectedStandard[sample].x && standard[sample].y == expectedStandard[sample].y, "standard sample coordinate scale or Y direction changed"); AgcDriver::Graphics::ValidateSampleLocations({}, state); for (const auto samples : {2u, 8u}) { queue = multisampled(); From 9bc72d105ee604937778870fd1bb933c2d68229a Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:05:05 -0700 Subject: [PATCH 3/6] fix(agc): preserve graphics behavior after rebase Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 27 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index e61ba99f4..a8f77a1af 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -108,7 +108,9 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } bool ValidateSampleLocations(const Context& context, const State& state) { - const bool enabled = (context.sampleLocationSampleCounts & state.samples) != 0 && (!state.depth || SampleLocationsCompatibleDepth(context, state.depth->format, state.depth->samples)); + 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"); + const bool enabled = extensionSupportsSamples && (!state.depth || SampleLocationsCompatibleDepth(context, state.depth->format, state.depth->samples)); Require(!state.customSampleLocations || enabled, "custom sample locations require a compatible depth format and sample count"); return enabled; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d0f00ee39..c0922a655 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1741,13 +1741,19 @@ void multisampleTests() { auto queue = makeState(); queue.context[0x2f8] = 0x00100001; queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; queue.context[0x31d] = 0x9000; - queue.context[0x3b8] |= (0x18u << 19u) | 0x4000000u; queue.context[0x1b3] = 2; queue.context[0x1b4] = 2; return queue; }; + const auto cmaskAddress = reinterpret_cast(cmaskMemory.data()); + const auto setCmaskAddress = [&](auto& target, std::uint32_t slot = 0u) { + const auto stride = slot * 0xfu; + target.context[0x31f + stride] = static_cast(cmaskAddress >> 8u); + target.context[0x398 + slot] = static_cast(cmaskAddress >> 40u); + }; auto queue = multisampled(); auto state = DecodeState(queue); Require(state.samples == 2 && state.colors.size() == 1 && state.colors[0].samples == 2 && state.colors[0].cmaskAddress == 0, "2x multisampling did not decode"); @@ -1837,28 +1843,28 @@ void multisampleTests() { expectFailure([&] { DecodeState(queue); }, "coverage conversion"); queue = multisampled(); queue.context[0x31c] |= 0x6000; - queue.context[0x31f] = 0x1234; - Require(DecodeState(queue).colors[0].cmaskAddress == 0x123400, "the CMASK address of a multisampled target did not decode"); + setCmaskAddress(queue); + Require(DecodeState(queue).colors[0].cmaskAddress == cmaskAddress, "the CMASK address of a multisampled target did not decode"); queue = makeState(); queue.context[0x31c] |= 0x4000; expectFailure([&] { DecodeState(queue); }, "color compression"); queue = multisampled(); queue.context[0x31c] |= 0x2000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); const auto fastCleared = DecodeState(queue).colors[0]; - Require(fastCleared.cmaskAddress == 0x123400 && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); + Require(fastCleared.cmaskAddress == cmaskAddress && fastCleared.cmaskFastClear && fastCleared.cmaskBytes == 0x1000, "a multisampled FAST_CLEAR target did not decode its CMASK at the metablock size"); queue = multisampled(); queue.context[0x31c] |= 0x4000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); const auto compressed = DecodeState(queue).colors[0]; - Require(compressed.cmaskAddress == 0x123400 && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); + Require(compressed.cmaskAddress == cmaskAddress && !compressed.cmaskFastClear && compressed.cmaskBytes == 0x1000, "a multisampled COMPRESSION target did not decode its CMASK at the metablock size"); queue = multisampled(); queue.context[0x31c] |= 0x2000; queue.context[0x31f] = 0; expectFailure([&] { DecodeState(queue); }, "without a CMASK address"); queue = multisampled(); queue.context[0x31c] |= 0x2000; - queue.context[0x31f] = 0x1234; + setCmaskAddress(queue); std::vector reads; AgcDriver::Graphics::RegisterReadLog() = &reads; static_cast(DecodeState(queue)); @@ -1872,6 +1878,8 @@ void multisampleTests() { queue.context[0x32c] = 0; queue.context[0x3b1] = queue.context[0x3b0]; queue.context[0x3b9] = queue.context[0x3b8]; + setCmaskAddress(queue); + setCmaskAddress(queue, 1); auto pass = DecodeColorMetadataPass(queue); Require(pass.has_value() && pass->mode == ColorMetadataPass::Mode::Resolve && pass->source.has_value() && pass->source->samples == 2 && pass->targets.size() == 1 && pass->targets[0].samples == 1, "a CB resolve of a multisampled target did not decode"); queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u); @@ -1884,8 +1892,7 @@ void multisampleTests() { queue.context[0x3b1] = queue.context[0x3b0]; queue.context[0x3b9] = queue.context[0x3b8]; queue.context[0x32b] = queue.context[0x31c] | 0x2000u; - queue.context[0x32e] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 8u); - queue.context[0x399] = static_cast(reinterpret_cast(cmaskMemory.data()) >> 40u); + setCmaskAddress(queue, 1); expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target"); queue = multisampled(); queue.context[0x2f8] = 0; From 33efe5ac5d010c667c81a5972a557656d38cc128 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 11 Oct 2026 09:59:05 +0200 Subject: [PATCH 4/6] fix(agc): the standard 8x pattern's last sample is at (7, -7) Vulkan's standard 8x sample 7 is (0.9375, 0.0625), (7, -7) in signed sixteenths; the table had (7, -8), so the standard pattern (PPSA99902's PA_SC_AA_SAMPLE_LOCS words 0xbd153fd1 / 0x9773f95b) was refused as nonstandard. Regression row with those words. Reported by @Amoenus. Co-Authored-By: Amoenus <5391396+Amoenus@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) (cherry picked from commit 9a1a0672cac0f8e3dcaa6e28cbe6b7b684a40a1f) --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c0922a655..0d5ddb92e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1830,6 +1830,15 @@ void multisampleTests() { Require(endpoints[sample].x == expected && endpoints[sample].y == expected, "signed sample coordinate endpoints changed"); } } + queue.context[0x2f8] = 0x00300003; + queue.context[0x31d] = 0x1b000; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) { + queue.context[0x2fe + pixel * 4u] = 0xbd153fd1u; + queue.context[0x2ff + pixel * 4u] = 0x9773f95bu; + } + 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); queue = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); From fb7615e3078cb4081c89a60a39f771b4dab27bca Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:15:34 -0700 Subject: [PATCH 5/6] fix(agc): key GPL libraries by custom sample locations --- .../prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 8 ++++++++ core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index a8f77a1af..dcc557fd5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -197,6 +197,14 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); appendKey(keys.fragmentShader, state.samples); appendKey(keys.fragmentOutput, state.samples); + appendKey(keys.fragmentShader, state.customSampleLocations); + appendKey(keys.fragmentOutput, state.customSampleLocations); + if (state.customSampleLocations) { + for (std::uint32_t sample = 0; sample < state.samples; ++sample) { + appendKey(keys.fragmentShader, state.sampleLocations[sample]); + appendKey(keys.fragmentOutput, state.sampleLocations[sample]); + } + } for (const auto& blend : state.blends) appendKey(keys.fragmentOutput, blend); for (const auto value : state.blendConstants) appendKey(keys.fragmentOutput, value); appendKey(keys.renderPass, state.blends.size()); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5484435e9..7cdb976a1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5292,6 +5292,19 @@ void pipelineLibraryTests(const Device& device) { state.samples = 1; state.colors.front().samples = 1; Require(lookup() != multisampled && counters().built == std::array{2, 2, 4, 4}, "the 1x pipeline after a 4x one built its libraries again"); + if ((context.sampleLocationSampleCounts & VK_SAMPLE_COUNT_4_BIT) != 0) { + state.samples = 4; + state.colors.front().samples = 4; + state.customSampleLocations = true; + state.sampleLocations = {0x10, 0x21, 0x32, 0x43}; + const auto pattern = lookup(); + const auto beforePatternChange = counters().built; + state.sampleLocations[0] = 0x20; + const auto otherPattern = lookup(); + Require(otherPattern != pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u && counters().built[0] == beforePatternChange[0] && counters().built[1] == beforePatternChange[1], "a custom sample pattern did not rebuild exactly the fragment shader and fragment output libraries"); + state.sampleLocations[0] = 0x10; + Require(lookup() == pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u, "a cached custom sample pattern was not reused"); + } WaitForOptimizedPipelines(context.device); const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization; Require(counters().optimized == (lto ? counters().linked : 0u), "a linked pipeline was not optimized in the background"); From 605741d39f2e272963fe6db053e290555432c1dc Mon Sep 17 00:00:00 2001 From: authurtabo-byte <298079739+authurtabo-byte@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:43:05 -0700 Subject: [PATCH 6/6] fix(agc): key GPL libraries by sample-location enablement --- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 3 +++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index dcc557fd5..139cff586 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -197,6 +197,9 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); appendKey(keys.fragmentShader, state.samples); appendKey(keys.fragmentOutput, state.samples); + const auto sampleLocationsEnabled = ValidateSampleLocations(context, state); + appendKey(keys.fragmentShader, sampleLocationsEnabled); + appendKey(keys.fragmentOutput, sampleLocationsEnabled); appendKey(keys.fragmentShader, state.customSampleLocations); appendKey(keys.fragmentOutput, state.customSampleLocations); if (state.customSampleLocations) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 7cdb976a1..5f194e197 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5304,6 +5304,24 @@ void pipelineLibraryTests(const Device& device) { Require(otherPattern != pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u && counters().built[0] == beforePatternChange[0] && counters().built[1] == beforePatternChange[1], "a custom sample pattern did not rebuild exactly the fragment shader and fragment output libraries"); state.sampleLocations[0] = 0x10; Require(lookup() == pattern && counters().built[2] == beforePatternChange[2] + 1u && counters().built[3] == beforePatternChange[3] + 1u, "a cached custom sample pattern was not reused"); + + std::optional compatibleDepth, incompatibleDepth; + state.customSampleLocations = false; + for (const auto format : {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT_S8_UINT}) { + state.depth = DepthTarget{}; + state.depth->format = format; + state.depth->samples = state.samples; + (ValidateSampleLocations(context, state) ? compatibleDepth : incompatibleDepth) = format; + } + if (compatibleDepth && incompatibleDepth) { + state.depth->format = *compatibleDepth; + const auto compatible = lookup(); + const auto beforeDepthChange = counters().built; + state.depth->format = *incompatibleDepth; + const auto incompatible = lookup(); + Require(incompatible != compatible && counters().built[2] == beforeDepthChange[2] + 1u && counters().built[3] == beforeDepthChange[3] + 1u, "depth-format sample-location enablement did not rebuild both fragment libraries"); + } + state.customSampleLocations = true; } WaitForOptimizedPipelines(context.device); const auto lto = std::getenv("APS5_NO_PIPELINE_LTO") == nullptr && context.pipelineLinkTimeOptimization;