From ebf361103c2178186dd01203f65b6c7c53ad4071 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sat, 3 Oct 2026 01:06:40 +0200 Subject: [PATCH 1/8] feat(agc): multisampled color and depth targets with CB resolves Draws with PA_SC_AA_CONFIG multisampling (2, 4 or 8 samples, every target with as many fragments, the standard sample locations, MSAA_ENABLE) render into host-only multisampled color images and multisampled depth surfaces; the pipeline rasterizes with that sample count. A CB_COLOR_CONTROL resolve over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image. A zero fill over a target's CMASK fast-clears it to the CB_COLOR_CLEAR_WORD values of its next use. EQAA, coverage conversion, nonstandard sample locations, sampling a multisampled surface and multisampled mips stay refused. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../src/Driver/Memory/BufferFill.cpp | 6 +- .../src/Driver/Packets/PacketMemory.cpp | 7 +- .../Execution/src/VulkanDevice.cpp | 2 + .../Graphics/include/MultisampleTarget.hpp | 20 ++ .../Graphics/include/Pipeline.hpp | 3 +- .../Graphics/include/State.hpp | 17 +- .../Graphics/include/Texture.hpp | 1 + .../Graphics/src/DepthSurface.cpp | 7 +- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 37 ++- .../Graphics/src/MultisampleTarget.cpp | 262 ++++++++++++++++++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 24 +- .../libSceAgcDriver/Graphics/src/State.cpp | 93 ++++++- .../libSceAgcDriver/Graphics/src/Texture.cpp | 4 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 54 ++++ 15 files changed, 500 insertions(+), 39 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 675ae9ca6e4..349f1ea3d87 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -173,6 +173,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp @@ -841,6 +842,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/DepthSurfaceReuse.cpp Graphics/src/Draw.cpp Graphics/src/DepthSurface.cpp + Graphics/src/MultisampleTarget.cpp Graphics/src/Pipeline.cpp Graphics/src/PipelineLibrary.cpp Graphics/src/State.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp index 5e7f9a53cc6..b8e7adb7772 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include #include #include @@ -265,7 +266,10 @@ bool Driver::fillBuffer(QueueState& queue, std::uint32_t queueId, std::span(block).first(std::min(chunk, bytes - done)), 16); phase(FillCpu); } - if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); + if (pattern[0] == pattern[1] && pattern[1] == pattern[2] && pattern[2] == pattern[3]) { + Graphics::NoteDepthMetadataFill(base, bytes, pattern[0]); + Graphics::NoteColorMetadataFill(base, bytes, pattern[0]); + } if (profile) { const auto now = std::chrono::steady_clock::now(); const auto cover = static_cast(coverage.cover); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index c7b1198a17b..83641032434 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include #include #include @@ -60,7 +61,11 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } if (opcode == 0x50 && packet.size() >= 7) { - if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) Graphics::NoteDepthMetadataFill(packet[4] | (static_cast(packet[5]) << 32u), packet[6] & 0x3ffffffu, packet[2]); + if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) { + const auto address = packet[4] | (static_cast(packet[5]) << 32u); + Graphics::NoteDepthMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); + Graphics::NoteColorMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); + } } if (!drainAll && !endOfPipeInterrupt && (opcode == 0x49 || opcode == 0x37)) { if (const auto label = Pm4::DecodeLabelWrite(packet)) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 87d211a4990..b41b2bdb3e7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -15,6 +15,7 @@ #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" @@ -598,6 +599,7 @@ struct VulkanDevice::State { // belong to this device and must be destroyed while it lives. Graphics::ClearCachedPipelines(device); Graphics::ClearDepthSurfaces(device); + Graphics::ClearMultisampleTargets(device); { std::lock_guard pipelines(computePipelinesMutex); computePipelines.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp new file mode 100644 index 00000000000..58ec1c2fb11 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -0,0 +1,20 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include + +namespace AgcDriver::Graphics { + +class StorageTexture; + +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); +void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); +void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); +void ClearMultisampleTargets(VkDevice device); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 5258bc026b6..bb817d437c2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -45,7 +45,7 @@ class Pipeline { Pipeline(const Pipeline&) = delete; Pipeline& operator=(const Pipeline&) = delete; VkPipelineLayout Layout() const; - std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent); + std::shared_ptr AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::span persistent = {}); // Begins the render pass on the framebuffer, binds the pipeline and sets its dynamic state. void Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, VkExtent2D extent, const State& state) const; // The same inside a render pass another pipeline of the same attachments began (compatible by @@ -64,6 +64,7 @@ class Pipeline { struct CachedFramebuffer { std::vector views; std::vector> owners; + std::vector owned; VkExtent2D extent; std::shared_ptr framebuffer; }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 0012dbfa556..f02127656bd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -60,6 +60,8 @@ struct ColorTarget { std::uint32_t exportIndex = 0; bool uintExport = false; std::uint32_t pipeBankXor = 0; + std::uint32_t samples = 1; + std::uint64_t cmaskAddress = 0; }; struct DepthTarget { @@ -71,6 +73,7 @@ struct DepthTarget { std::uint8_t clearStencil; std::uint64_t htileAddress = 0; bool htileStencil = false; + std::uint32_t samples = 1; }; struct State { @@ -112,6 +115,7 @@ struct State { VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT; VkPipelineColorBlendAttachmentState blend; std::array blendConstants; + std::uint32_t samples = 1; }; ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup = false); @@ -123,9 +127,10 @@ std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); struct ColorMetadataPass { - enum class Mode { EliminateFastClear, DccDecompress }; + enum class Mode { EliminateFastClear, DccDecompress, Resolve }; Mode mode; std::vector targets; + std::optional source; }; std::optional DecodeColorMetadataPass(const QueueState& queue); std::string DepthMaintenanceRejection(const QueueState& queue); @@ -166,16 +171,18 @@ struct DrawKeyRange { std::uint32_t first; std::uint32_t count; }; -inline constexpr std::array DrawKeyRegisters{{ +inline constexpr std::array DrawKeyRegisters{{ {RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5}, {RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6}, // SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL, SPI_SHADER_POS/Z/COL_FORMAT, // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, - // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. - {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, - // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3. + // PA_SU_VTX_CNTL, PA_SU_POLY_OFFSET_*, PA_SC_AA_CONFIG, the sample locations, the sample masks, + // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. + {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, + // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, CMASK_BASE_EXT, DCC_BASE_EXT, + // ATTRIB2, ATTRIB3. {RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18}, // The pixel program address, RSRC2 and user words; the geometry-back user pointer and program // address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 5e71b21d65c..6f79dffc6e1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -38,6 +38,7 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat); bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys); std::uint64_t SampledTextureMemory(); bool ClearKeepsDenormals(const Context& context, VkFormat format); +bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear); // A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload // still writes it (see the snapshot constructor), so the texture may go before the batch completes. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b29e8d1f7a7..0b5d5eb79e0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -39,9 +39,9 @@ class DepthSurface { info.extent = {target.extent.width, target.extent.height, 1}; info.mipLevels = 1; info.arrayLayers = 1; - info.samples = VK_SAMPLE_COUNT_1_BIT; + info.samples = static_cast(target.samples); info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | (target.samples == 1 ? VK_IMAGE_USAGE_SAMPLED_BIT : 0u); info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage depth"); @@ -113,6 +113,7 @@ class DepthSurface { key[10] = components.b; key[11] = components.a; if (const auto found = textures.find(key); found != textures.end()) return found->second; + Require(target.samples == 1, "sampling a multisampled depth surface is not implemented"); const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress; const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT; @@ -158,7 +159,7 @@ class DepthSurface { }; bool sameSurface(const DepthTarget& a, const DepthTarget& b) { - return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format; + return a.address == b.address && a.stencilAddress == b.stencilAddress && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.format == b.format && a.samples == b.samples; } std::mutex& surfacesMutex() { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b7454391d33..219430bd458 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" @@ -1701,7 +1702,9 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam if (args->countIndirect) reads.emplace_back(args->countAddress, args->countAddress + 4); } auto images = resources.StorageImages(); - for (const auto& target : record.targets) images.emplace_back(target->Image(), true); + for (const auto& target : record.targets) { + if (target != nullptr) images.emplace_back(target->Image(), true); + } recorder->NoteAccess(CommandClass::Draw, Recorder::Access{reads, resources.GpuWrites(), images, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, resources.HoldsLease()}); } std::unique_ptr scratch; @@ -1866,6 +1869,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& std::vector original; TileMipLayout mip{}; std::unique_ptr target; + bool multisampled = false; // APS5_DUMP_TARGETS: the rendered linear pixels, read back for target_
_.raw. std::unique_ptr dump; }; @@ -1879,6 +1883,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& binding.color = state.colors[index]; const auto& color = binding.color; binding.gpuTiling = color.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr; + if (color.samples > 1) { + binding.multisampled = true; + timer.phase(PhaseSetup); + targetViews.push_back(MultisampleTargetView(context, color)); + timer.phase(PhaseReadTarget); + continue; + } APS5_LOG_OUT_DEBUG("Creating color target %zu address=0x%llx bytes=%llu extent=%ux%u", index, static_cast(color.address), static_cast(color.bytes), color.extent.width, color.extent.height); const ColorTargetLayout colorLayout(color.extent.width, color.extent.height, color.tileMode, color.elementBytes); constexpr VkBufferUsageFlags copies = VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; @@ -1953,7 +1964,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& timer.phase(PhasePrepare); const bool recordDraws = RecordDraws(); auto* recorder = Recorder::Active(); - const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr; }); + const bool recordable = recordDraws && recorder != nullptr && dumpLimit == 0 && std::all_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.resident != nullptr || binding.multisampled; }); auto resolved = resolveDrawResources(context, state, draw, shaders, snapshots, indexBytes, recordable, outcome, timer); auto& resources = resolved.resources; const auto& contentKey = resolved.contentKey; @@ -2064,6 +2075,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // transitions of every target and one pass per draw, as before. static const bool drawTransitions = std::getenv("APS5_DRAW_TRANSITIONS") != nullptr; const bool lean = recorded && !drawTransitions; + const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); + Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); resolved.moved.clear(); @@ -2084,8 +2097,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own. std::vector> owners; owners.reserve(targets.size()); - for (const auto& binding : targets) owners.push_back(binding.resident); - auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent); + std::vector persistent; + persistent.reserve(targets.size()); + for (const auto& binding : targets) { + owners.push_back(binding.resident); + persistent.push_back(binding.multisampled ? 1u : 0u); + } + auto framebuffer = pipeline->AcquireFramebuffer(targetViews, owners, state.renderExtent, persistent); timer.phase(PhasePipeline); APS5_LOG_CHARS_OUT_DEBUG("Pipeline created"); if (lean) { @@ -2109,7 +2127,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& // only a template the resource cache serves under this content key (reusable: no lease, // no copied writes, every direct region import- or mirror-served) of a direct draw, so a // hit's proof is the template's ProveCurrent and nothing needs completion work. - if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty()) { + if (recipeOut != nullptr && DrawRecipes() && cacheable && !outcome.waited && args == nullptr && resources->Reusable() && !state.depth && resolved.moved.empty() && !multisampled) { auto recipe = std::make_shared(); recipe->device = context.device; recipe->templateRef = resources; @@ -2498,6 +2516,15 @@ std::shared_ptr metadataPassResident(const Context& context, con } void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) { + if (pass.mode == ColorMetadataPass::Mode::Resolve) { + Require(pass.source.has_value() && pass.targets.size() == 1, "CB resolve without its source and destination"); + const auto& destination = pass.targets.front(); + Require(destination.tileMode == ColorTileMode::RenderTarget && context.detiler != nullptr, "CB resolve into a target without a resident image is not implemented"); + const auto resident = CachedStorageSurface(context, SurfaceForTarget(destination)); + Require(resident->Attachable(), "storage format cannot be a color attachment"); + ResolveMultisampleTarget(context, *pass.source, *resident, resident->AttachmentView(destination.format, destination.mip)); + return; + } for (const auto& color : pass.targets) { if (color.cmaskAddress != 0) { const auto resident = metadataPassResident(context, color); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp new file mode 100644 index 00000000000..22860840d85 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -0,0 +1,262 @@ +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +std::uint64_t cmaskBytes(VkExtent2D extent) { + const auto tiles = static_cast((extent.width + 7u) / 8u) * ((extent.height + 7u) / 8u); + return (tiles + 1u) / 2u; +} + +bool sameTarget(const ColorTarget& a, const ColorTarget& b) { + return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; +} + +VkClearColorValue fastClearColor(const ColorTarget& target) { + Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled"); + std::array texels{}; + for (std::size_t offset = 0; offset < texels.size(); offset += target.elementBytes) std::memcpy(texels.data() + offset, target.clearWords.data(), target.elementBytes); + std::array pattern{}; + std::memcpy(pattern.data(), texels.data(), texels.size()); + VkClearColorValue clear{}; + Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "the fast-clear color of a multisampled target has no exact clear value in its format"); + return clear; +} + +template +void recordCommands(const Context& context, Record&& record) { + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + if (recorder == nullptr) batch = std::make_unique(context); + record(recorder != nullptr ? recorder->Commands() : batch->Handle(), recorder); + if (batch) batch->SubmitAndWait(); +} + +class MultisampleTarget { +public: + MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target) { + this->context.bufferPool.reset(); + const auto samples = static_cast(target.samples); + Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); + VkFormatProperties properties{}; + context.formatProperties(context.physical, target.format, &properties); + Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment"); + constexpr VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + VkImageFormatProperties supported{}; + Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target"); + Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits"); + try { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = target.format; + info.extent = {target.extent.width, target.extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = samples; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage multisampled target"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory multisampled target"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory multisampled target"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = target.format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView multisampled target"); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL; + toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toGeneral.image = image; + toGeneral.subresourceRange = viewInfo.subresourceRange; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); + const VkClearColorValue zero{}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &toGeneral.subresourceRange); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } catch (...) { + release(); + throw; + } + } + ~MultisampleTarget() { release(); } + MultisampleTarget(const MultisampleTarget&) = delete; + MultisampleTarget& operator=(const MultisampleTarget&) = delete; + + void ApplyFastClear(const ColorTarget& use) { + if (!fastCleared) return; + fastCleared = false; + const auto clear = fastClearColor(use); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + }); + } + + VkRenderPass ResolvePass() { + if (resolvePass != VK_NULL_HANDLE) return resolvePass; + std::array attachments{}; + attachments[0].format = target.format; + attachments[0].samples = static_cast(target.samples); + attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachments[1].format = target.format; + attachments[1].samples = VK_SAMPLE_COUNT_1_BIT; + attachments[1].loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachments[1].storeOp = VK_ATTACHMENT_STORE_OP_STORE; + for (auto& attachment : attachments) { + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + } + const VkAttachmentReference color{0, VK_IMAGE_LAYOUT_GENERAL}; + const VkAttachmentReference resolve{1, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color; + subpass.pResolveAttachments = &resolve; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = static_cast(attachments.size()); + info.pAttachments = attachments.data(); + info.subpassCount = 1; + info.pSubpasses = &subpass; + Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &resolvePass), "vkCreateRenderPass resolve"); + return resolvePass; + } + + Context context; + ColorTarget target; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkRenderPass resolvePass = VK_NULL_HANDLE; + bool fastCleared = false; + +private: + void release() noexcept { + if (resolvePass) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + } +}; + +struct CmaskFill { + std::uint64_t address; + std::uint64_t bytes; +}; + +std::mutex& targetsMutex() { + static std::mutex mutex; + return mutex; +} + +std::vector>& targets() { + static std::vector> list; + return list; +} + +std::vector& earlyFills() { + static std::vector fills; + return fills; +} + +bool coversCmask(std::uint64_t address, std::uint64_t bytes, const ColorTarget& target) { + return target.cmaskAddress != 0 && target.cmaskAddress >= address && target.cmaskAddress + cmaskBytes(target.extent) <= address + bytes; +} + +MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { + for (const auto& target : targets()) { + if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + target->ApplyFastClear(use); + return *target; + } + targets().push_back(std::make_unique(context, use)); + auto& created = *targets().back(); + auto& fills = earlyFills(); + created.fastCleared = std::any_of(fills.begin(), fills.end(), [&](const CmaskFill& fill) { return coversCmask(fill.address, fill.bytes, use); }); + created.ApplyFastClear(use); + return created; +} + +} + +VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { + std::lock_guard lock(targetsMutex()); + return acquire(context, target).view; +} + +void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) { + std::lock_guard lock(targetsMutex()); + auto& target = acquire(context, source); + const std::array views{target.view, destinationView}; + auto framebuffer = std::make_shared(context, target.ResolvePass(), views, source.extent); + recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT); + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = target.ResolvePass(); + begin.framebuffer = framebuffer->Handle(); + begin.renderArea = {{0, 0}, source.extent}; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT); + if (recorder != nullptr) { + Recorder::CountBarriers(Recorder::CommandClass::Draw, 2); + recorder->Keep(framebuffer); + } + }); + destination.MarkDirty(); +} + +void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { + std::lock_guard lock(targetsMutex()); + auto& fills = earlyFills(); + std::erase_if(fills, [&](const CmaskFill& fill) { return fill.address < address + bytes && address < fill.address + fill.bytes; }); + bool covered = false; + for (const auto& target : targets()) { + if (!coversCmask(address, bytes, target->target)) continue; + covered = true; + static std::atomic reports{0}; + if (pattern == 0 && !target->fastCleared && reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast(target->target.surfaceAddress), static_cast(target->target.cmaskAddress), static_cast(address), bytes); + target->fastCleared = pattern == 0; + } + constexpr std::size_t remembered = 64; + if (pattern == 0 && !covered && fills.size() < remembered) fills.push_back({address, bytes}); +} + +void ClearMultisampleTargets(VkDevice device) { + std::lock_guard lock(targetsMutex()); + std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 43dbbbe4b19..d4fbfec765a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -209,7 +209,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput this->context.bufferPool.reset(); Require(state.blends.size() == (state.colors.empty() ? 0u : state.colors.back().exportIndex + 1u) && state.colors.size() <= state.blends.size(), "blend states do not match decoded color state"); Require(state.blends.size() <= context.limits.maxColorAttachments, "color targets exceed device attachment limits"); - Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & VK_SAMPLE_COUNT_1_BIT) != 0, "device does not support single-sample rendering without attachments"); + Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); + Require(state.samples == 1 || !state.depth.has_value() || (context.limits.framebufferDepthSampleCounts & static_cast(state.samples)) != 0, "device does not support depth targets with the draw's sample count"); Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test"); Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping"); Require(!state.dualSourceBlend || (context.dualSrcBlend && state.blends.size() <= context.limits.maxFragmentDualSrcAttachments), "device does not support dual-source blending into this many attachments"); @@ -270,7 +271,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (std::uint32_t index = 0; index < state.colors.size(); ++index) { VkAttachmentDescription color{}; color.format = state.colors[index].format; - color.samples = VK_SAMPLE_COUNT_1_BIT; + color.samples = static_cast(state.samples); color.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; color.storeOp = VK_ATTACHMENT_STORE_OP_STORE; color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; @@ -288,7 +289,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (state.depth) { VkAttachmentDescription depth{}; depth.format = state.depth->format; - depth.samples = VK_SAMPLE_COUNT_1_BIT; + depth.samples = static_cast(state.samples); depth.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; depth.storeOp = VK_ATTACHMENT_STORE_OP_STORE; depth.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD; @@ -363,7 +364,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput raster.pNext = &provoking; } VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; - samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + samples.rasterizationSamples = static_cast(state.samples); VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; depthStencil.depthTestEnable = state.depthTest; depthStencil.depthWriteEnable = state.depthWrite; @@ -482,21 +483,26 @@ VkPipelineLayout Pipeline::Layout() const { return layout; } -std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent) { +std::shared_ptr Pipeline::AcquireFramebuffer(std::span targets, std::span> owners, VkExtent2D extent, std::span persistent) { Require(targets.size() == attachments && owners.size() == colorAttachments, "render targets do not match the pipeline's attachments"); - const bool resident = std::all_of(owners.begin(), owners.end(), [](const auto& owner) { return owner != nullptr; }); + bool resident = true; + for (std::size_t i = 0; i < owners.size(); ++i) resident = resident && (owners[i] != nullptr || (i < persistent.size() && persistent[i] != 0)); if (resident) { // Entries whose views are gone can never match again and go as soon as no recorded draw holds // them (Kept keeps its framebuffer until the batch completes). std::erase_if(framebuffers, [](const CachedFramebuffer& entry) { - return entry.framebuffer.use_count() == 1 && std::any_of(entry.owners.begin(), entry.owners.end(), [](const auto& owner) { return owner.expired(); }); + if (entry.framebuffer.use_count() != 1) return false; + for (std::size_t i = 0; i < entry.owners.size(); ++i) { + if (entry.owned[i] && entry.owners[i].expired()) return true; + } + return false; }); for (auto it = framebuffers.begin(); it != framebuffers.end(); ++it) { if (it->extent.width != extent.width || it->extent.height != extent.height || !std::equal(it->views.begin(), it->views.end(), targets.begin(), targets.end())) continue; // View handles are recycled once a StorageTexture is destroyed, so the owners must be the // very objects the views were made for. bool same = true; - for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owners[i].lock().get() == owners[i].get(); + for (std::size_t i = 0; i < owners.size() && same; ++i) same = it->owned[i] == (owners[i] != nullptr) && it->owners[i].lock().get() == owners[i].get(); if (!same) continue; std::rotate(it, std::next(it), framebuffers.end()); return framebuffers.back().framebuffer; @@ -514,6 +520,7 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span& key, const Context& context, const Stat append(key, state.blends.size()); 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.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 b4b1e6ec38d..629e2b9baa9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include namespace AgcDriver::Graphics { @@ -199,7 +201,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) const bool stencilClear = (read(cx, 0x000) & 2u) != 0; const auto view = read(cx, 0x002); zero(cx, 0x002, 0x3c000000u, "depth mips (DB_DEPTH_VIEW MIP_LEVEL)"); - zero(cx, 0x010, 0x000f100cu, "multisampled, partially resident or mipmapped depth (DB_Z_INFO)"); + zero(cx, 0x010, 0x000f1000u, "partially resident or mipmapped depth (DB_Z_INFO)"); zero(cx, 0x011, 0x00001000u, "partially resident stencil (DB_STENCIL_INFO)"); const auto zFormat = read(cx, 0x010) & 3u; const bool stencil = (read(cx, 0x011) & 1u) != 0; @@ -215,6 +217,7 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, State& result) Require(!stencilClear || (stencil && !stencilReadOnly), "stencil clear requires a writable stencil plane"); Require(!depthClear || (zFormat != 0 && !depthReadOnly), "depth clear requires a writable depth plane"); DepthTarget depth{}; + depth.samples = 1u << ((read(cx, 0x010) >> 2u) & 3u); depth.address = zFormat != 0 ? base(0x012, 0x01a) : 0; depth.stencilAddress = stencil ? base(0x013, 0x01b) : 0; Require(zFormat == 0 || depthReadOnly || base(0x014, 0x01c) == depth.address, "depth read and written at different addresses is unsupported"); @@ -302,6 +305,34 @@ std::uint32_t effectiveDepthControl(std::uint32_t depthControl) { return (depthControl & 3u) == 0 ? depthControl & ~4u : depthControl; } +constexpr std::array, 2> StandardLocations2{{{4, 4}, {-4, -4}}}; +constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; +constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -8}}}; + +std::uint32_t decodeSamples(const Registers& cx) { + const auto config = read(cx, 0x2f8); + zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); + const auto log2 = config & 7u; + if (log2 == 0) { + Require(((config >> 20u) & 7u) == 0, "exposed samples without multisampling are unsupported"); + return 1; + } + 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; + 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) { + const auto word = read(cx, 0x2fe + pixel * 4u + sample / 4u); + 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"); + } + } + return samples; +} + bool colorControlSupported(std::uint32_t colorControl, bool hasColorTarget) { colorControl &= ~1u; return colorControl == 0xcc0010u || ((colorControl >> 4u) & 7u) == 0u || (!hasColorTarget && (colorControl & ~0x70u) == 0xcc0000u); @@ -579,6 +610,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } } zero(cx, 0x207, ~LayerExports, "clip distances, layer, viewport or auxiliary vertex exports"); + result.samples = decodeSamples(cx); { const auto depthControl = read(cx, 0x200); const auto renderControl = find(cx, 0x000); @@ -606,8 +638,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution"); result.alphaToCoverage = read(cx, 0x2dc) == AlphaToCoverageUndithered; if (!result.alphaToCoverage) zero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); - zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); - zero(cx, 0x292, ScanModeMask, "scan conversion mode"); + Require(!result.depth || result.depth->samples == result.samples, "the depth target's sample count differs from the rasterizer's"); + zero(cx, 0x292, result.samples > 1 ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); + Require(result.samples == 1 || (read(cx, 0x292) & 1u) != 0, "multisampled targets rasterized without MSAA_ENABLE are unsupported"); zero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); zero(cx, 0x80, ~0u, "window offset"); zero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); @@ -669,6 +702,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (slotExport == 7 && ((read(cx, 0x31c + slot * 0xfu) >> 8u) & 7u) != 4u) throw std::runtime_error("AGC graphics: color export format 7 (UINT16_ABGR) into a target that is not unsigned integer is unsupported"); color.exportIndex = index; color.uintExport = slotExport == 7; + Require(color.samples == result.samples, "a color target's sample count differs from the rasterizer's"); APS5_LOG_OUT_DEBUG("Color %u address=0x%llx extent=%ux%u bytes=%llu VkFormat=%u", slot, static_cast(color.address), color.extent.width, color.extent.height, static_cast(color.bytes), static_cast(color.format)); if (result.colors.empty()) { result.renderExtent = color.extent; @@ -851,17 +885,28 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { APS5_LOG_OUT_DEBUG("Color %u info=0x%x number=%u swap=%u", slot, info, number, swap); const auto format = (info >> 2u) & 0x1fu; const auto decoded = DecodeColorFormat(format, number, swap); + const auto attrib = read(cx, 0x31d + stride); + zero(cx, 0x31d + stride, ~0x3f000u, "color destination alpha override or other CB_COLOR_ATTRIB modes"); + color.samples = 1u << ((attrib >> 12u) & 7u); + Require(color.samples <= 8u, "color targets with more than eight samples are unsupported"); + Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported"); + const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u; + Require(color.samples == 1 || (info & 0x10000000u) == 0, "DCC over a multisampled color target is unsupported"); // ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written - if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); + if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")"); Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported"); const auto view = read(cx, 0x31b + stride); Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set"); const auto slice = view & 0x1fffu; Require(slice == ((view >> 13u) & 0x1fffu), "color views of several array slices are unsupported"); const auto viewMip = (view >> 26u) & 0xfu; - zero(cx, 0x31d + stride, ~0x20000u, "color samples, fragments or destination alpha override"); const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; + Require(color.samples == 1 || maxMip == 0, "multisampled color mips are unsupported"); + if (color.samples > 1 && (info & 0x4000u) != 0) { + const auto cmaskHigh = find(cx, 0x398 + slot); + color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); + } Require(viewMip <= maxMip, "color view mip exceeds the surface"); const auto attrib3 = read(cx, 0x3b8 + slot); color.tileMode = DecodeColorTileMode(attrib3); @@ -945,11 +990,14 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto control = find(cx, 0x202); if (control == cx.end()) return std::nullopt; const auto mode = (control->second >> 4u) & 7u; - if (mode != 2u && mode != 6u) return std::nullopt; - ColorMetadataPass pass{mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}}; + const auto aaConfig = find(cx, 0x2f8); + const bool resolve = mode == 3u && aaConfig != cx.end() && (aaConfig->second & 7u) != 0; + if (mode != 2u && mode != 6u && !resolve) return std::nullopt; + ColorMetadataPass pass{resolve ? ColorMetadataPass::Mode::Resolve : mode == 2u ? ColorMetadataPass::Mode::EliminateFastClear : ColorMetadataPass::Mode::DccDecompress, {}, std::nullopt}; Require((control->second & ~0x70u) == 0xcc0000u, "CB metadata pass with a nonstandard ROP, dual quads disabled or degamma"); Require((read(cx, 0x200) & 0xfu) == 0 && (read(cx, 0x0) & 0xfu) == 0, "CB metadata pass with depth or stencil work"); - zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); + const auto samples = resolve ? decodeSamples(cx) : 1u; + if (!resolve) zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); zero(cx, 0x80, ~0u, "window offset"); const auto viewportControl = read(cx, 0x206); if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); @@ -962,16 +1010,30 @@ std::optional DecodeColorMetadataPass(const QueueState& queue intersect(covered, cx, 0x81, false); intersect(covered, cx, 0x90, false); if ((read(cx, 0x292) & 2u) != 0) intersect(covered, cx, 0x94, false); - const auto targetMask = read(cx, 0x8e); - for (std::uint32_t slot = 0; slot < 8; ++slot) { - if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; - const auto target = DecodeColorBuffer(cx, slot); - Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); + const auto requireCovered = [&](const ColorTarget& target) { const auto width = static_cast(target.extent.width); const auto height = static_cast(target.extent.height); const bool viewportCovers = xo - xs <= 0.0f && xo + xs >= width && yo - ys <= 0.0f && yo + ys >= height; const bool scissorCovers = covered.offset.x == 0 && covered.offset.y == 0 && covered.extent.width >= target.extent.width && covered.extent.height >= target.extent.height; Require(viewportCovers && scissorCovers, "CB metadata pass over part of a color target"); + }; + if (resolve) { + const auto source = DecodeColorBuffer(cx, 0); + const auto destination = DecodeColorBuffer(cx, 1); + Require(source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's"); + Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); + Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); + requireCovered(destination); + pass.source = source; + pass.targets.push_back(destination); + return pass; + } + const auto targetMask = read(cx, 0x8e); + for (std::uint32_t slot = 0; slot < 8; ++slot) { + if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; + const auto target = DecodeColorBuffer(cx, slot); + Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); + requireCovered(target); pass.targets.push_back(target); } return pass; @@ -1024,8 +1086,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x2dc, word) && word != AlphaToCoverageUndithered) { if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "dithered or rounded alpha-to-coverage"); !reason.empty()) return reason; } - if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason; - if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; + if (auto reason = nonzero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u), "coverage conversion or nonstandard multisampling"); !reason.empty()) return reason; + const bool multisampled = value(cx, 0x2f8, word) && (word & 7u) != 0; + if (auto reason = nonzero(cx, 0x292, multisampled ? ScanModeMask & ~1u : ScanModeMask, "scan conversion mode"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x293, ScanControlMask, "sample iteration, primitive discard or out-of-order rasterization"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x80, ~0u, "window offset"); !reason.empty()) return reason; if (auto reason = nonzero(cx, 0x8d, ScreenOffsetMask, "reserved PA_SU_HARDWARE_SCREEN_OFFSET bits"); !reason.empty()) return reason; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e9558ef4cb7..5974f433f9d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1265,6 +1265,10 @@ bool ClearKeepsDenormals(const Context& context, VkFormat format) { return keeps; } +bool ClearColorForTexel(VkFormat format, std::uint32_t elementBytes, std::span pattern, VkClearColorValue& clear) { + return FillClearColor(format, elementBytes, pattern, clear); +} + VkImageView StorageTexture::createView(std::uint32_t mip, bool firstLayer, VkFormat format) const { Require(mip < descriptor.mipCount, "storage texture mip level is outside the texture"); Require(!firstLayer || descriptor.dimension == TextureDimension::k2DArray, "a first-layer storage view needs a 2D array surface"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 111bf9b1c83..b6f38a7f93e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1658,6 +1658,59 @@ void uint16ExportTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "color export format 8"); } +void multisampleTests() { + using AgcDriver::Graphics::ColorMetadataPass; + using AgcDriver::Graphics::DecodeColorMetadataPass; + using AgcDriver::Graphics::DecodeState; + const auto multisampled = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0x00100001; + queue.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = 0xcc44; + queue.context[0x31d] = 0x9000; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + return queue; + }; + 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"); + const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false); + Require(rejection.empty(), "the precheck refused 2x multisampling: " + rejection); + queue.context[0x302] = 0x44cc; + expectFailure([&] { DecodeState(queue); }, "nonstandard sample locations"); + queue = multisampled(); + queue.context[0x31d] = 0x1000; + expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); + queue.context[0x31d] = 0; + expectFailure([&] { DecodeState(queue); }, "sample count differs"); + queue = multisampled(); + queue.context[0x292] = 2; + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE"); + queue = multisampled(); + queue.context[0x2f8] |= 0x10; + 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"); + queue = makeState(); + queue.context[0x31c] |= 0x4000; + expectFailure([&] { DecodeState(queue); }, "color compression"); + + queue = multisampled(); + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0030; + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; + queue.context[0x32c] = 0; + queue.context[0x3b1] = queue.context[0x3b0]; + queue.context[0x3b9] = queue.context[0x3b8]; + 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); + expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); +} + void DepthClipTests() { auto queue = makeState(); const auto direct = AgcDriver::Graphics::DecodeState(queue); @@ -4362,6 +4415,7 @@ int main(int argc, char** argv) { cmaskTests(); uint16ExportTests(); uint8x4TargetTests(); + multisampleTests(); ShaderStageTests(); NggVertexWorkgroupTests(); TuningFieldTests(); From 163947ef7226a2dba66e16020f8671981125de42 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Wed, 7 Oct 2026 18:17:38 +0200 Subject: [PATCH 2/8] style(agc): drop code comments the conventions check rejects Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index f02127656bd..f09250a94ae 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -178,11 +178,9 @@ inline constexpr std::array DrawKeyRegisters{{ // CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP. {RegisterBank::Context, 0x191, 32}, {RegisterBank::Context, 0x1b3, 2}, {RegisterBank::Context, 0x1b6, 1}, {RegisterBank::Context, 0x1c3, 3}, {RegisterBank::Context, 0x1e0, 8}, {RegisterBank::Context, 0x1ff, 1}, // DB_DEPTH_CONTROL .. PA_CL_VS_OUT_CNTL, PA_SC_MODE_CNTL_0/1, VGT_GS_MODE, VGT_GS_VERT_ITEMSIZE, - // PA_SU_VTX_CNTL, PA_SU_POLY_OFFSET_*, PA_SC_AA_CONFIG, the sample locations, the sample masks, - // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. + // PA_SU_VTX_CNTL, the sample masks, PA_SC_CONSERVATIVE_RASTERIZATION_CNTL. {RegisterBank::Context, 0x200, 8}, {RegisterBank::Context, 0x286, 2}, {RegisterBank::Context, 0x292, 2}, {RegisterBank::Context, 0x29b, 1}, {RegisterBank::Context, 0x2ab, 1}, {RegisterBank::Context, 0x2ce, 1}, {RegisterBank::Context, 0x2d5, 2}, {RegisterBank::Context, 0x2db, 2}, {RegisterBank::Context, 0x2de, 6}, {RegisterBank::Context, 0x2f8, 2}, {RegisterBank::Context, 0x2fe, 16}, {RegisterBank::Context, 0x30e, 2}, {RegisterBank::Context, 0x313, 1}, - // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, CMASK_BASE_EXT, DCC_BASE_EXT, - // ATTRIB2, ATTRIB3. + // CB_COLOR0..7_BASE .. DCC_BASE (15 words a slot), CB_COLOR0..7_BASE_EXT, DCC_BASE_EXT, ATTRIB2, ATTRIB3. {RegisterBank::Context, 0x318, 0x78}, {RegisterBank::Context, 0x390, 8}, {RegisterBank::Context, 0x398, 8}, {RegisterBank::Context, 0x3a8, 0x18}, // The pixel program address, RSRC2 and user words; the geometry-back user pointer and program // address; the vertex/geometry-front RSRC1/RSRC2 and user words; the vertex program address; From eeefb06549217567ee7c726841361c3682d0e649 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Thu, 8 Oct 2026 18:32:58 +0200 Subject: [PATCH 3/8] fix(agc): multisampled targets use main's CMASK fields; their CMASK state refuses what it cannot model The duplicate ColorTarget::cmaskAddress, decode and size helper are gone: multisampled targets use main's cmaskAddress/cmaskBytes (whole metablocks). Single-sample targets keep main's rule (CMASK under FAST_CLEAR); a multisampled one takes it under FAST_CLEAR or COMPRESSION. A zero fill of the whole CMASK clears at the next use, 0xFFFFFFFF marks it expanded, any other or partial fill refuses the next use; eliminate/DCC passes over a multisampled target and a CB resolve into a fast-clear destination are refused. The draw key adds CMASK_BASE_EXT (0x398), which main decodes but did not key. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Graphics/include/MultisampleTarget.hpp | 20 +++++ .../Graphics/include/State.hpp | 2 +- .../Graphics/src/MultisampleTarget.cpp | 87 +++++++++++-------- .../libSceAgcDriver/Graphics/src/State.cpp | 24 ++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 66 ++++++++++++++ docs/dev/TechnicalDebt.md | 5 ++ 6 files changed, 158 insertions(+), 46 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index 58ec1c2fb11..f0c327c0b3b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -2,14 +2,34 @@ #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_MULTISAMPLETARGET_HPP #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include #include +#include namespace AgcDriver::Graphics { class StorageTexture; +class MultisampledCmask { +public: + MultisampledCmask() = default; + MultisampledCmask(std::uint64_t cmaskAddress, std::uint64_t cmaskBytes) : address(cmaskAddress), bytes(cmaskBytes) {} + void NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern); + void SeedKeys(DccKeys seeded); + void CheckAddress(std::uint64_t cmaskAddress) const; + bool TakeClear(); + DccKeys Keys() const { return keys; } + const std::string& Refusal() const { return refusal; } + +private: + std::uint64_t address = 0; + std::uint64_t bytes = 0; + DccKeys keys = DccKeys::Uncompressed; + std::string refusal; +}; + VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target); void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index f09250a94ae..bb18791e688 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -45,6 +45,7 @@ struct ColorTarget { bool dccPipeAligned = false; std::uint64_t cmaskAddress = 0; std::size_t cmaskBytes = 0; + bool cmaskFastClear = false; std::uint64_t surfaceAddress = 0; VkExtent2D surfaceExtent{}; std::uint32_t mipCount = 1; @@ -61,7 +62,6 @@ struct ColorTarget { bool uintExport = false; std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; - std::uint64_t cmaskAddress = 0; }; struct DepthTarget { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 22860840d85..667b8711283 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -16,11 +16,6 @@ namespace AgcDriver::Graphics { namespace { -std::uint64_t cmaskBytes(VkExtent2D extent) { - const auto tiles = static_cast((extent.width + 7u) / 8u) * ((extent.height + 7u) / 8u); - return (tiles + 1u) / 2u; -} - bool sameTarget(const ColorTarget& a, const ColorTarget& b) { return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples; } @@ -47,7 +42,7 @@ void recordCommands(const Context& context, Record&& record) { class MultisampleTarget { public: - MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target) { + MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes) { this->context.bufferPool.reset(); const auto samples = static_cast(target.samples); Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count"); @@ -109,8 +104,7 @@ class MultisampleTarget { MultisampleTarget& operator=(const MultisampleTarget&) = delete; void ApplyFastClear(const ColorTarget& use) { - if (!fastCleared) return; - fastCleared = false; + if (!cmask.TakeClear()) return; const auto clear = fastClearColor(use); recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) { RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); @@ -160,7 +154,7 @@ class MultisampleTarget { VkDeviceMemory memory = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; VkRenderPass resolvePass = VK_NULL_HANDLE; - bool fastCleared = false; + MultisampledCmask cmask; private: void release() noexcept { @@ -171,11 +165,6 @@ class MultisampleTarget { } }; -struct CmaskFill { - std::uint64_t address; - std::uint64_t bytes; -}; - std::mutex& targetsMutex() { static std::mutex mutex; return mutex; @@ -186,31 +175,66 @@ std::vector>& targets() { return list; } -std::vector& earlyFills() { - static std::vector fills; - return fills; -} - -bool coversCmask(std::uint64_t address, std::uint64_t bytes, const ColorTarget& target) { - return target.cmaskAddress != 0 && target.cmaskAddress >= address && target.cmaskAddress + cmaskBytes(target.extent) <= address + bytes; -} - MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { for (const auto& target : targets()) { if (target->context.device != context.device || !sameTarget(target->target, use)) continue; + target->cmask.CheckAddress(use.cmaskAddress); target->ApplyFastClear(use); return *target; } targets().push_back(std::make_unique(context, use)); auto& created = *targets().back(); - auto& fills = earlyFills(); - created.fastCleared = std::any_of(fills.begin(), fills.end(), [&](const CmaskFill& fill) { return coversCmask(fill.address, fill.bytes, use); }); + if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); return created; } } +void MultisampledCmask::NoteFill(std::uint64_t fillAddress, std::size_t fillBytes, std::uint32_t pattern) { + if (address == 0 || fillAddress >= address + bytes || address >= fillAddress + fillBytes) return; + if (fillAddress > address || fillAddress + fillBytes < address + bytes) { + char text[192]; + std::snprintf(text, sizeof(text), "a fill of 0x%llx+0x%zx partly covers the CMASK 0x%llx+0x%llx of a multisampled color target, leaving its tiles mixed", static_cast(fillAddress), fillBytes, static_cast(address), static_cast(bytes)); + refusal = text; + return; + } + refusal.clear(); + if (pattern == 0) { + keys = DccKeys::Clear0000; + } else if (pattern == 0xffffffffu) { + keys = DccKeys::Uncompressed; + } else { + char text[192]; + std::snprintf(text, sizeof(text), "a fill of the CMASK 0x%llx+0x%llx of a multisampled color target with 0x%08x leaves its tiles mixed (neither fast-cleared 0 nor expanded 0xffffffff)", static_cast(address), static_cast(bytes), pattern); + refusal = text; + } +} + +void MultisampledCmask::SeedKeys(DccKeys seeded) { + if (seeded == DccKeys::Clear0000 || seeded == DccKeys::Uncompressed) { + keys = seeded; + return; + } + char text[192]; + std::snprintf(text, sizeof(text), "the CMASK 0x%llx of a multisampled color target reads as %s, which is not modeled", static_cast(address), DccKeysName(seeded)); + refusal = text; +} + +void MultisampledCmask::CheckAddress(std::uint64_t cmaskAddress) const { + if (cmaskAddress == address) return; + char text[192]; + std::snprintf(text, sizeof(text), "a multisampled color target used CMASK 0x%llx after its first use with CMASK 0x%llx, which is not modeled", static_cast(cmaskAddress), static_cast(address)); + Require(false, std::string(text)); +} + +bool MultisampledCmask::TakeClear() { + Require(refusal.empty(), refusal); + if (keys != DccKeys::Clear0000) return false; + keys = DccKeys::Uncompressed; + return true; +} + VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target) { std::lock_guard lock(targetsMutex()); return acquire(context, target).view; @@ -240,18 +264,13 @@ void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) { std::lock_guard lock(targetsMutex()); - auto& fills = earlyFills(); - std::erase_if(fills, [&](const CmaskFill& fill) { return fill.address < address + bytes && address < fill.address + fill.bytes; }); - bool covered = false; for (const auto& target : targets()) { - if (!coversCmask(address, bytes, target->target)) continue; - covered = true; + const auto before = target->cmask.Keys(); + target->cmask.NoteFill(address, bytes, pattern); + if (before == DccKeys::Clear0000 || target->cmask.Keys() != DccKeys::Clear0000) continue; static std::atomic reports{0}; - if (pattern == 0 && !target->fastCleared && reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast(target->target.surfaceAddress), static_cast(target->target.cmaskAddress), static_cast(address), bytes); - target->fastCleared = pattern == 0; + if (reports.fetch_add(1) < 8) std::fprintf(stderr, "[gpu] multisampled color target 0x%llx fast-cleared through its CMASK 0x%llx (fill 0x%llx+0x%zx)\n", static_cast(target->target.surfaceAddress), static_cast(target->target.cmaskAddress), static_cast(address), bytes); } - constexpr std::size_t remembered = 64; - if (pattern == 0 && !covered && fills.size() < remembered) fills.push_back({address, bytes}); } void ClearMultisampleTargets(VkDevice device) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 629e2b9baa9..b66fae1e8e7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -903,10 +903,6 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto attrib2 = read(cx, 0x3b0 + slot); const auto maxMip = attrib2 >> 28u; Require(color.samples == 1 || maxMip == 0, "multisampled color mips are unsupported"); - if (color.samples > 1 && (info & 0x4000u) != 0) { - const auto cmaskHigh = find(cx, 0x398 + slot); - color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); - } Require(viewMip <= maxMip, "color view mip exceeds the surface"); const auto attrib3 = read(cx, 0x3b8 + slot); color.tileMode = DecodeColorTileMode(attrib3); @@ -958,15 +954,19 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot) { const auto clear = find(cx, 0x323 + word + stride); color.clearWords[word] = clear == cx.end() ? 0u : clear->second; } - if ((info & 0x2000u) != 0) { - Require(maxMip == 0 && !volume && slice == 0, "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); + if ((info & 0x2000u) != 0 || (color.samples > 1 && (info & 0x4000u) != 0)) { + Require(color.samples > 1 || (maxMip == 0 && !volume && slice == 0), "CMASK fast clears of a mipmapped, 3D or array color target are unsupported"); Require(((attrib3 >> 19u) & 0x1fu) == 0x18u && (attrib3 & 0x4000000u) != 0, "CMASK fast clears need pipe-aligned SW_64KB_Z_X metadata (CB_COLOR_ATTRIB3 FMASK_SW_MODE 24, CMASK_PIPE_ALIGNED)"); - Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); + if (color.samples == 1) Require(color.elementBytes <= 8, "CMASK fast clears of texels over 64 bits are unsupported"); const auto cmaskHigh = find(cx, 0x398 + slot); - color.cmaskAddress = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); - Require(color.cmaskAddress != 0, "CMASK fast clears without a CMASK address are unsupported"); - color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); - GuestMemory::CheckRange(reinterpret_cast(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true); + const auto cmask = ((cmaskHigh == cx.end() ? 0ull : static_cast(cmaskHigh->second & 0xffu)) << 40u) | (static_cast(read(cx, 0x31f + stride)) << 8u); + Require((info & 0x2000u) == 0 || cmask != 0, "CMASK fast clears without a CMASK address are unsupported"); + if (cmask != 0) { + color.cmaskAddress = cmask; + color.cmaskBytes = CmaskBytes(color.extent.width, color.extent.height); + if (color.samples == 1) GuestMemory::CheckRange(reinterpret_cast(color.cmaskAddress), color.cmaskBytes, CmaskLayout::Alignment, true); + } + color.cmaskFastClear = (info & 0x2000u) != 0; } if ((info & 0x10000000u) != 0) { if (maxMip == 0) { @@ -1022,6 +1022,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue const auto destination = DecodeColorBuffer(cx, 1); Require(source.samples == samples, "CB resolve from a target whose sample count differs from the rasterizer's"); Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported"); + Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)"); Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported"); requireCovered(destination); pass.source = source; @@ -1032,6 +1033,7 @@ std::optional DecodeColorMetadataPass(const QueueState& queue for (std::uint32_t slot = 0; slot < 8; ++slot) { if (((targetMask >> (4u * slot)) & 0xfu) == 0 || ((read(cx, 0x31c + slot * 0xfu) >> 2u) & 0x1fu) == 0) continue; const auto target = DecodeColorBuffer(cx, slot); + Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)"); Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled"); requireCovered(target); pass.targets.push_back(target); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b6f38a7f93e..647f20dee36 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7,6 +7,8 @@ #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp" @@ -1668,6 +1670,7 @@ void multisampleTests() { queue.context[0x292] = 3; 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; @@ -1697,6 +1700,28 @@ void multisampleTests() { queue = makeState(); queue.context[0x31c] |= 0x4000; expectFailure([&] { DecodeState(queue); }, "color compression"); + queue = multisampled(); + queue.context[0x31c] |= 0x2000; + queue.context[0x31f] = 0x1234; + 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"); + queue = multisampled(); + queue.context[0x31c] |= 0x4000; + queue.context[0x31f] = 0x1234; + 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"); + 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; + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + static_cast(DecodeState(queue)); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a CMASK register the decoder reads: " + std::to_string(read.offset)); queue = multisampled(); queue.context[0x0] = 0; @@ -1709,6 +1734,47 @@ void multisampleTests() { 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); expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents"); + queue = multisampled(); + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0030; + for (const auto offset : {0x318u, 0x31bu, 0x31cu, 0x390u}) queue.context[offset + (offset >= 0x390u ? 1u : 0xfu)] = queue.context[offset]; + queue.context[0x32c] = 0; + 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); + expectFailure([&] { DecodeColorMetadataPass(queue); }, "fast-clear color target"); + queue = multisampled(); + queue.context[0x2f8] = 0; + queue.context[0x0] = 0; + queue.context[0x202] = 0xcc0020; + expectFailure([&] { DecodeColorMetadataPass(queue); }, "multisampled color target"); + + using AgcDriver::Graphics::DccKeys; + using AgcDriver::Graphics::MultisampledCmask; + MultisampledCmask cmask(0x10000, 0x1000); + cmask.NoteFill(0x10000, 0x1000, 0x11111111u); + expectFailure([&] { cmask.TakeClear(); }, "leaves its tiles mixed"); + cmask.NoteFill(0x10800, 0x1000, 0); + expectFailure([&] { cmask.TakeClear(); }, "partly covers"); + cmask.NoteFill(0x10000, 0x1000, 0); + Require(cmask.TakeClear() && !cmask.TakeClear(), "a zero fill of the whole CMASK did not clear once"); + cmask.NoteFill(0x10000, 0x1000, 0xffffffffu); + Require(!cmask.TakeClear(), "an expanded CMASK cleared"); + cmask.NoteFill(0x20000, 0x1000, 0x11111111u); + Require(!cmask.TakeClear() && cmask.Keys() == DccKeys::Uncompressed, "a fill of another range changed the CMASK"); + cmask.CheckAddress(0x10000); + expectFailure([&] { cmask.CheckAddress(0x20000); }, "used CMASK 0x20000"); + MultisampledCmask seeded(0x30000, 0x1000); + seeded.SeedKeys(DccKeys::Clear0000); + Require(seeded.TakeClear(), "a fast-cleared seed did not clear its first use"); + MultisampledCmask mixedSeed(0x30000, 0x1000); + mixedSeed.SeedKeys(DccKeys::Mixed); + expectFailure([&] { mixedSeed.TakeClear(); }, "not modeled"); + MultisampledCmask unaddressed; + unaddressed.NoteFill(0, 0x1000, 0); + Require(!unaddressed.TakeClear(), "a target without a CMASK cleared"); } void DepthClipTests() { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 48a8cd098b0..c01cc5c593e 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -519,6 +519,11 @@ Throughout the project, every function at every stage either **does exactly what - [HTILE depth fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. - [Host imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, a run of adjacent registered ranges whose shared views are not one contiguous run of views of a section gets no read-write alias, so its ranges are imported one at a time, as before runs were imported whole, and a buffer that crosses two of them is copied. Adjacent registered ranges are joined into one import only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. +- [Host imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. +- [Cube texture descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Seamless cube filtering and out-of-range cube-array sampling remain unimplemented. PS5 equivalence has not been measured on console hardware. +- [Multisampled CMASK](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. +- [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. - [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 ae6dfd9073b3fc020350b4c2b9a17368daf44ab1 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 11 Oct 2026 09:59:05 +0200 Subject: [PATCH 4/8] 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) --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index b66fae1e8e7..eb0ca112cc0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -307,7 +307,7 @@ std::uint32_t effectiveDepthControl(std::uint32_t depthControl) { constexpr std::array, 2> StandardLocations2{{{4, 4}, {-4, -4}}}; constexpr std::array, 4> StandardLocations4{{{-2, -6}, {6, -2}, {-6, 2}, {2, 6}}}; -constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -8}}}; +constexpr std::array, 8> StandardLocations8{{{1, -3}, {-1, 3}, {5, 1}, {-3, -5}, {-5, 5}, {-7, -1}, {3, 7}, {7, -7}}}; std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 647f20dee36..3b27e5ac0a9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -1683,6 +1683,15 @@ void multisampleTests() { queue.context[0x302] = 0x44cc; expectFailure([&] { DecodeState(queue); }, "nonstandard sample locations"); queue = multisampled(); + 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.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 = multisampled(); queue.context[0x31d] = 0x1000; expectFailure([&] { DecodeState(queue); }, "fewer fragments than samples"); queue.context[0x31d] = 0; From b64da7caf733ee1734f10693477259f36cee0857 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 11 Oct 2026 16:32:47 +0200 Subject: [PATCH 5/8] fix(agc): GPL fragment libraries keyed by the sample count The fragment shader and fragment output libraries are built with the pipeline's rasterizationSamples, so a 1x and a 4x pipeline with the same shaders and formats must not share them. Reported by adriadam10 (found by Raghav314 in #2242). Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 ++ core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d4fbfec765a..27bfd7ee955 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -186,6 +186,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& appendKey(keys.preRasterization, state.depth.has_value() && state.depthBias); appendKey(keys.preRasterization, state.stages.tessellation.has_value()); if (state.stages.tessellation) appendKey(keys.preRasterization, state.stages.tessellation->inputControlPoints); + appendKey(keys.fragmentShader, state.samples); + appendKey(keys.fragmentOutput, state.samples); 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 0c791250021..15f75e4deaa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5040,6 +5040,13 @@ void pipelineLibraryTests(const Device& device) { state.colors.front().format = VK_FORMAT_R16G16B16A16_SFLOAT; static_cast(lookup()); Require(counters().built == std::array{2, 2, 3, 3}, "another target format rebuilt more than the fragment output library"); + state.samples = 4; + state.colors.front().samples = 4; + const auto multisampled = lookup(); + Require(counters().built == std::array{2, 2, 4, 4}, "a 4x pipeline shared the 1x fragment shader or fragment output library"); + 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"); 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 d109f3966fe82b0d12dd4d6bae711f66714ad220 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Sun, 11 Oct 2026 16:37:12 +0200 Subject: [PATCH 6/8] fix(agc): guest access to a multisampled color target's memory throws A multisampled target's samples are only on the host, so its guest memory never holds them: a CPU read through GuestMemory::Read/ReadCommitted, a DMA_DATA copy from it and a texture or storage image lookup at its address throw instead of returning bytes that were never written there. Asked by oneandonlydean; the test reads a resolved-only target's memory each way. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/include/GuestMemory.hpp | 1 + .../src/Driver/Packets/PacketMemory.cpp | 1 + .../Execution/src/GuestMemory.cpp | 8 ++ .../Graphics/include/MultisampleTarget.hpp | 1 + .../Graphics/src/MultisampleTarget.cpp | 51 +++++++++ .../Graphics/src/ShaderResources.cpp | 3 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 105 ++++++++++++++++++ 7 files changed, 170 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index ba201ca1519..5321424ce26 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -183,6 +183,7 @@ unsigned long long CodeOffset(const void* address); // every read and store through this interface calls first. Code that reads guest memory directly // calls FlushGpuWrites itself. void SetFlushHook(void (*hook)(std::uint64_t address, std::size_t bytes)); +void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)); void FlushGpuWrites(std::uint64_t address, std::size_t bytes); // Moves with every ForgetPages call (the libc invalidator: memory unmapped or re-registered): odd // while the call stores its page states, moved past it after, so a reader that loads an even diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 83641032434..a2ba8abae22 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -61,6 +61,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } } if (opcode == 0x50 && packet.size() >= 7) { + if (Pm4::DmaSource(packet) == 0 || Pm4::DmaSource(packet) == 3) Graphics::RequireNoMultisampleTarget(packet[2] | (static_cast(packet[3]) << 32u), packet[6] & 0x3ffffffu, "a DMA copy"); if (Pm4::DmaSource(packet) == 2 && Pm4::DmaDestination(packet) != 1) { const auto address = packet[4] | (static_cast(packet[5]) << 32u); Graphics::NoteDepthMetadataFill(address, packet[6] & 0x3ffffffu, packet[2]); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index f826163439c..b764c499d32 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1751,12 +1751,19 @@ void SetFlushHook(void (*hook)(std::uint64_t, std::size_t)) { flushHook.store(hook, std::memory_order_release); } +std::atomic readGuard{nullptr}; + +void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)) { + readGuard.store(guard, std::memory_order_release); +} + void FlushGpuWrites(std::uint64_t address, std::size_t bytes) { if (const auto hook = flushHook.load(std::memory_order_acquire)) hook(address, bytes); } void Read(std::uint64_t address, std::span destination, std::size_t alignment) { if (destination.empty()) return; + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); @@ -1773,6 +1780,7 @@ void Read(std::uint64_t address, std::span destination, std::size_t a void ReadCommitted(std::uint64_t address, std::span destination) { if (destination.empty()) return; + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index f0c327c0b3b..5d9d14728ac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -34,6 +34,7 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView); void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern); void ClearMultisampleTargets(VkDevice device); +void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 667b8711283..da9e4f889cd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -1,3 +1,4 @@ +#include #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" @@ -165,6 +166,41 @@ class MultisampleTarget { } }; +struct GuardedRange { + VkDevice device; + std::uint64_t address; + std::uint64_t bytes; + std::uint32_t samples; +}; + +std::shared_mutex& guardedMutex() { + static std::shared_mutex mutex; + return mutex; +} + +std::vector& guarded() { + static std::vector ranges; + return ranges; +} + +std::atomic& anyGuarded() { + static std::atomic any{false}; + return any; +} + +void guardReads(std::uint64_t address, std::size_t bytes) { + RequireNoMultisampleTarget(address, bytes, "a CPU read"); +} + +void guard(const Context& context, const ColorTarget& target) { + const auto bytes = std::max(target.bytes, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); + std::unique_lock lock(guardedMutex()); + guarded().push_back({context.device, target.address, bytes, target.samples}); + anyGuarded().store(true, std::memory_order_release); + static std::once_flag installed; + std::call_once(installed, [] { GuestMemory::SetReadGuard(&guardReads); }); +} + std::mutex& targetsMutex() { static std::mutex mutex; return mutex; @@ -183,6 +219,7 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) { return *target; } targets().push_back(std::make_unique(context, use)); + guard(context, use); auto& created = *targets().back(); if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast(use.cmaskBytes) * 256u)); created.ApplyFastClear(use); @@ -276,6 +313,20 @@ void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32 void ClearMultisampleTargets(VkDevice device) { std::lock_guard lock(targetsMutex()); std::erase_if(targets(), [&](const auto& target) { return target->context.device == device; }); + std::unique_lock guardLock(guardedMutex()); + std::erase_if(guarded(), [&](const auto& range) { return range.device == device; }); + anyGuarded().store(!guarded().empty(), std::memory_order_release); +} + +void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) { + if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return; + std::shared_lock lock(guardedMutex()); + for (const auto& range : guarded()) { + if (address >= range.address + range.bytes || range.address >= address + bytes) continue; + char text[256]; + std::snprintf(text, sizeof(text), "AGC graphics: %s of 0x%llx+0x%zx overlaps multisampled color target 0x%llx+0x%llx (%u samples), whose samples are only on the host: guest access to it is not implemented", access, static_cast(address), bytes, static_cast(range.address), static_cast(range.bytes), range.samples); + throw std::runtime_error(text); + } } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 8ce6009bf13..aacd4f29242 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "ThreadOwned.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -513,6 +514,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedTexture(const Context& context, std::span words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false) { CaptureTrace::Log("sampled-lookup address=%llx width=%u height=%u dcc=%llx", static_cast(resource.baseAddress), resource.width, resource.height, static_cast(resource.dccAddress)); + RequireNoMultisampleTarget(resource.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a texture read"); if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes(); if (auto depth = DepthSurfaceTexture(context, words, resource, components)) { logLookup({depth.get(), resource, 0, DccKeys::Uncompressed, 0, nullptr, true}); @@ -831,6 +833,7 @@ GuestTextureResource StorageSurface(const Context& context, const GuestTextureRe // `guestBytes` is the surface size when the caller described the surface already (0: described here). std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes) { + RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast(guestBytes) : 1u, "a storage image access"); if (DepthSurfaceAt(viewed.baseAddress)) { char text[112]; std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented", static_cast(viewed.baseAddress)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 15f75e4deaa..fe1a3164bbc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -8,6 +8,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -38,6 +39,7 @@ #else #include #endif +#include #include #include #include @@ -4312,6 +4314,108 @@ void dataRefreshTests(const Device& device, Recorder& recorder) { recorder.Sync(); } +void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { + const auto& base = device.GetContext(); + if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.NullDescriptors()) { + std::cout << "4x color targets or nullDescriptor unavailable: multisampled target guest access not tested\n"; + return; + } + constexpr std::uint32_t side = 64; + constexpr std::size_t targetBytes = side * side * 4u; + constexpr std::size_t bytes = 4u * 65536u; +#ifdef _WIN32 + void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +#else + void* block = std::aligned_alloc(65536, bytes); +#endif + Require(block != nullptr, "cannot allocate the multisampled target block"); + std::memset(block, 0, bytes); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + ClearMultisampleTargets(context.device); + ClearCachedTextures(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + } unregister{base, block}; + TextureDetiler detiler(base); + auto context = base; + context.detiler = &detiler; + context.nullDescriptors = true; + TextureCache cache(context); + context.textureCache = &cache; + const auto refused = [](const std::function& access, const std::string& expected, const std::string& what) { + std::string message; + try { + access(); + } catch (const std::exception& error) { + message = error.what(); + } + Require(message.find(expected) != std::string::npos && message.find("multisampled color target") != std::string::npos, what + " of a resolved-only multisampled target's memory did not throw: " + message); + }; + ColorTarget target{}; + target.address = address; + target.surfaceAddress = address; + target.extent = {side, side}; + target.surfaceExtent = target.extent; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.bytes = targetBytes; + target.samples = 4; + { + std::lock_guard gpu(GpuMutex()); + Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the multisampled target has no view"); + recorder.Submit(); + device.WaitQueue(); + recorder.Sync(); + } + std::array texels{}; + refused([&] { AgcDriver::GuestMemory::Read(address + 256u, texels); }, "a CPU read", "a CPU read"); + refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy"); + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; + binding.role = ShaderRecompiler::DescriptorRole::GuestImages; + binding.descriptorSet = 0; + binding.binding = ShaderRecompiler::RuntimeAbi::FirstImageBinding; + binding.count = 1; + binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D; + binding.imageSamplers = {0}; + binding.guestDescriptor = { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (56u << 20u) | (((side - 1u) & 3u) << 30u), + ((side - 1u) >> 2u) | ((side - 1u) << 14u), + 0xfacu | (9u << 28u), + 0u, + 0u, + 0u, + 0u, + }; + program.bindings.push_back(binding); + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + refused([&] { + std::lock_guard gpu(GpuMutex()); + ShaderResources resources(context, compute); + }, "a texture read", "a texture read"); + AgcDriver::GuestMemory::Read(address + 2u * 65536u, texels); + ClearMultisampleTargets(context.device); + AgcDriver::GuestMemory::Read(address + 256u, texels); + std::cout << "Multisampled target guest access tests passed\n"; +} + void depthSurfaceProofTests(const Device& device, Recorder& recorder) { const auto& base = device.GetContext(); if (!device.NullDescriptors()) { @@ -5153,6 +5257,7 @@ int main(int argc, char** argv) { resourceReadTests(device, recorder); readWrittenStagingTests(device, recorder); depthSurfaceProofTests(device, recorder); + multisampleGuestAccessTests(device, recorder); misalignedSnapshotTests(device, recorder); misalignedRegionTests(device, recorder); drawSnapshotReuseTests(device, recorder); From 5c5912c49e3710572a6740ee2a725579422bf19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 23:18:10 +0200 Subject: [PATCH 7/8] fix(agc): guard a multisampled target's whole sample planes; docs for host-only multisampled targets The guest-access guard covers CB bytes times samples (the single-sample layout size alone missed the later sample planes' tile padding), with a test row in that padding. Remove two TechnicalDebt lines duplicated by the rebase, document the guard bound and the unevicted targets, and add the multisampled targets to ARCHITECTURE.md. --- .../prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 7 ++++--- docs/dev/ARCHITECTURE.md | 1 + docs/dev/TechnicalDebt.md | 3 +-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index da9e4f889cd..743b469cdaa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -193,7 +193,7 @@ void guardReads(std::uint64_t address, std::size_t bytes) { } void guard(const Context& context, const ColorTarget& target) { - const auto bytes = std::max(target.bytes, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); + const auto bytes = std::max(target.bytes * target.samples, static_cast(target.extent.width) * target.extent.height * target.elementBytes * target.samples); std::unique_lock lock(guardedMutex()); guarded().push_back({context.device, target.address, bytes, target.samples}); anyGuarded().store(true, std::memory_order_release); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index fe1a3164bbc..227dddc118c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -4321,8 +4321,8 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { return; } constexpr std::uint32_t side = 64; - constexpr std::size_t targetBytes = side * side * 4u; - constexpr std::size_t bytes = 4u * 65536u; + constexpr std::size_t targetBytes = 65536u; + constexpr std::size_t bytes = 5u * 65536u; #ifdef _WIN32 void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else @@ -4410,7 +4410,8 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { std::lock_guard gpu(GpuMutex()); ShaderResources resources(context, compute); }, "a texture read", "a texture read"); - AgcDriver::GuestMemory::Read(address + 2u * 65536u, texels); + refused([&] { AgcDriver::GuestMemory::Read(address + 3u * 65536u + 256u, texels); }, "a CPU read", "a CPU read in the padding of a later sample plane"); + AgcDriver::GuestMemory::Read(address + 4u * 65536u, texels); ClearMultisampleTargets(context.device); AgcDriver::GuestMemory::Read(address + 256u, texels); std::cout << "Multisampled target guest access tests passed\n"; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 0f72550b6cf..7572b0e12da 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -86,6 +86,7 @@ flowchart LR - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A compute shader whose `s_swappc_b64` calls a user-data pointer cannot be linked at registration, because the callee's address is only known at dispatch. `InvocationFor` in [`ShaderRegistry.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) reads each callee from guest memory, appends one copy per call after the caller (`LinkUserDataCalls`) and prepares that linked code, so a different callee gets its own prepared entry. - A draw's `Graphics::Pipeline` comes from `CachedPipeline`. When the device has `VK_EXT_graphics_pipeline_library` with fast linking, `VK_EXT_extended_dynamic_state` and `VK_KHR_dynamic_rendering`, draws render with `vkCmdBeginRenderingKHR` instead of render passes and the pipeline is linked from four libraries (vertex input, pre-rasterization shaders, fragment shader, fragment output) kept per device in [`PipelineLibrary.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp). A library built for one pipeline is reused by the next: another blend, vertex layout or target format builds only that part (the shader libraries name no attachment format), and cull mode, front face, depth and stencil state are dynamic, so changing them only links. The fast link is what the draw waits for; a background thread then links the same libraries with link-time optimization and the pipeline binds that one from the next draw on. Push constants then use a 256-byte block with a 128-byte slot per side: the pre-rasterization stages stack from byte 0, the pixel shader always starts at byte 128 (`StagePushOffset`), so its library does not depend on how much push data its vertex shader uses. Pipelines with per-face depth bias, a separate alpha blend into an 8_8 target with the alternate component order (both draw twice), rect lists, geometry shaders or a stage without a variant id are created whole, still with dynamic rendering; devices without the extensions (or with `APS5_NO_GPL=1`) keep render passes and whole pipelines. +- A draw whose `PA_SC_AA_CONFIG` selects 2, 4 or 8 samples renders into host-only multisampled images. [`MultisampleTarget.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) keeps one image per device, surface address, format, extent and sample count (`MultisampleTargetView`); the pipeline, render pass and library keys carry `State::samples`, and a multisampled depth target is a multisampled depth surface. The guest sample planes and FMASK are never read or written, so the registry also guards the target's guest range: a CPU read (`GuestMemory::SetReadGuard`), a DMA copy, and a sampled or storage image read of it throw. A `CB_COLOR_CONTROL` resolve mode over a multisampled CB0 runs as a render pass whose resolve attachment is CB1's resident image (`ResolveMultisampleTarget`). A zero fill of the target's CMASK (`NoteColorMetadataFill`, from fill kernels and CP DMA fills) clears the image at its next use. ### Shader MODE register access diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c01cc5c593e..737fbe794a0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -519,11 +519,10 @@ Throughout the project, every function at every stage either **does exactly what - [HTILE depth fast clears](../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - HTILE is not modeled. Only RADV's `clear_htile_mask` compute kernel (17 GFX10 words, 64x1x1 groups) is recognized, and only when it leaves ZMASK at 0 (`user[4]` and `user[5]` low nibbles both 0); the dispatch still runs, and the host depth image is cleared to `DB_DEPTH_CLEAR` the next time the surface with that `DB_HTILE_DATA_BASE` address is bound. The address match is exact, so a clear of a different HTILE range does not hit. HTILE initialization, the stencil-only update, and fast clears made by the SDK's own paths, CP DMA fills or other kernels are not covered and leave the depth image as it was. The kernel words are the ones the PR author saw RADV submit; they are not compared with Mesa's source. - [Host imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. On Windows, a run of adjacent registered ranges whose shared views are not one contiguous run of views of a section gets no read-write alias, so its ranges are imported one at a time, as before runs were imported whole, and a buffer that crosses two of them is copied. Adjacent registered ranges are joined into one import only where they fill a whole 8 MiB aligned window; ranges of a partly filled window are imported one at a time, and a buffer that crosses two imports is copied. The 8 MiB window is a choice: unbounded runs were re-imported whole on every growth of a heap mapped in small pieces, and the NVIDIA driver (610.57.04, 615.78.08) then refused imports or lost the device. Not measured on RADV. - [Cube texture descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Out-of-range cube-array sampling remains unimplemented. Samplers with `DISABLE_CUBE_WRAP` are created with `VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT`, the flag RADV maps to that bit (Mesa `src/amd/vulkan/radv_sampler.c`, `src/amd/common/ac_descriptors.c`); without `VK_EXT_non_seamless_cube_map` a cube texture sampled through such a sampler throws. PS5 equivalence has not been measured on console hardware. -- [Host imports](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) on Linux skip ranges that are not mapped read-write, which are copied instead. The NVIDIA driver (595.91.07) refuses `VK_EXT_external_memory_host` imports of read-only or executable mappings (and shared mappings of regular files) with `VK_ERROR_OUT_OF_DEVICE_MEMORY`, after which every `vkAllocateMemory` on the device fails the same way. Read-only ranges are never imported, even on drivers that accept them. Shared mappings of regular files are not checked for. -- [Cube texture descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) with equal `BASE_ARRAY` and `DEPTH` expose six faces starting at `BASE_ARRAY`. Measured with `image_load`, `image_sample` and `image_get_resinfo` on RX 6600 (gfx1032), using 64x64 R32_UINT faces and base slices 0, 5 and 6. Other ranges containing fewer than six faces or incomplete cube arrays still throw: hardware load bounds and filtered-sample addressing differ. Seamless cube filtering and out-of-range cube-array sampling remain unimplemented. PS5 equivalence has not been measured on console hardware. - [Multisampled CMASK](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [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. - [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 208b908238aa9daea8f532dba0e3d2aaf6618097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 11 Oct 2026 23:26:42 +0200 Subject: [PATCH 8/8] test(agc): register the multisampled target guest-access test in ctest The test only ran in the full recorder binary, which no ctest entry starts. It now has its own mode and entry (agc_multisample_guest_access); with the guard back at the single-sample size its new padding row fails. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +++- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 349f1ea3d87..e0f74d37374 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -791,8 +791,10 @@ if(BUILD_TESTING) set_tests_properties(agc_descriptor_pool_recycling PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_draw_snapshot_cap COMMAND agc_driver_recorder_tests --draw-snapshot-cap-only) set_tests_properties(agc_draw_snapshot_cap PROPERTIES SKIP_RETURN_CODE 77) + add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only) + set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77) if(WIN32) - set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_host_import_unmap agc_unit_shadow agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_multisample_guest_access PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 227dddc118c..a1ab66b71a5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5230,6 +5230,10 @@ int main(int argc, char** argv) { std::cout << "CMASK copied clear, write-back and failure tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") { + multisampleGuestAccessTests(device, recorder); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") { drawDescriptorWriteTests(device, recorder); std::cout << "Draw descriptor writes and snapshot overrides passed\n";