diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 75f7e48d00d..eef942c7023 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2269,6 +2269,10 @@ target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $< target_include_directories(agc_driver_packed_unorm4444_color_target_tests PRIVATE $) +target_include_directories(agc_driver_packed_unorm4444_partial_write_tests PRIVATE $) + +target_include_directories(agc_driver_packed_unorm4444_multisample_partial_write_tests PRIVATE $) + target_include_directories(agc_driver_color1555_target_tests PRIVATE $) target_include_directories(agc_driver_color565_target_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 4b8ea5e15b3..309a87a2aa1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -185,6 +185,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; product->pixel.reversedBlend = product->state.reversedBlend; + product->pixel.targetMerge = Graphics::ExportTargetMerge(product->state); product->pixel.alphaToCoverage = product->state.alphaToCoverage; product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state); if (product->state.sampleLocationGrid) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 5cb99827514..ec65e9734ea 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -79,6 +79,12 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(value)); mix(pixel.dualSourceBlend); mix(pixel.reversedBlend); + mix(pixel.targetMerge.target); + mix(pixel.targetMerge.mask); + mix(pixel.targetMerge.samples); + for (const auto word : pixel.targetMerge.descriptor) mix(word); + mix(pixel.targetMerge.depthStencilWrites); + mix(pixel.targetMerge.depthStencilTests); mix(pixel.alphaToCoverage); mix(static_cast(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 288ce328040..62fea0e6ae3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -228,7 +228,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& s = a.state; const auto& t = b.state; const auto sameColor = [](const Graphics::ColorTarget& x, const Graphics::ColorTarget& y) { - return x.address == y.address && x.extent.width == y.extent.width && x.extent.height == y.extent.height && x.format == y.format && x.bytes == y.bytes && x.componentMapping == y.componentMapping && x.tileMode == y.tileMode && x.elementBytes == y.elementBytes && x.dccAddress == y.dccAddress && x.dccAlphaOnMsb == y.dccAlphaOnMsb && x.slot == y.slot && x.exportIndex == y.exportIndex; + return x.address == y.address && x.extent.width == y.extent.width && x.extent.height == y.extent.height && x.format == y.format && x.bytes == y.bytes && x.componentMapping == y.componentMapping && x.tileMode == y.tileMode && x.elementBytes == y.elementBytes && x.dccAddress == y.dccAddress && x.dccAlphaOnMsb == y.dccAlphaOnMsb && x.slot == y.slot && x.exportIndex == y.exportIndex && x.partialWriteMask == y.partialWriteMask; }; const auto sameBlend = [](const VkPipelineColorBlendAttachmentState& x, const VkPipelineColorBlendAttachmentState& y) { return x.blendEnable == y.blendEnable && x.srcColorBlendFactor == y.srcColorBlendFactor && x.dstColorBlendFactor == y.dstColorBlendFactor && x.colorBlendOp == y.colorBlendOp && x.srcAlphaBlendFactor == y.srcAlphaBlendFactor && x.dstAlphaBlendFactor == y.dstAlphaBlendFactor && x.alphaBlendOp == y.alphaBlendOp && x.colorWriteMask == y.colorWriteMask; @@ -262,8 +262,8 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false; - if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.truncatedCoverage != t.truncatedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.targetMerge != q.targetMerge || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false; + if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.truncatedCoverage != t.truncatedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ || s.pixelKill != t.pixelKill) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 89e151fb24e..17b5f7cb3b3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -97,6 +97,7 @@ float EffectiveMinLod(const GuestTextureResource& resource); bool IsNullTextureResource(std::span words); GuestTextureResource DecodeTextureResource(std::span words); +std::array EncodeTextureResource(const GuestTextureResource& resource); bool DescriptorSingleLevel(std::span words); bool FirstLayerDescriptor(std::span words, std::array& narrowed); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 1ee631c131f..abfb3226a68 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -87,6 +87,8 @@ class Pipeline { VkPipeline secondChannel = VK_NULL_HANDLE; std::size_t attachments = 0; std::size_t colorAttachments = 0; + std::vector boundAttachments; + std::uint32_t boundColorAttachments = 0; bool depthBounds = false; bool depthBias = false; bool libraries = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 704ad5d3076..2cbd239cf16 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -214,6 +214,7 @@ class Recorder { // Submits and waits for every batch, running completions in order. void Sync(); void CountSamples(); + bool CountingSamples() const { return countingSamples; } bool RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span rules); std::uint64_t SamplesTotal(); bool DumpSamples(VkDeviceAddress target, std::uint64_t address = 0); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 2008d77be3f..7cf40177edf 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -70,6 +70,7 @@ struct ColorTarget { std::uint32_t dccControl = 0; std::uint64_t fmaskAddress = 0; std::uint32_t fmaskPipeBankXor = 0; + std::uint16_t partialWriteMask = 0; }; struct DepthTarget { @@ -162,6 +163,7 @@ struct State { float pointSizeMax = 0.0f; std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; bool pixelEarlyZ = false; + bool pixelKill = false; }; // PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two @@ -186,6 +188,11 @@ bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order); std::array QuadSampleMasks(const State& state, const SampleOrder& order); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); +std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); +GuestTextureResource TargetMergeSurface(const ColorTarget& color); +bool DepthStencilWrites(const State& state); +bool DepthStencilTests(const State& state); +ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); bool PartialSampleMask(const State& state); bool UniformSampleMask(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 146b038eba5..b34f12db411 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -232,6 +232,9 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: mix(state.blends.size()); for (const auto& color : state.colors) mix(color.exportIndex); } + for (const auto& color : state.colors) { + if (color.partialWriteMask != 0u) mix(0x6d65726765000000ull | color.exportIndex); + } mix(state.renderExtent.width); mix(state.renderExtent.height); mix(state.samples); @@ -1322,6 +1325,10 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK"); Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target"); if (draw.indexed) RequireNoMultisampleTarget(draw.indexAddress, static_cast(indexBytes), "an index buffer read"); + if (state.pixelKill && !state.pixelEarlyZ && std::ranges::any_of(state.colors, [](const ColorTarget& color) { return color.partialWriteMask != 0; })) { + const auto* recorder = Recorder::Active(); + Require(recorder == nullptr || !recorder->CountingSamples(), "a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests cannot use while occlusion queries count samples: the early tests would count the pixels it kills"); + } APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders"); if (recipe != nullptr) { inputs.fragmentOutputs = recipe->fragmentOutputs; @@ -2500,6 +2507,10 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara if (color.samples > 1) { binding.multisampled = true; timer.phase(PhaseSetup); + if (color.partialWriteMask != 0) { + static_cast(MultisampleTargetView(context, color)); + static_cast(MultisampleTargetStorageView(context, TargetMergeSurface(color))); + } targetViews.push_back(MultisampleTargetView(context, color, binding.multisampleOwner, &binding.multisampleImage)); timer.phase(PhaseReadTarget); continue; @@ -2731,7 +2742,18 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara const bool multisampled = prepared.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"); for (const auto& binding : targets) { - if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); + if (!binding.multisampled) continue; + const auto& sources = resources->MultisampleSources(); + const auto reads = std::count(sources.begin(), sources.end(), binding.color.surfaceAddress); + Require(reads == 0 || (reads == 1 && binding.color.partialWriteMask != 0), "sampling a multisampled color target the draw renders to is not implemented"); + Require(binding.color.partialWriteMask == 0 || (reads == 1 && MultisampleTargetStorable(context, binding.color)), "the partial write of a multisampled packed color target does not store into the target's image"); + if (binding.color.partialWriteMask != 0) static_cast(MultisampleTargetStorageView(context, TargetMergeSurface(binding.color))); + } + for (const auto& binding : targets) { + if (binding.color.partialWriteMask == 0 || binding.multisampled) continue; + Require(lean && binding.resident != nullptr && !binding.proxied, "a partial write of a packed color target is merged into its resident image by a recorded draw only"); + const auto images = resources->StorageImages(); + Require(std::ranges::any_of(images, [&](const std::pair& image) { return image.first == binding.resident->Image() && image.second; }), "the partial write of a packed color target does not store into the target's resident image"); } 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); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 866371295c9..7a7788c9647 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -6,6 +6,7 @@ #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include #include +#include #include #include #include @@ -292,6 +293,43 @@ GuestTextureResource DecodeTextureResource(std::span words) return result; } +std::array EncodeTextureResource(const GuestTextureResource& resource) { + const bool multisampled = resource.dimension == TextureDimension::k2DMsaa; + Require((resource.dimension == TextureDimension::k2D && resource.samples == 1u) || (multisampled && resource.samples >= 2u && resource.samples <= 16u && std::has_single_bit(resource.samples)), "only 2D and multisampled 2D surfaces have a texture descriptor encoding"); + Require(resource.mipCount == 1u && resource.allocatedMipCount <= 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.depthOrLastArray == 0u && resource.baseArray == 0u && resource.dccAddress == 0u && resource.minLod == 0u && !resource.iterate256 && resource.bcSwizzle == 0u, "only single-level, single-layer surfaces without metadata have a texture descriptor encoding"); + Require(resource.width >= 1u && resource.width <= 0x4000u && resource.height >= 1u && resource.height <= 0x4000u && resource.format < 0x200u, "the surface's extent or format has no texture descriptor encoding"); + const auto xorMask = resource.tileMode == TextureTileMode::kS4KBX || resource.tileMode == TextureTileMode::kD4KBX ? 0xfffull : 0xffffull; + Require(XorSwizzleMode(resource.tileMode) != 0 ? (resource.baseAddress & xorMask) == 0 && (resource.pipeBankXor & ~xorMask) == 0 : resource.pipeBankXor == 0, "the surface's pipe and bank XOR has no texture descriptor encoding"); + const auto base = (resource.baseAddress | resource.pipeBankXor) >> 8u; + Require(base != 0 && (resource.baseAddress & 0xffu) == 0 && base <= 0xffffffffffull, "the surface's address has no texture descriptor encoding"); + std::uint32_t tileMode = 0; + switch (resource.tileMode) { + case TextureTileMode::kLinear: tileMode = 0x00; break; + case TextureTileMode::kStandard256B: tileMode = 0x01; break; + case TextureTileMode::kD256B: tileMode = 0x02; break; + case TextureTileMode::kStandard4KB: tileMode = 0x05; break; + case TextureTileMode::kD4KB: tileMode = 0x06; break; + case TextureTileMode::kStandard64KB: tileMode = 0x09; break; + case TextureTileMode::kD64KB: tileMode = 0x0a; break; + case TextureTileMode::kS64KBT: tileMode = 0x11; break; + case TextureTileMode::kD64KBT: tileMode = 0x12; break; + case TextureTileMode::kS4KBX: tileMode = 0x15; break; + case TextureTileMode::kD4KBX: tileMode = 0x16; break; + case TextureTileMode::kZ64KBX: tileMode = 0x18; break; + case TextureTileMode::kS64KBX: tileMode = 0x19; break; + case TextureTileMode::kD64KBX: tileMode = 0x1a; break; + case TextureTileMode::kR64KBX: tileMode = 0x1b; break; + } + const auto sampleLevel = static_cast(std::countr_zero(resource.samples)); + std::array words{}; + words[0] = static_cast(base); + words[1] = static_cast(base >> 32u) | (resource.format << 20u) | (((resource.width - 1u) & 3u) << 30u); + words[2] = ((resource.width - 1u) >> 2u) | ((resource.height - 1u) << 14u); + words[3] = resource.dstSelX | (resource.dstSelY << 3u) | (resource.dstSelZ << 6u) | (resource.dstSelW << 9u) | (sampleLevel << 16u) | (tileMode << 20u) | ((multisampled ? 14u : 9u) << 28u); + words[5] = sampleLevel << 4u; + return words; +} + float EffectiveMinLod(const GuestTextureResource& resource) { if (resource.minLod <= resource.baseLevel * 256u) return 0.0f; return std::min(static_cast(resource.minLod) / 256.0f, static_cast(resource.lastLevel)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 470b77f6f3d..00c006e96bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -355,6 +355,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& for (const auto& color : state.colors) { appendKey(keys.renderPass, color.format); appendKey(keys.renderPass, color.exportIndex); + appendKey(keys.renderPass, color.partialWriteMask != 0u); } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); @@ -397,6 +398,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); + for (const auto& color : state.colors) boundAttachments.push_back(color.partialWriteMask == 0u); + if (state.depth) boundAttachments.push_back(true); sampleLocationsEnabled = ValidateSampleLocations(context, state); const auto host = HostSampleLocationsFor(context, state); sampleLocationGrid = host.grid; @@ -406,7 +409,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 & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); + const bool boundColor = std::ranges::any_of(state.colors, [](const ColorTarget& color) { return color.partialWriteMask == 0u; }); + Require((state.hasColorTarget && (boundColor || state.colors.empty() || state.depth.has_value())) || (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"); @@ -479,6 +483,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput std::vector colors; std::vector references(state.blends.size(), VkAttachmentReference{VK_ATTACHMENT_UNUSED, attachmentLayout}); for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].partialWriteMask != 0u) continue; VkAttachmentDescription color{}; color.format = state.colors[index].format; color.samples = state.mergedCoverage || state.truncatedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast(state.samples); @@ -488,9 +493,10 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; color.initialLayout = attachmentLayout; color.finalLayout = attachmentLayout; + references.at(state.colors[index].exportIndex) = {static_cast(colors.size()), attachmentLayout}; colors.push_back(color); - references.at(state.colors[index].exportIndex) = {index, attachmentLayout}; } + boundColorAttachments = static_cast(colors.size()); VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = static_cast(references.size()); @@ -510,7 +516,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput subpass.pDepthStencilAttachment = &depthReference; } std::vector renderingColors(state.blends.size(), VK_FORMAT_UNDEFINED); - for (const auto& color : state.colors) renderingColors.at(color.exportIndex) = color.format; + for (const auto& color : state.colors) { + if (color.partialWriteMask == 0u) renderingColors.at(color.exportIndex) = color.format; + } VkPipelineRenderingCreateInfoKHR rendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; rendering.colorAttachmentCount = static_cast(renderingColors.size()); rendering.pColorAttachmentFormats = renderingColors.empty() ? nullptr : renderingColors.data(); @@ -527,7 +535,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (dynamicRendering) { depthFormat = state.depth ? HostDepthFormat(context, state.depth->format) : VK_FORMAT_UNDEFINED; colorSlots.assign(state.blends.size(), ~0u); - for (std::uint32_t index = 0; index < state.colors.size(); ++index) colorSlots.at(state.colors[index].exportIndex) = index; + for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].partialWriteMask == 0u) colorSlots.at(state.colors[index].exportIndex) = index; + } this->attachmentLayout = attachmentLayout; } VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; @@ -773,7 +783,11 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span(context, renderPass, targets, extent, layers); + std::vector boundViews; + for (std::size_t i = 0; i < targets.size(); ++i) { + if (boundAttachments[i]) boundViews.push_back(targets[i]); + } + auto framebuffer = std::make_shared(context, renderPass, renderPass == VK_NULL_HANDLE ? targets : std::span(boundViews), extent, layers); if (!resident) return framebuffer; // Beyond the bound the least recently used unreferenced entry goes. constexpr std::size_t bound = 8; @@ -831,7 +845,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V const auto locations = SampleLocations(state); const auto grid = sampleLocationGrid; const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(state.samples), grid, state.samples * grid.width * grid.height, locations.data()}; - const VkAttachmentSampleLocationsEXT initial{static_cast(state.colors.size()), info}; + const VkAttachmentSampleLocationsEXT initial{boundColorAttachments, info}; const VkSubpassSampleLocationsEXT post{0, info}; VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; sampleLocations.attachmentInitialSampleLocationsCount = state.depth ? 1 : 0; @@ -982,6 +996,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto& color : state.colors) { append(key, color.format); append(key, color.componentMapping); + append(key, color.partialWriteMask != 0u); } if (state.blends.size() != state.colors.size()) { for (const auto& color : state.colors) append(key, color.exportIndex); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index ceea544e4e2..5f81a61e05e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 8u; +constexpr std::uint32_t FileFormat = 9u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -195,6 +195,7 @@ void encodeState(Writer& writer, const State& state) { writer.U32(static_cast(color.format)); writer.U32(color.exportIndex); writer.Raw(color.componentMapping); + writer.Raw(color.partialWriteMask); } writer.Raws(std::span(state.blends)); for (const auto value : state.blendConstants) writer.Raw(value); @@ -260,6 +261,7 @@ void decodeState(Reader& reader, State& state) { color.format = static_cast(reader.U32()); color.exportIndex = reader.U32(); color.componentMapping = reader.Raw(); + color.partialWriteMask = reader.Raw(); state.colors.push_back(color); } reader.Raws(state.blends, 8u); @@ -1038,6 +1040,7 @@ State CanonicalPipelineState(const State& state) { canonicalColor.format = color.format; canonicalColor.exportIndex = color.exportIndex; canonicalColor.componentMapping = color.componentMapping; + canonicalColor.partialWriteMask = color.partialWriteMask; canonical.colors.push_back(canonicalColor); } canonical.blends = state.blends; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 23ed20c037e..a730b9df794 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -605,6 +605,25 @@ bool sampleCoverageSupported(const QueueState& queue, std::uint32_t samples, boo return (inputs & PixelSampleCoverageInput) == 0u || (!perSampleShading && (inputs & PixelSampleRateInputs) == 0u); } +bool pixelRateInputsKept(const QueueState& queue, const State& state) { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + constexpr std::uint32_t numbered = PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage); + constexpr std::uint32_t located = PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::PerspectiveCentroid) | PixelInputBit(PixelInput::PerspectivePullModel) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::LinearCentroid) | PixelInputBit(PixelInput::PositionZ) | PixelInputBit(PixelInput::PositionW); + const bool centred = state.customSampleLocations && std::ranges::all_of(state.sampleLocations, [](std::uint8_t location) { return location == 0u; }); + const auto moving = numbered | (centred ? 0u : located); + if (PixelProgramSkipped(queue)) return true; + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + if (ena == queue.context.end() || addr == queue.context.end()) return true; + const auto inputs = ena->second & addr->second; + if ((inputs & moving) != 0u) return false; + if (centred || (inputs & (PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY))) == 0u) return true; + const auto position = find(queue.context, 0x1b8); + const auto control = position == queue.context.end() ? 0u : position->second; + return (control & 0x100000u) != 0u || ((control >> 16u) & 3u) == 0u; +} + bool coveredCentroidSupported(const QueueState& queue) { const auto config = find(queue.context, 0x2f8); if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true; @@ -1102,7 +1121,8 @@ std::array PixelSampleLocations(const State& state, const Shad std::array locations{}; const bool sampleBarycentrics = (pixel.inputAddr & (PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample))) != 0u; const bool samplePosition = !state.perSampleShading && (pixel.posX || pixel.posY) && pixel.positionLocation == ShaderRecompiler::PixelPositionLocation::Sample; - if (!sampleBarycentrics && !samplePosition) return locations; + const bool mergedCentres = pixel.targetMerge.samples > 1u && (pixel.inputAddr & (PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter))) != 0u; + if (!sampleBarycentrics && !samplePosition && !mergedCentres) return locations; const auto samples = state.perSampleShading ? state.samples : 1u; for (std::uint32_t pixelIndex = 0; pixelIndex < 4u; ++pixelIndex) { for (std::uint32_t sample = 0; sample < samples; ++sample) { @@ -1515,6 +1535,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } result.quadSampleMask = decodeQuadSampleMask(cx, result.samples); result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203)); + result.pixelKill = !PixelProgramSkipped(queue) && (read(cx, 0x203) & 0x40u) != 0; if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) { static bool reported = false; if (!reported) { @@ -1560,7 +1581,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.blends.assign(exportCount, VkPipelineColorBlendAttachmentState{}); std::array requiredBlendConstants{}; std::uint32_t requiredBlendMask = 0; - for (const auto& color : result.colors) { + for (auto& color : result.colors) { const auto slot = color.slot; const auto blend = read(cx, 0x1e0 + slot); APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend); @@ -1628,8 +1649,25 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported"); Require(!state.blendEnable, "blending into " + packed + " color target is unsupported"); - const std::uint32_t packedMask = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 ? 0xfu : 7u; - Require((exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); + const bool nibbles = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4; + const std::uint32_t packedMask = nibbles ? 0xfu : 7u; + Require(nibbles || (exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); + if ((exportedMask & packedMask) != packedMask) { + Require(color.mipCount == 1u && color.depth == 1u && color.layers == 1u && color.arrayLastSlice == 0u, "partial writes of a mipmapped, volume or array " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); + Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented"); + color.partialWriteMask = PackedWriteMask(mapping, exportedMask); + state.colorWriteMask = 0; + if (color.samples > 1u) { + Require(sampleCoverageSupported(queue, result.samples, true), "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample, or reads sample-rate or ancillary inputs, is unsupported (the input holds the whole pixel's coverage, gl_SampleMaskIn only the invocation's samples)"); + if (!result.perSampleShading && !pixelRateInputsKept(queue, result)) { + const auto value = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; }; + char detail[160]; + std::snprintf(detail, sizeof(detail), " (SPI_PS_INPUT_ENA 0x%x, SPI_PS_INPUT_ADDR 0x%x, SPI_BARYC_CNTL 0x%x, sample 1 location 0x%02x)", value(0x1b3), value(0x1b4), value(0x1b8), result.sampleLocations[1]); + Require(false, std::string("partial writes of a multisampled 4_4_4_4 unorm color target run the pixel shader once per sample, so a shader the guest runs once per pixel cannot read the ancillary word or, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W or a position at the centroid or a sample: they would follow the invocation's sample") + detail); + } + result.perSampleShading = true; + } + } } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; @@ -1646,7 +1684,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { for (std::uint32_t i = 0; i < 3; ++i) { if ((requiredBlendMask & (1u << i)) != 0) result.blendConstants[i] = requiredBlendConstants[i]; } - if (!result.colors.empty()) result.blend = result.blends[result.colors.front().exportIndex]; + Require((!result.mergedCoverage && !result.truncatedCoverage) || std::ranges::none_of(result.colors, [](const ColorTarget& color) { return color.partialWriteMask != 0; }), "partial writes of a 4_4_4_4 unorm color target under merged or truncated coverage (a one-sample target under a multisampled rasterizer) are not implemented"); + if (!result.colors.empty()) { + result.color = result.colors.front(); + result.blend = result.blends[result.colors.front().exportIndex]; + } Require(result.reversedBlend == 0 || (exportSlots.size() == 1 && exportCount == 1), "blending into a color target with a reversed component order through a second source color needs that target to be the shader's only color export"); if (result.dualSourceBlend) { Require(exportSlots.size() == 2 && exportSlots[0] == 0 && exportSlots[1] == 1 && exportCount == 1, "dual-source blending needs pixel exports to MRT slots 0 and 1 and color writes to slot 0 alone"); @@ -1673,6 +1715,50 @@ std::array ExportPackings(const State& return packings; } +std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask) { + std::uint16_t mask = 0; + for (std::uint32_t component = 0; component < 4; ++component) { + if (((exportedMask >> ((componentMapping >> (2u * component)) & 3u)) & 1u) != 0) mask = static_cast(mask | (0xfu << (4u * component))); + } + return mask; +} + +GuestTextureResource TargetMergeSurface(const ColorTarget& color) { + auto surface = SurfaceForTarget(color); + if (color.samples > 1u) { + Require(surface.baseAddress == color.surfaceAddress, "a merged multisampled color target does not start at its surface"); + surface.dimension = TextureDimension::k2DMsaa; + surface.samples = color.samples; + } + return surface; +} + +bool DepthStencilWrites(const State& state) { + if (!state.depth) return false; + const auto writes = [](const VkStencilOpState& face) { return (face.writeMask & 0xffu) != 0 && (face.failOp != VK_STENCIL_OP_KEEP || face.passOp != VK_STENCIL_OP_KEEP || face.depthFailOp != VK_STENCIL_OP_KEEP); }; + return state.depthWrite || (state.stencilTest && (writes(state.stencilFront) || writes(state.stencilBack))); +} + +bool DepthStencilTests(const State& state) { + return state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest); +} + +ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { + ShaderRecompiler::TargetMerge merge; + for (const auto& color : state.colors) { + if (color.partialWriteMask == 0) continue; + Require(merge.mask == 0 && color.exportIndex < 8u && color.samples <= 8u, "a draw merges into one packed color target at most"); + Require(color.samples == 1u || state.perSampleShading, "a merged multisampled color target needs per-sample shading"); + merge.target = static_cast(color.exportIndex); + merge.mask = color.partialWriteMask; + merge.samples = static_cast(color.samples); + merge.descriptor = EncodeTextureResource(TargetMergeSurface(color)); + merge.depthStencilWrites = DepthStencilWrites(state); + merge.depthStencilTests = DepthStencilTests(state); + } + return merge; +} + bool PartialSampleMask(const State& state) { const auto full = (1u << state.samples) - 1u; return std::ranges::any_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) != full; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 39a2b17eb85..3c042797737 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8709,6 +8709,24 @@ std::string interlockShape(std::span words) { return shape; } +bool coherentImageRead(std::span words) { + std::map sources; + std::set coherent; + std::uint32_t image = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationCoherent) coherent.insert(words[at + 1]); + if (op == spv::OpLoad || op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) sources[words[at + 2]] = words[at + 3]; + if (op == spv::OpImageRead) image = words[at + 3]; + } + for (std::size_t step = 0; image != 0 && step < 8u; ++step) { + if (coherent.contains(image)) return true; + const auto source = sources.find(image); + image = source == sources.end() ? 0u : source->second; + } + return false; +} + std::string interlockExits(std::span words, std::size_t& kills, std::size_t& terminates) { struct Block { std::vector ops; @@ -9870,9 +9888,293 @@ void unorm4_4_4_4TargetTests() { Require(state.color.format == VK_FORMAT_R16_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0x1bu && state.color.packing == ColorExportPacking::Unorm4_4_4_4, "a 4_4_4_4 unorm target did not decode to an R16_UINT attachment with reversed exports"); const auto packings = AgcDriver::Graphics::ExportPackings(state); Require(packings[0] == ColorExportPacking::Unorm4_4_4_4 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 4_4_4_4 unorm packing"); + Require(state.color.partialWriteMask == 0u && state.blends[0].colorWriteMask == 0xfu && AgcDriver::Graphics::ExportTargetMerge(state).mask == 0u, "a full write of a 4_4_4_4 unorm target merged into the target"); auto changed = queue; - changed.context[0x8e] = 7; - expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a 4_4_4_4 unorm color target"); + for (const auto& [targetMask, wordMask] : {std::pair{1u, 0xf000u}, std::pair{7u, 0xfff0u}, std::pair{8u, 0x000fu}, std::pair{0xau, 0x0f0fu}}) { + changed.context[0x8e] = targetMask; + Require(AgcDriver::Graphics::DrawRejection(changed, false).empty(), "the draw precheck refused CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target"); + const auto partial = AgcDriver::Graphics::DecodeState(changed); + Require(partial.color.partialWriteMask == wordMask && partial.colors.front().partialWriteMask == wordMask && partial.blends[0].colorWriteMask == 0u && partial.blend.colorWriteMask == 0u, "CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target did not merge its nibbles behind a closed write mask"); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); + const auto expected = AgcDriver::Graphics::SurfaceForTarget(partial.color); + const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); + Require(merge.target == 0u && merge.mask == wordMask && merge.samples == 1u && !merge.depthStencilWrites && surface.baseAddress == expected.baseAddress && surface.pipeBankXor == expected.pipeBankXor && surface.width == expected.width && surface.height == expected.height && surface.format == expected.format && surface.tileMode == expected.tileMode && surface.dimension == AgcDriver::Graphics::TextureDimension::k2D && surface.mipCount == 1u && surface.samples == 1u, "the merge descriptor of CB_TARGET_MASK " + std::to_string(targetMask) + " does not decode to the target's surface"); + } + { + auto depthQueue = queue; + depthQueue.context[0x8e] = 1; + depthQueue.context[0x000] = 0; + depthQueue.context[0x002] = 0; + depthQueue.context[0x007] = (1u << 16u) | 3u; + depthQueue.context[0x00a] = 7; + depthQueue.context[0x00b] = 0; + depthQueue.context[0x010] = 0x22900983; + depthQueue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) depthQueue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) depthQueue.context[offset] = 0x200; + depthQueue.context[0x005] = 0x300; + depthQueue.context[0x01e] = 0x1; + depthQueue.context[0x008] = std::bit_cast(0.25f); + depthQueue.context[0x009] = std::bit_cast(0.75f); + struct DepthStencilCase { + std::uint32_t control; + std::uint32_t ops; + std::uint32_t masks; + bool writes; + }; + for (const auto& [control, ops, masks, writes] : {DepthStencilCase{0x00700710u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700712u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700714u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700716u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700711u, 0x00050050u, 0x0100ff01u, false}, DepthStencilCase{0x00700713u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700718u, 0x00050050u, 0x01ffff01u, false}}) { + depthQueue.context[0x200] = control; + depthQueue.context[0x10b] = ops; + depthQueue.context[0x10c] = depthQueue.context[0x10d] = masks; + const auto what = "DB_DEPTH_CONTROL " + std::to_string(control) + " with stencil operations " + std::to_string(ops) + " and masks " + std::to_string(masks); + Require(AgcDriver::Graphics::DrawRejection(depthQueue, false).empty(), "the draw precheck refused a partial 4_4_4_4 write under " + what); + const auto partial = AgcDriver::Graphics::DecodeState(depthQueue); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); + Require(partial.depth.has_value() == ((control & 0xbu) != 0u) && partial.depthBoundsTest == ((control & 8u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes && merge.depthStencilTests == ((control & 0xbu) != 0u), "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); + } + auto truncated = depthQueue; + truncated.context[0x200] = 0x00700712u; + truncated.context[0x010] = 0x22900987u; + truncated.context[0x2f8] = 0x00100001u; + truncated.context[0x292] = 3; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) truncated.context[0x2fe + pixel * 4u] = 0xcc44; + truncated.context[0x31d] = truncated.context.at(0x31d) & ~0x3f000u; + truncated.context[0x8e] = 0xf; + const auto full = AgcDriver::Graphics::DecodeState(truncated); + Require(full.samples == 2 && full.truncatedCoverage && !full.mergedCoverage && full.color.samples == 1 && full.color.partialWriteMask == 0u, "a full write of a one-sample 4_4_4_4 unorm target beside a 2x depth target did not decode as truncated coverage"); + truncated.context[0x8e] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(truncated); }, "under merged or truncated coverage"); + } + { + auto smoothed = queue; + smoothed.context[0x2f8] = 0x0020c002u; + smoothed.context[0x292] = 3; + smoothed.context[0x3b8] = 0x0dc00000u; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) smoothed.context[0x2fe + pixel * 4u] = 0xcc44; + smoothed.context[0x31d] = smoothed.context.at(0x31d) & ~0x3f000u; + smoothed.context[0x201] = 0x02130000u; + const auto merged = AgcDriver::Graphics::DecodeState(smoothed); + Require(merged.samples == 4 && merged.mergedCoverage && merged.color.samples == 1 && merged.color.partialWriteMask == 0u, "a full write of a one-sample 4_4_4_4 unorm target under OVERRASTERIZATION_AMOUNT 2 did not decode as merged coverage"); + smoothed.context[0x8e] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(smoothed); }, "under merged or truncated coverage"); + } + { + changed.context[0x8e] = 1; + const auto partial = AgcDriver::Graphics::DecodeState(changed); + auto killQueue = changed; + killQueue.context[0x203] = 0x40u; + const auto killState = AgcDriver::Graphics::DecodeState(killQueue); + killQueue.context[0x203] = 0x10u; + const auto earlyState = AgcDriver::Graphics::DecodeState(killQueue); + Require(killState.pixelKill && !killState.pixelEarlyZ && !earlyState.pixelKill && earlyState.pixelEarlyZ, "DB_SHADER_CONTROL.KILL_ENABLE of a 4_4_4_4 merge did not decode as a kill under late tests"); + constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; + auto merging = packedExportRequest(changed, partial, mergeCode); + merging.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(partial); + const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat}; + merging.target.supportedCapabilities = capabilities; + merging.target.registrationWordLimit = 1u << 20u; + const ShaderRecompiler::RequestSerializer mergeSerializer; + Require(mergeSerializer.Deserialize(mergeSerializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "the 4_4_4_4 target merge was lost in serialization"); + auto unmerged = merging; + unmerged.context.pixel->targetMerge = {}; + auto moved = merging; + moved.context.pixel->targetMerge.descriptor[0] += 0x100u; + std::vector mergedKey, unmergedKey, movedKey; + ShaderRecompiler::RecompileCacheKey::BuildInterface(merging, mergedKey); + ShaderRecompiler::RecompileCacheKey::BuildInterface(unmerged, unmergedKey); + ShaderRecompiler::RecompileCacheKey::BuildInterface(moved, movedKey); + Require(mergedKey != unmergedKey && mergedKey == movedKey, "the static shader key does not separate merging from plain writes, or depends on the merged target's address"); + for (const bool registration : {false, true}) { + merging.target.registrationModules = registration; + const auto merged = ShaderRecompiler::Recompile(merging); + const auto words = merged.spirv.Copy(); + std::size_t reads = 0, writes = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + reads += static_cast(words[at] & 0xffffu) == spv::OpImageRead ? 1u : 0u; + writes += static_cast(words[at] & 0xffffu) == spv::OpImageWrite ? 1u : 0u; + } + const auto what = std::string(registration ? "a registration" : "a specialized") + " 4_4_4_4 merge module"; + Require(merged.registrationModule == registration && reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(merging), what + " did not read and write the target once inside a pixel interlock after early tests: " + interlockShape(words)); + Require(coherentImageRead(words), what + " does not read the target through a Coherent image variable"); + const auto bound = std::ranges::any_of(merged.bindings, [&](const ShaderRecompiler::DescriptorBinding& binding) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) return false; + for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) { + if (std::equal(merging.context.pixel->targetMerge.descriptor.begin(), merging.context.pixel->targetMerge.descriptor.end(), binding.guestDescriptor.begin() + static_cast(offset))) return true; + } + return false; + }); + Require(bound, what + " does not bind the merged target as a storage image"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &merged, 0}}}; + Require(AgcDriver::Graphics::ValidateShaders(shaders, partial, subgroup, false) == std::set{0u}, what + " lost its export to attachment 0"); + } + auto lacking = merging; + const std::array noInterlock{spv::CapabilityShader, spv::CapabilityStorageImageReadWithoutFormat}; + lacking.target.supportedCapabilities = noInterlock; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(lacking)); }, "fragmentShaderPixelInterlock"); + auto unformatted = merging; + const std::array noFormatlessReads{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT}; + unformatted.target.supportedCapabilities = noFormatlessReads; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(unformatted)); }, "storage image reads without a format"); + auto killing = merging; + killing.context.pixel->pixelKillEnable = true; + killing.context.pixel->earlyZ = false; + for (const bool registration : {false, true}) { + killing.target.registrationModules = registration; + const auto words = ShaderRecompiler::Recompile(killing).spirv.Copy(); + std::size_t reads = 0, writes = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + reads += static_cast(words[at] & 0xffffu) == spv::OpImageRead ? 1u : 0u; + writes += static_cast(words[at] & 0xffffu) == spv::OpImageWrite ? 1u : 0u; + } + Require(reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(killing), "a killing 4_4_4_4 merge module under late tests of a draw without depth or stencil writes did not merge inside a pixel interlock after early tests"); + } + killing.context.pixel->targetMerge.depthStencilWrites = true; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(killing)); }, "kills pixels under late tests of a draw writing depth or stencil"); + auto earlyKilling = killing; + earlyKilling.context.pixel->earlyZ = true; + static_cast(ShaderRecompiler::Recompile(earlyKilling)); + auto exporting = merging; + exporting.context.pixel->depthExportEnable = true; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(exporting)); }, "exports depth"); + alignas(16) static std::array counted{}; + static const std::array counter{static_cast(reinterpret_cast(counted.data())), static_cast(reinterpret_cast(counted.data()) >> 32u) & 0xffffu, 16u, 0x31016facu}; + constexpr std::array countingCode{0x7e000281u, 0xe0c80000u, 0x80000000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto counting = packedExportRequest(changed, partial, countingCode); + counting.context.userData = counter; + counting.context.pixel->targetMerge = merging.context.pixel->targetMerge; + counting.target = merging.target; + counting.target.registrationModules = false; + counting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + static const std::array atomicCapabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInt64}; + counting.target.supportedCapabilities = atomicCapabilities; + counting.context.pixel->earlyZ = false; + static_cast(ShaderRecompiler::Recompile(counting)); + counting.context.pixel->targetMerge.depthStencilTests = true; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(counting)); }, "writes guest memory (stores, atomics or GDS) under late tests"); + counting.context.pixel->earlyZ = true; + const auto earlyWords = ShaderRecompiler::Recompile(counting).spirv.Copy(); + std::size_t atomics = 0; + for (std::size_t at = 5; at < earlyWords.size() && (earlyWords[at] >> 16u) != 0; at += earlyWords[at] >> 16u) atomics += static_cast(earlyWords[at] & 0xffffu) == spv::OpAtomicIAdd ? 1u : 0u; + Require(atomics == 1u && interlockShape(earlyWords) == "capability extension ordered begin end " && declaresEarlyFragmentTests(counting), "a 4_4_4_4 merge whose shader adds to a counter under early tests did not keep its one atomic add beside the merge"); + constexpr std::array gdsReadCode{0xbefc03ffu, 0x00000100u, 0x7e000280u, 0xd8da0000u, 0x04000000u, 0xbf8c0000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto gdsReading = packedExportRequest(changed, partial, gdsReadCode); + gdsReading.context.pixel->targetMerge = counting.context.pixel->targetMerge; + gdsReading.context.pixel->earlyZ = false; + gdsReading.target = counting.target; + static_cast(ShaderRecompiler::Recompile(gdsReading)); + constexpr std::array gdsAddCode{0xbefc03ffu, 0x00000100u, 0x7e000280u, 0xd8020000u, 0x00000000u, 0xbf8c0000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto gdsAdding = gdsReading; + gdsAdding.shader.code = gdsAddCode; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(gdsAdding)); }, "writes guest memory (stores, atomics or GDS) under late tests"); + auto lateMerge = counting; + lateMerge.context.pixel->earlyZ = false; + lateMerge.context.pixel->targetMerge = {}; + static_cast(ShaderRecompiler::Recompile(lateMerge)); + } + changed = queue; + changed.context[0x8e] = 1u; + changed.context[0x31d] = (changed.context.at(0x31d) & ~0x3f000u) | (1u << 12u) | (1u << 15u); + { + const auto multisampled = AgcDriver::Graphics::DecodeState(changed); + Require(multisampled.samples == 2u && multisampled.color.samples == 2u && multisampled.color.partialWriteMask == 0xf000u && multisampled.blends[0].colorWriteMask == 0u && multisampled.perSampleShading, "a partial write of a 2-sample 4_4_4_4 unorm target did not merge per sample behind a closed write mask"); + auto coverage = changed; + coverage.context[0x1b3] = 0x4002u; + coverage.context[0x1b4] = 0x4002u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(coverage); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); + coverage.context[0x8e] = 0xfu; + Require(!AgcDriver::Graphics::DecodeState(coverage).perSampleShading, "a full write of a 2-sample 4_4_4_4 unorm target with a SAMPLE_COVERAGE input was refused or ran per sample"); + struct InputCase { + std::uint32_t inputs; + std::uint32_t position; + const char* what; + }; + auto rasterized = changed; + rasterized.context[0x2f8] = 0x00100001u; + rasterized.context[0x292] = 3; + rasterized.context[0x3b8] = 0x0dc00000u; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) rasterized.context[0x2fe + pixel * 4u] = 0xcc44; + const auto inputState = [](AgcDriver::QueueState queue, std::uint32_t inputs, std::uint32_t position) { + queue.context[0x1b3] = inputs; + queue.context[0x1b4] = inputs; + queue.context[0x1b8] = position; + return queue; + }; + for (const auto& [inputs, position, what] : {InputCase{0x0004u, 0u, "PERSP_CENTROID"}, InputCase{0x0040u, 0u, "LINEAR_CENTROID"}, InputCase{0x0001u, 0u, "PERSP_SAMPLE"}, InputCase{0x0008u, 0u, "PERSP_PULL_MODEL"}, InputCase{0x2002u, 0u, "ANCILLARY"}, InputCase{0x0402u, 0u, "POS_Z_FLOAT"}, InputCase{0x0802u, 0u, "POS_W_FLOAT"}, InputCase{0x0302u, 0x20000u, "POS_X_FLOAT and POS_Y_FLOAT at a sample"}, InputCase{0x0102u, 0x10000u, "POS_X_FLOAT at the centroid"}}) { + auto moved = inputState(rasterized, inputs, position); + expectFailure([&] { AgcDriver::Graphics::DecodeState(moved); }, "cannot read the ancillary word or, unless every sample sits at the pixel centre"); + moved.context[0x293] = moved.context.at(0x293) | 0x10000u; + Require(AgcDriver::Graphics::DecodeState(moved).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target whose shader the guest runs per sample was refused for reading ") + what); + if (inputs == 0x2002u) { + expectFailure([&] { AgcDriver::Graphics::DecodeState(inputState(changed, inputs, position)); }, "cannot read the ancillary word"); + continue; + } + Require(AgcDriver::Graphics::DecodeState(inputState(changed, inputs, position)).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target raised from a one-sample rasterizer, every sample at the pixel centre, was refused for reading ") + what); + } + for (const auto& [inputs, position, what] : {InputCase{0x0302u, 0u, "POS_X_FLOAT and POS_Y_FLOAT at the centre"}, InputCase{0x0302u, 0x100000u, "POS_X_FLOAT and POS_Y_FLOAT at the upper-left corner"}, InputCase{0x9022u, 0u, "PERSP_CENTER, LINEAR_CENTER, FRONT_FACE and POS_FIXED_PT"}}) { + Require(AgcDriver::Graphics::DecodeState(inputState(rasterized, inputs, position)).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target whose pixel-rate shader reads ") + what + " was refused"); + } + const auto merge = AgcDriver::Graphics::ExportTargetMerge(multisampled); + const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); + Require(merge.samples == 2u && merge.mask == 0xf000u && surface.dimension == AgcDriver::Graphics::TextureDimension::k2DMsaa && surface.samples == 2u && surface.baseAddress == multisampled.color.surfaceAddress && surface.width == multisampled.color.extent.width && surface.height == multisampled.color.extent.height && surface.format == AgcDriver::Graphics::TargetMergeSurface(multisampled.color).format, "the merge descriptor of a 2-sample 4_4_4_4 unorm target does not decode to its multisampled surface"); + auto unshaded = multisampled; + unshaded.perSampleShading = false; + expectFailure([&] { static_cast(AgcDriver::Graphics::ExportTargetMerge(unshaded)); }, "needs per-sample shading"); + constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; + auto merging = packedExportRequest(changed, multisampled, mergeCode); + merging.context.pixel->targetMerge = merge; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample, spv::CapabilityInterpolationFunction}; + merging.target.supportedCapabilities = capabilities; + merging.target.fragmentShaderBarycentricEnabled = true; + const auto words = ShaderRecompiler::Recompile(merging).spirv.Copy(); + std::size_t sampled = 0; + bool sampleRate = false; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpCapability && words[at + 1] == spv::CapabilitySampleRateShading) sampleRate = true; + if (op == spv::OpImageRead && (words[at] >> 16u) == 7u && words[at + 5] == spv::ImageOperandsSampleMask) ++sampled; + if (op == spv::OpImageWrite && (words[at] >> 16u) == 6u && words[at + 4] == spv::ImageOperandsSampleMask) ++sampled; + } + Require(sampleRate && sampled == 2u && interlockShape(words) == "capability extension ordered begin end ", "a 2-sample 4_4_4_4 merge module does not read and write the shaded sample inside a pixel interlock"); + auto single = merging; + const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInterpolationFunction}; + single.target.supportedCapabilities = noMultisampleStorage; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(single)); }, "storage image multisampling"); + auto centred = merging; + centred.target.fragmentShaderBarycentricEnabled = false; + centred.context.pixel->sampleLocations = AgcDriver::Graphics::PixelSampleLocations(multisampled, *centred.context.pixel); + Require(std::ranges::all_of(centred.context.pixel->sampleLocations, [](std::uint8_t location) { return location == 0u; }), "a merge raised from a one-sample rasterizer did not keep every sample at the pixel centre"); + static_cast(ShaderRecompiler::Recompile(centred)); + const auto rasterizedState = AgcDriver::Graphics::DecodeState(rasterized); + auto offCentre = packedExportRequest(rasterized, rasterizedState, mergeCode); + offCentre.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(rasterizedState); + offCentre.context.pixel->iteratesSamples = rasterizedState.perSampleShading; + offCentre.context.pixel->sampleLocations = AgcDriver::Graphics::PixelSampleLocations(rasterizedState, *offCentre.context.pixel); + Require(std::ranges::any_of(offCentre.context.pixel->sampleLocations, [](std::uint8_t location) { return location != 0u; }), "a 2-sample rasterizer's merge did not pass its off-centre sample locations"); + offCentre.target.supportedCapabilities = capabilities; + offCentre.target.fragmentShaderBarycentricEnabled = false; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(offCentre)); }, "its centre-located barycentrics would follow the sample"); + offCentre.target.fragmentShaderBarycentricEnabled = true; + static_cast(ShaderRecompiler::Recompile(offCentre)); + alignas(16) static std::array counted{}; + static const std::array counter{static_cast(reinterpret_cast(counted.data())), static_cast(reinterpret_cast(counted.data()) >> 32u) & 0xffffu, 16u, 0x31016facu}; + constexpr std::array countingCode{0x7e000281u, 0xe0c80000u, 0x80000000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto counting = packedExportRequest(changed, multisampled, countingCode); + counting.context.userData = counter; + counting.context.pixel->targetMerge = merge; + counting.context.pixel->iteratesSamples = true; + counting.context.pixel->earlyZ = true; + counting.target = merging.target; + counting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + static const std::array atomicCapabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample, spv::CapabilityInterpolationFunction, spv::CapabilityInt64}; + counting.target.supportedCapabilities = atomicCapabilities; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(counting)); }, "each covered sample would repeat its writes"); + auto unmergedCounting = counting; + unmergedCounting.context.pixel->targetMerge = {}; + static_cast(ShaderRecompiler::Recompile(unmergedCounting)); + } for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; changed.context[0x31c] = (changed.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); @@ -11197,6 +11499,15 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { BuildPipelineStoreKey(mergedKey, context, mergedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); BuildPipelineStoreKey(mergedReplayed, context, mergedDecoded.state, mergedDecoded.vertexInput, layouts.View(), shaders, mergedDecoded.attachmentLayout); Require(mergedKey != fourSamples && mergedKey == mergedReplayed, "the decoded merged coverage state keys another pipeline"); + auto partialState = state; + partialState.colors[0].partialWriteMask = 0xf000u; + GraphicsPipelineRecipe partialDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(partialState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), partialDecoded), "the partial write recipe does not decode"); + Require(partialDecoded.state.colors.size() == partialState.colors.size() && partialDecoded.state.colors[0].partialWriteMask == 0xf000u, "the recipe dropped the merged target's partial write mask"); + std::vector partialKey, partialReplayed; + BuildPipelineStoreKey(partialKey, context, partialState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(partialReplayed, context, partialDecoded.state, partialDecoded.vertexInput, layouts.View(), shaders, partialDecoded.attachmentLayout); + Require(partialKey != original && partialKey == partialReplayed, "the decoded partial write state keys another pipeline"); auto movedState = state; movedState.viewport.x = 5; movedState.colors[0].address = 0x9000; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 18e07d3fcd7..06485a1a63e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4931,7 +4931,7 @@ void verifyPixelRequestSerialization() { value.posZ, value.posW, value.frontFace, value.frontFaceAllBits, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend, value.targetMerge); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -4979,6 +4979,7 @@ void verifyPixelRequestSerialization() { pixel.stencilRefExportEnable = true; pixel.ditheredAlphaToCoverage = true; pixel.reversedBlend = 0x570001u; + pixel.targetMerge = {1u, 0x0ff0u, 1u, {0x100u, 0x2u, 0x3u, 0x4u, 0x5u, 0x6u, 0x7u, 0x8u}}; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -5011,6 +5012,22 @@ void verifyPixelRequestSerialization() { withoutReversedBlend.context.pixel->reversedBlend = 0u; RecompileCacheKey::Build(withoutReversedBlend, replayKey); require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(withoutReversedBlend), "the reversed-order blend code changed the static shader identity"); + auto movedMerge = request; + movedMerge.context.pixel->targetMerge.descriptor[0] = 0x200u; + RecompileCacheKey::Build(movedMerge, replayKey); + require(key == replayKey, "the merged target's descriptor changed the static shader identity"); + auto depthWritingMerge = request; + depthWritingMerge.context.pixel->targetMerge.depthStencilWrites = true; + RecompileCacheKey::Build(depthWritingMerge, replayKey); + require(key != replayKey, "the merge draw's depth or stencil writes did not change the static shader identity"); + auto depthTestingMerge = request; + depthTestingMerge.context.pixel->targetMerge.depthStencilTests = true; + RecompileCacheKey::Build(depthTestingMerge, replayKey); + require(key != replayKey, "the merge draw's depth or stencil tests did not change the static shader identity"); + auto withoutMerge = request; + withoutMerge.context.pixel->targetMerge = {}; + RecompileCacheKey::Build(withoutMerge, replayKey); + require(key != replayKey, "a color target merge did not change the static shader identity"); auto withoutCoverage = request; withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); @@ -5033,7 +5050,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQS0AAAA=", "new requests did not use serialization version 45"); + require(requestPrefix(encoded, 8u) == "NVNQQS4AAAA=", "new requests did not use serialization version 46"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -5151,7 +5168,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQS4AAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQS8AAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5295,18 +5312,39 @@ void verifyVertexInfoVersion19() { while (allBitsOffset < plainPixel.size() && plainPixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset; require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); - constexpr std::size_t sampleMaskBytes = 16u; constexpr std::size_t iterationBytes = 35u; constexpr std::size_t sampleOrderBytes = 16u; + constexpr std::size_t mergeBytes = 40u; + constexpr std::size_t sampleMaskBytes = 16u; const auto orderOffset = blendOffset + 4u + iterationBytes; + const auto mergeOffset = static_cast(orderOffset + sampleOrderBytes); + auto merging = pixel; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 2u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true, true}; + const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 2u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 40 bytes the version 46 pixel info adds after the sample order"); + require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); + // A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag or storage size at the end. + const auto version43Bytes = [&](std::vector bytes) { + require(bytes.end() - bytes.begin() > mergeOffset + static_cast(mergeBytes) && std::equal(plainPixel.begin() + mergeOffset, plainPixel.begin() + mergeOffset + static_cast(mergeBytes), bytes.begin() + mergeOffset), "a replay fixture does not hold the default color target merge after the sample order"); + bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); + bytes.resize(bytes.size() - sizeof(std::uint32_t) - 2u); + return bytes; + }; + auto version42 = version43Bytes(plainPixel); + version42[4] = 42u; + const auto replay42 = serializer.Deserialize(encodeBase64(version42)); + require(replay42.request.context.pixel.has_value() && replay42.request.context.pixel->targetMerge == TargetMerge{} && serializer.Serialize(replay42.request) == encodedPixel, "a version 42 request read a color target merge or misread the fields after it"); + auto version39 = version42; + version39.pop_back(); + version39.erase(version39.begin() + static_cast(orderOffset), version39.begin() + mergeOffset); + version39[4] = 39u; + const auto replay39 = serializer.Deserialize(encodeBase64(version39)); + require(replay39.request.context.pixel.has_value() && replay39.request.context.pixel->targetMerge == TargetMerge{} && replay39.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(replay39.request) == encodedPixel, "a version 39 request read a sample order or color target merge, or misread the fields after them"); auto ordered = pixel; ordered.context.pixel->sampleOrder = {0x76543210u, 0x76543210u, 0x76543201u, 0x76543201u}; const auto orderedBytes = decodeBase64(serializer.Serialize(ordered)); require(orderedBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + static_cast(orderOffset), orderedBytes.begin()) && orderedBytes[orderOffset] == 0x10u && orderedBytes[orderOffset + 11u] == 0x76u && orderedBytes[orderOffset + 12u] == 0x01u && std::equal(plainPixel.begin() + static_cast(orderOffset + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast(orderOffset + sampleOrderBytes)), "the sample order is not the four words the version 42 pixel info adds after the stencil operation value export flag"); - auto orderedVersion34 = orderedBytes; - orderedVersion34.resize(orderedVersion34.size() - sizeof(std::uint32_t)); - orderedVersion34.pop_back(); - orderedVersion34.pop_back(); + auto orderedVersion34 = version43Bytes(orderedBytes); orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); @@ -5314,18 +5352,15 @@ void verifyVertexInfoVersion19() { orderedVersion34[4] = 34u; const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); - auto orderedVersion30 = orderedBytes; - orderedVersion30.resize(orderedVersion30.size() - 6u - sizeof(std::uint32_t)); + auto orderedVersion30 = version43Bytes(orderedBytes); + orderedVersion30.resize(orderedVersion30.size() - 4u); orderedVersion30.erase(orderedVersion30.begin() + static_cast(blendOffset + 4u), orderedVersion30.begin() + static_cast(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); require(orderedReplay30.request.context.pixel.has_value() && orderedReplay30.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay30.request) == encodedPixel, "a version 30 request read a sample order, or misread the fields after its reversed-order blend code"); auto orderedSpanned = ordered; orderedSpanned.target.spannedWaves = true; - auto orderedVersion32 = decodeBase64(serializer.Serialize(orderedSpanned)); - orderedVersion32.resize(orderedVersion32.size() - sizeof(std::uint32_t)); - orderedVersion32.pop_back(); - orderedVersion32.pop_back(); + auto orderedVersion32 = version43Bytes(decodeBase64(serializer.Serialize(orderedSpanned))); orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); orderedVersion32.pop_back(); @@ -5346,10 +5381,7 @@ void verifyVertexInfoVersion19() { sampledPixel.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x4u, 0x8u}; const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); - auto version30 = sampledBytes; - version30.resize(version30.size() - sizeof(std::uint32_t)); - version30.pop_back(); - version30.pop_back(); + auto version30 = version43Bytes(sampledBytes); version30.pop_back(); version30.pop_back(); version30.pop_back(); @@ -5360,10 +5392,7 @@ void verifyVertexInfoVersion19() { require(replay30.request.context.pixel.has_value() && !replay30.request.context.pixel->iteratesSamples && replay30.request.context.pixel->positionLocation == PixelPositionLocation::Center && replay30.request.context.pixel->sampleLocations == std::array{} && decodeBase64(serializer.Serialize(replay30.request)) == sampledBytes, "a version 30 request read pixel iteration, a position location or sample locations, or misread the fields after its reversed-order blend code"); auto spanned = sampledPixel; spanned.target.spannedWaves = true; - auto version32 = decodeBase64(serializer.Serialize(spanned)); - version32.resize(version32.size() - sizeof(std::uint32_t)); - version32.pop_back(); - version32.pop_back(); + auto version32 = version43Bytes(decodeBase64(serializer.Serialize(spanned))); version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); @@ -5372,19 +5401,13 @@ void verifyVertexInfoVersion19() { version32[4] = 32u; const auto replay32 = serializer.Deserialize(encodeBase64(version32)); require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag"); - auto version29 = sampledBytes; - version29.resize(version29.size() - sizeof(std::uint32_t)); - version29.pop_back(); - version29.pop_back(); + auto version29 = version43Bytes(sampledBytes); version29.pop_back(); version29.pop_back(); version29.pop_back(); version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); - auto version36 = sampledBytes; - version36.resize(version36.size() - sizeof(std::uint32_t)); - version36.pop_back(); - version36.pop_back(); + auto version36 = version43Bytes(sampledBytes); version36.pop_back(); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version36[4] = 36u; @@ -5393,10 +5416,7 @@ void verifyVertexInfoVersion19() { version29[4] = 29u; const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); - auto version28 = sampledBytes; - version28.resize(version28.size() - sizeof(std::uint32_t)); - version28.pop_back(); - version28.pop_back(); + auto version28 = version43Bytes(sampledBytes); version28.pop_back(); version28.pop_back(); version28.pop_back(); @@ -5405,10 +5425,7 @@ void verifyVertexInfoVersion19() { version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); - auto version27 = maskedBytes; - version27.resize(version27.size() - sizeof(std::uint32_t)); - version27.pop_back(); - version27.pop_back(); + auto version27 = version43Bytes(maskedBytes); version27.pop_back(); version27.pop_back(); version27.pop_back(); @@ -5417,10 +5434,7 @@ void verifyVertexInfoVersion19() { version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); - auto legacyPixel = plainPixel; - legacyPixel.resize(legacyPixel.size() - sizeof(std::uint32_t)); - legacyPixel.pop_back(); - legacyPixel.pop_back(); + auto legacyPixel = version43Bytes(plainPixel); legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); @@ -5540,7 +5554,7 @@ void verifyMergedHullConfiguration() { require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); - require(legacy.size() > 54u + pointTailBytes && legacy[4] == 45u, "a merged hull request did not use serialization version 45"); + require(legacy.size() > 54u + pointTailBytes && legacy[4] == 46u, "a merged hull request did not use serialization version 46"); legacy.resize(legacy.size() - sizeof(std::uint32_t) - 2u - pointTailBytes); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp new file mode 100644 index 00000000000..894ccc375a1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -0,0 +1,427 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 16; +constexpr std::array SampleCounts{2, 4, 8}; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr double EdgeMargin = 1.0 / 32.0; +constexpr std::array First{0.2f, 0.4f, 0.6f, 0.8f}; +constexpr std::array Second{13.0f / 15.0f, 0.0f, 1.0f, 0.0f}; +constexpr std::array Third{1.0f / 15.0f, 1.0f, 0.0f, 4.0f / 15.0f}; +constexpr std::array Fourth{0.0f, 7.0f / 15.0f, 0.0f, 11.0f / 15.0f}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +constexpr std::array, 3> Covering{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +constexpr std::array, 3> Cutting{{ + {-0.93f, -1.0f, 0.5f, 1.0f}, {0.37f, -1.0f, 0.5f, 1.0f}, {-0.21f, 0.94f, 0.5f, 1.0f} +}}; + +constexpr std::array, 3> Crossing{{ + {-0.6f, -0.2f, 0.5f, 1.0f}, {0.9f, -0.85f, 0.5f, 1.0f}, {0.5f, 0.9f, 0.5f, 1.0f} +}}; + +constexpr std::array, 3> Overlapping{{ + {-0.08f, -0.47f, 0.5f, 1.0f}, {1.0f, -0.13f, 0.5f, 1.0f}, {0.29f, 1.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 24> Programs{}; +std::size_t programCount = 0; +alignas(256) std::array Buffer{}; + +std::uint32_t MultisampledAttrib(std::uint32_t samples) { + const auto shift = static_cast(std::countr_zero(samples)); + return (shift << 12u) | (shift << 15u); +} + +std::uint32_t Unorm(float value) { + if (!(value > 0.0f)) return 0; + if (value >= 1.0f) return 15; + return static_cast(std::nearbyint(static_cast(value) * 15.0)); +} + +std::uint32_t Packed(std::array rgba) { + return Unorm(rgba[3]) | (Unorm(rgba[2]) << 4u) | (Unorm(rgba[1]) << 8u) | (Unorm(rgba[0]) << 12u); +} + +std::span Float32Program(std::array rgba) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < 4u; ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = std::bit_cast(rgba[vgpr]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t samples) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm4_4_4_4Info; + cx[0x31d] = MultisampledAttrib(samples); + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.samples == samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 packed R16_UINT surface of " + std::to_string(samples) + " samples"); + return color; +} + +AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask, std::uint32_t samples) { + AgcDriver::QueueState queue; + queue.userConfig[0x242] = 4; + queue.context = { + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, targetMask}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, Float32Export}, {0x1c3, 4}, + {0x318, static_cast(block.Address() >> 8u)}, {0x390, static_cast(block.Address() >> 40u)}, + {0x31b, 0}, {0x31c, Unorm4_4_4_4Info}, {0x31d, MultisampledAttrib(samples)}, + {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, (Side << 16u) | Side}, + {0x81, 0x80000000}, {0x82, (Side << 16u) | Side}, + {0x90, 0x80000000}, {0x91, (Side << 16u) | Side}, + {0x94, 0x80000000}, {0x95, (Side << 16u) | Side}, + {0x10f, std::bit_cast(static_cast(Side) / 2.0f)}, {0x110, std::bit_cast(static_cast(Side) / 2.0f)}, + {0x111, std::bit_cast(-static_cast(Side) / 2.0f)}, {0x112, std::bit_cast(static_cast(Side) / 2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; + return queue; +} + +AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exportedMask, std::uint32_t samples) { + const bool partial = exportedMask != 0xfu; + const auto decoded = AgcDriver::Graphics::DecodeState(Queue(block, exportedMask, samples)); + const auto& color = decoded.colors.front(); + char detail[256]; + std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the %u-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, samples, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); + Require(decoded.colors.size() == 1u && decoded.samples == samples && color.samples == samples && color.partialWriteMask == (partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(decoded); + Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded " + std::to_string(samples) + "-sample target exported another merge than its own surface"); + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.colors = decoded.colors; + state.color = decoded.color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = decoded.blends; + state.blend = decoded.blend; + state.blendConstants = {}; + state.samples = decoded.samples; + state.perSampleShading = decoded.perSampleShading; + return state; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, const std::array, 3>& triangle, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture, std::uint32_t samples) { + std::vector userData(16, 0u); + const auto address = reinterpret_cast(Buffer.data()); + const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(LoadCode.data()), std::as_bytes(std::span(LoadCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(LoadCode.data()), LoadCode, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + std::vector loaded(Side * Side * samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) loaded[first + tid] = Buffer[tid * Words + LoadResult]; + } + return loaded; +} + +double EdgeDistance(const std::array, 3>& triangle, double x, double y, std::size_t from) { + const auto pixels = [](const std::array& vertex) { + return std::array{(static_cast(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side}; + }; + const auto a = pixels(triangle[from]); + const auto b = pixels(triangle[(from + 1u) % triangle.size()]); + const auto c = pixels(triangle[(from + 2u) % triangle.size()]); + const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); + const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; + return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); +} + +double Inside(const std::array, 3>& triangle, double x, double y) { + return std::min({EdgeDistance(triangle, x, y, 0), EdgeDistance(triangle, x, y, 1), EdgeDistance(triangle, x, y, 2)}); +} + +void RunSamples(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t samples) { + const auto color = DecodeTarget(block, samples); + const auto full = TargetState(block, 0xfu, samples); + Draw(device, full, Covering, Float32Program(First)); + const auto merged = TargetState(block, 0x1u, samples); + Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); + Draw(device, merged, Cutting, Float32Program(Second)); + Draw(device, full, Overlapping, Float32Program(Third)); + const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); + const auto loaded = LoadSamples(device, texture, samples); + const auto background = Packed(First); + const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); + const auto over = Packed(Third); + Require(written != background && over != written && over != background, "the test colors do not differ in the merged nibble"); + const auto locations = AgcDriver::Graphics::SampleLocations(merged); + std::vector pixelsByCoverage(samples + 1u); + std::uint32_t checked = 0; + std::uint32_t overdrawn = 0; + std::uint32_t mergedAfterOverdraw = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto word = loaded[(y * Side + x) * samples + sample]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); + Require(word == written || word == background || word == over, std::string(where) + ", neither the merged word, the first draw's word nor the last draw's word"); + covered += word == written ? 1u : 0u; + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto distance = Inside(Cutting, px, py); + const auto overDistance = Inside(Overlapping, px, py); + if (std::fabs(distance) < EdgeMargin || std::fabs(overDistance) < EdgeMargin) continue; + ++checked; + const auto expected = overDistance > 0 ? over : distance > 0 ? written : background; + overdrawn += overDistance > 0 && distance > 0 ? 1u : 0u; + mergedAfterOverdraw += overDistance <= 0 && distance > 0 ? 1u : 0u; + Require(word == expected, std::string(where) + (overDistance > 0 ? ", which the last full write covers" : distance > 0 ? ", which only the merging triangle covers" : ", which no later triangle covers")); + } + ++pixelsByCoverage[covered]; + } + } + const auto partlyMerged = std::accumulate(pixelsByCoverage.begin() + 1, pixelsByCoverage.end() - 1, 0u); + Require(checked >= Side * Side * samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[samples] != 0 && partlyMerged != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); + Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); + Draw(device, full, Covering, Float32Program(First)); + const auto lowMerged = TargetState(block, 0x8u, samples); + Require(lowMerged.colors.front().partialWriteMask == 0x000fu, "a W write of the 4_4_4_4 target did not merge bits 3..0"); + Draw(device, merged, Cutting, Float32Program(Second)); + Draw(device, lowMerged, Crossing, Float32Program(Fourth)); + const auto twice = LoadSamples(device, texture, samples); + std::uint32_t checkedTwice = 0; + std::uint32_t mergedTwice = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto high = Inside(Cutting, px, py); + const auto low = Inside(Crossing, px, py); + if (std::fabs(high) < EdgeMargin || std::fabs(low) < EdgeMargin) continue; + ++checkedTwice; + mergedTwice += high > 0 && low > 0 ? 1u : 0u; + auto expected = background; + if (high > 0) expected = (expected & 0x0fffu) | (Packed(Second) & 0xf000u); + if (low > 0) expected = (expected & 0xfff0u) | (Packed(Fourth) & 0x000fu); + const auto word = twice[(y * Side + x) * samples + sample]; + char where[192]; + std::snprintf(where, sizeof(where), "after an X merge and a W merge drawn back to back, sample %u of pixel (%u, %u) holds 0x%04x, expected 0x%04x", sample, x, y, word, expected); + Require(word == expected, where); + } + } + } + Require(checkedTwice >= Side * Side * samples * 8u / 10u && mergedTwice != 0, "the back-to-back merges leave fewer than eight in ten samples clear of their edges, or no sample inside both"); + std::printf("4_4_4_4 unorm %u-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, a full write drawn right after covered %u merged samples, and %u samples kept both of two merges drawn back to back\n", samples, partlyMerged, overdrawn, mergedTwice); +} + +bool SupportsSamples(const AgcDriver::Graphics::Context& context, std::uint32_t samples) { + VkImageFormatProperties properties{}; + const VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + return (context.limits.framebufferColorSampleCounts & samples) != 0u && context.imageFormatProperties(context.physical, VK_FORMAT_R16_UINT, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0u; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + for (const auto& [present, feature] : {std::pair{context.storageImageMultisample, "shaderStorageImageMultisample"}, std::pair{context.sampleRateShading, "sampleRateShading"}, std::pair{TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT), "fragmentShaderPixelInterlock"}}) { + if (present) continue; + std::printf("skipped, the device lacks %s\n", feature); + return VulkanTestSkipped; + } + std::vector> blocks; + std::uint32_t tested = 0; + for (const auto samples : SampleCounts) { + if (!SupportsSamples(context, samples)) { + std::printf("%u-sample R16_UINT storage color targets skipped, the device lacks them\n", samples); + continue; + } + blocks.push_back(std::make_unique(4u * BlockBytes)); + RunSamples(*device, *blocks.back(), samples); + ++tested; + } + if (tested == 0) return VulkanTestSkipped; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp new file mode 100644 index 00000000000..1acad9581f7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -0,0 +1,531 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; +constexpr std::uint32_t PositionInputs = 0x300u; +constexpr std::uint32_t DepthInput = 0x400u; +constexpr std::uint32_t LateKill = 0x40u; +constexpr std::array CheckerboardKill{0x7e040f00u, 0x7e060f01u, 0x3a040702u, 0x36040481u, 0x7da40480u}; + +enum class Kill { None, BeforeExport, AfterExport }; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 64> Programs{}; +std::size_t programCount = 0; + +std::uint32_t Unorm(float value) { + if (!(value > 0.0f)) return 0; + if (value >= 1.0f) return 15; + return static_cast(std::nearbyint(static_cast(value) * 15.0)); +} + +std::uint32_t Packed(std::array rgba) { + return Unorm(rgba[3]) | (Unorm(rgba[2]) << 4u) | (Unorm(rgba[1]) << 8u) | (Unorm(rgba[0]) << 12u); +} + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed, Kill kill = Kill::None) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + if (kill == Kill::BeforeExport) { + for (const auto word : CheckerboardKill) code[at++] = word; + } + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + if (kill == Kill::AfterExport) { + code[at++] = compressed ? 0xf800040fu : 0xf800000fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + for (const auto word : CheckerboardKill) code[at++] = word; + code[at++] = 0xf8001890u; + code[at++] = 0u; + } else { + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + } + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgba, Kill kill = Kill::None) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false, kill); +} + +std::span Float16Program(std::array rgba) { + return Program({Half(rgba[0]) | (Half(rgba[1]) << 16u), Half(rgba[2]) | (Half(rgba[3]) << 16u), 0, 0}, true); +} + +std::span DepthAsXProgram(std::array rgba) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0x7e080300u; + for (std::uint32_t vgpr = 1; vgpr < 4u; ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = std::bit_cast(rgba[vgpr]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +const std::array, 3>& TriangleAt(float z) { + static std::map, 3>> triangles; + return triangles.try_emplace(z, std::array, 3>{{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}).first->second; +} + +struct DrawSetup { + std::uint32_t inputs = 0; + std::uint32_t shaderControl = 0; + std::span> vertices = Triangle; + std::uint64_t depth = 0; + bool depthWrite = false; +}; + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm4_4_4_4Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.tileMode == ColorTileMode::RenderTarget && color.bytes == block.bytes, "the tiled 4_4_4_4 unorm target did not decode as a packed R16_UINT target"); + return color; +} + +std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(setup.vertices.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(setup.vertices.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const bool partial = exportedMask != 0xfu; + color.partialWriteMask = partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = partial ? 0u : VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.pixelKill = (setup.shaderControl & LateKill) != 0u; + if (setup.depth != 0) { + state.depth = AgcDriver::Graphics::DepthTarget{setup.depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; + state.depthTest = true; + state.depthWrite = setup.depthWrite; + state.depthCompare = VK_COMPARE_OP_LESS; + } + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, setup.inputs}, {0x1b4, setup.inputs}, {0x203, setup.shaderControl}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + Require(fragment.context.pixel->pixelKillEnable == ((setup.shaderControl & LateKill) != 0u) && !fragment.context.pixel->earlyZ && fragment.context.pixel->targetMerge.depthStencilWrites == (partial && setup.depth != 0 && setup.depthWrite), "the draw's pixel kill under late tests or its depth and stencil writes did not decode"); + Require((fragment.context.pixel->targetMerge.mask != 0u) == partial && fragment.context.pixel->targetMerge.mask == color.partialWriteMask, "the draw's target merge disagrees with its write mask"); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const bool registered = pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId; + Require(registered != partial, partial ? "the merging draw reused the registered pixel shader" : "the full write did not use the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(setup.vertices.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + return color.partialWriteMask; +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const std::uint32_t expected = inside ? (before[index] & ~wordMask) | (word & wordMask) : before[index]; + if (stored[index] == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, inside ? "inside" : "outside", stored[index], expected, before[index]); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block, std::uint32_t seed) { + std::vector words(block.bytes / 2u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast(((index + seed) * 0x9e3779b9u) >> 11u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +struct Case { + std::uint32_t mask; + std::uint32_t wordMask; + std::array rgba; +}; + +std::uint32_t DrawCase(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t mask, std::uint32_t wordMask, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { + const auto drawn = Draw(device, color, mask, exportFormat, pixelCode, setup); + char detail[160]; + std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x merged the word mask 0x%04x, not 0x%04x", mask, drawn, mask == 0xfu ? 0u : wordMask); + Require(drawn == (mask == 0xfu ? 0u : wordMask), detail); + return wordMask; +} + +void ExpectWords(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& expected, const std::vector& before, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + if (stored[index] == expected[index]) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, stored[index], expected[index], before[index]); + throw std::runtime_error(what + detail); + } + } +} + +void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, std::span draws, std::uint32_t seed, const std::string& what) { + const auto before = Fill(block, seed); + auto expected = before; + for (const auto& [mask, wordMask, rgba] : draws) { + DrawCase(device, color, mask, wordMask, Float32Export, Float32Program(rgba)); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < ScissorWidth; ++x) { + auto& word = expected[layout.Offset(x, y) / 2u]; + word = static_cast((word & ~wordMask) | (Packed(rgba) & wordMask)); + } + } + } + ExpectWords(device, block, layout, expected, before, what); +} + +void SameDrawOverdraw(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, float first, float last, std::uint32_t seed) { + constexpr std::uint32_t Layers = 16; + static std::map>> stacks; + auto& vertices = stacks[last]; + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const auto& triangle = TriangleAt(layer % 2u == Layers % 2u ? first : last); + vertices.insert(vertices.end(), triangle.begin(), triangle.end()); + } + const auto before = Fill(block, seed); + auto expected = before; + const std::uint32_t nibble = static_cast(std::nearbyint(static_cast(last) * 15.0)); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < ScissorWidth; ++x) { + auto& word = expected[layout.Offset(x, y) / 2u]; + word = static_cast((word & 0x0fffu) | (nibble << 12u)); + } + } + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, DepthAsXProgram({0.0f, 0.4f, 0.6f, 1.0f}), {DepthInput, 0u, vertices}); + char what[160]; + std::snprintf(what, sizeof(what), "one draw of %u full-screen triangles alternating z %.2f and %.2f, merging z into X", Layers, first, last); + ExpectWords(device, block, layout, expected, before, what); +} + +void DepthRejection(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color) { + Block depth(static_cast(Width) * Height * 4u); + const std::vector cleared(depth.bytes / 4u, 1.0f); + std::memcpy(depth.data, cleared.data(), depth.bytes); + const std::array nearer{0.6f, 0.2f, 0.8f, 0.4f}; + const std::array behind{0.0f, 1.0f, 1.0f, 1.0f}; + const std::array front{1.0f, 0.0f, 0.0f, 1.0f / 15.0f}; + auto before = Fill(block, 301u); + auto expected = before; + const auto inside = [](std::uint32_t x) { return x < ScissorWidth; }; + const auto survivor = [&](std::uint32_t x, std::uint32_t y) { return inside(x) && ((x ^ y) & 1u) == 0u; }; + const auto expect = [&](const auto& select, std::uint32_t word, std::uint32_t wordMask) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + if (!select(x, y)) continue; + auto& stored = expected[layout.Offset(x, y) / 2u]; + stored = static_cast((stored & ~wordMask) | (word & wordMask)); + } + } + }; + DrawCase(device, color, 0xfu, 0xffffu, Float32Export, Float32Program(nearer, Kill::BeforeExport), {PositionInputs, LateKill, TriangleAt(0.5f), depth.Address(), true}); + expect(survivor, Packed(nearer), 0xffffu); + ExpectWords(device, block, layout, expected, before, "a full write at z 0.5 that kills a checkerboard and writes depth"); + before = expected; + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(behind), {0u, 0u, TriangleAt(0.75f), depth.Address(), false}); + expect([&](std::uint32_t x, std::uint32_t y) { return inside(x) && !survivor(x, y); }, Packed(behind), 0xf000u); + ExpectWords(device, block, layout, expected, before, "an X merge at z 0.75 behind the depth 0.5 the surviving pixels hold"); + before = expected; + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(front), {0u, 0u, TriangleAt(0.25f), depth.Address(), false}); + expect([&](std::uint32_t x, std::uint32_t) { return inside(x); }, Packed(front), 0xf000u); + ExpectWords(device, block, layout, expected, before, "an X merge at z 0.25 in front of every stored depth"); +} + +void CountedKill(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color) { + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the test device has no command recorder to count samples"); + recorder->CountSamples(); + } + const auto before = Fill(block, 501u); + const std::array rgba{0.6f, 0.2f, 0.8f, 0.4f}; + std::string refusal; + try { + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(rgba, Kill::BeforeExport), {PositionInputs, LateKill}); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("while occlusion queries count samples") != std::string::npos, "a merge that kills under late tests drew while occlusion queries count samples" + (refusal.empty() ? std::string() : ": " + refusal)); + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(rgba)); + ExpectSurface(device, block, layout, before, Packed(rgba), 0xf000u, "a merged X write that kills nothing while occlusion queries count samples"); +} + +void ExpectKilled(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool kept = x < ScissorWidth && ((x ^ y) & 1u) == 0u; + const std::uint32_t expected = kept ? (before[index] & ~wordMask) | (word & wordMask) : before[index]; + if (stored[index] == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": %s pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", kept ? "a surviving" : x < ScissorWidth ? "a killed" : "an unscissored", x, y, stored[index], expected, before[index]); + throw std::runtime_error(what + detail); + } + } +} + +std::string Describe(std::uint32_t mask, std::array rgba) { + char text[160]; + std::snprintf(text, sizeof(text), "CB_TARGET_MASK 0x%x write of (%.9g, %.9g, %.9g, %.9g)", mask, rgba[0], rgba[1], rgba[2], rgba[3]); + return text; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) { + std::puts("skipped, the device lacks fragmentShaderPixelInterlock"); + return VulkanTestSkipped; + } + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 2); + Block block(layout.Bytes()); + const auto color = DecodeTarget(block); + Require(AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 1u) == 0xf000u && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 8u) == 0x000fu && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 5u) == 0xf0f0u, "the export components did not map to the guest nibbles (X in 15..12, W in 3..0)"); + const std::array cases{{ + {0x1u, 0xf000u, {1.0f, 0.0f, 0.0f, 0.0f}}, + {0x1u, 0xf000u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, + {0x5u, 0xf0f0u, {0.2f, 0.4f, 0.6f, 0.8f}}, + {0x2u, 0x0f00u, {0.0f, 2.0f / 15.0f, 1.0f, 1.0f}}, + {0x8u, 0x000fu, {1.0f, 1.0f, 1.0f, 11.0f / 15.0f}}, + {0x6u, 0x0ff0u, {0.0f, 0.0f, 0.0f, 0.0f}}, + {0xeu, 0x0fffu, {0.0f, 1.0f, 0.5f, 0.25f}}, + {0xfu, 0xffffu, {0.1f, 0.7f, 0.9f, 0.3f}}, + {0x1u, 0xf000u, {0.0f, 0.0f, 0.0f, 0.0f}}, + }}; + const std::array goldens{0xf000u, 0x7fffu, 0x369cu, 0x02ffu, 0xfffbu, 0x0000u, 0x0f84u, 0x2ad5u, 0x0000u}; + auto before = Fill(block, 0); + for (std::size_t index = 0; index < cases.size(); ++index) { + const auto& [mask, wordMask, rgba] = cases[index]; + DrawCase(*device, color, mask, wordMask, Float32Export, Float32Program(rgba)); + ExpectSurface(*device, block, layout, before, goldens[index], wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + before = ReadBack(*device, block); + } + const std::array b{2.0f / 15.0f, 1.0f, 1.0f, 1.0f}; + const std::array c{0.6f, 0.2f, 0.8f, 0.4f}; + const std::array d{1.0f, 0.0f, 9.0f / 15.0f, 0.0f}; + const std::array mergeThenFull{{{0x1u, 0xf000u, b}, {0xfu, 0xffffu, c}}}; + const std::array fullThenMerge{{{0xfu, 0xffffu, c}, {0x1u, 0xf000u, b}}}; + const std::array mergeThenMerge{{{0x1u, 0xf000u, b}, {0x4u, 0x00f0u, d}, {0x8u, 0x000fu, c}}}; + BackToBack(*device, block, layout, color, mergeThenFull, 11, "a merged X write followed at once by a full write"); + BackToBack(*device, block, layout, color, fullThenMerge, 12, "a full write followed at once by a merged X write"); + BackToBack(*device, block, layout, color, mergeThenMerge, 13, "merged X, Z and W writes drawn back to back"); + before = Fill(block, 77); + const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; + const auto wordMask = DrawCase(*device, color, 0x9u, 0xf00fu, Float16Export, Float16Program(halves)); + ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); + for (const auto kill : {Kill::BeforeExport, Kill::AfterExport}) { + for (const auto& [mask, wordMask, rgba] : {Case{0x1u, 0xf000u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, 0x0ff0u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { + before = Fill(block, 91u + mask); + const auto killedMask = DrawCase(*device, color, mask, wordMask, Float32Export, Float32Program(rgba, kill), {PositionInputs, LateKill}); + ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, std::string(kill == Kill::BeforeExport ? "a late-test checkerboard kill before " : "a late-test checkerboard kill after ") + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + } + } + SameDrawOverdraw(*device, block, layout, color, 0.2f, 0.8f, 401u); + SameDrawOverdraw(*device, block, layout, color, 0.8f, 0.2f, 402u); + DepthRejection(*device, block, layout, color); + CountedKill(*device, block, layout, color); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences, 4 checkerboard-killing draws, 2 same-draw overdraws of 16 layers, a depth-rejected merge and a refused killing merge while samples are counted, into a %ux%u tiled target, the killed, rejected and unscissored pixels kept\n", cases.size() + 1u, Width, Height); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 10aaf73ff46..7c90dd1d839 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -218,6 +218,11 @@ class RecompileCacheKey { append(key, value.positionLocation); append(key, value.sampleLocations); append(key, value.targetOutputMode); + append(key, value.targetMerge.target); + append(key, value.targetMerge.mask); + append(key, value.targetMerge.samples); + append(key, value.targetMerge.depthStencilWrites); + append(key, value.targetMerge.depthStencilTests); append(key, value.sampleOrder); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index ebfe29da9fc..62014408a58 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -357,6 +357,12 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { for (const std::uint8_t location : info.sampleLocations) writer.WriteU8(location); writer.WriteBool(info.stencilOperationValueExport); for (const std::uint32_t order : info.sampleOrder) writer.WriteU32(order); + writer.WriteU8(info.targetMerge.target); + writer.WriteU32(info.targetMerge.mask); + writer.WriteU8(info.targetMerge.samples); + for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word); + writer.WriteBool(info.targetMerge.depthStencilWrites); + writer.WriteBool(info.targetMerge.depthStencilTests); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -428,6 +434,16 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 42u) { for (std::uint32_t& order : info.sampleOrder) order = reader.ReadU32(); } + if (version >= 46u) { + info.targetMerge.target = reader.ReadU8(); + const auto mask = reader.ReadU32(); + if (mask > 0xffffu) throw std::runtime_error("invalid recompile request target merge mask"); + info.targetMerge.mask = static_cast(mask); + info.targetMerge.samples = reader.ReadU8(); + for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32(); + info.targetMerge.depthStencilWrites = reader.ReadBool(); + info.targetMerge.depthStencilTests = reader.ReadBool(); + } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -832,7 +848,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(45u); + writer.WriteU32(46u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -881,7 +897,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 45u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 46u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 307b83cdff8..0b6a5ceb0d3 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -105,6 +105,7 @@ struct ImageResource { std::uint32_t emulatedCompare = 0; std::uint32_t table = NoTable; TableOperation tableOperation = TableOperation::Supported; + bool targetMerge = false; bool operator==(const ImageResource& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 3a9286f578f..299b8384946 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -199,6 +199,11 @@ struct ShaderPixelInputInfo { bool psIteratesSamples = false; PixelPositionLocation psPositionLocation = PixelPositionLocation::Center; std::array sampleLocations{}; + std::uint8_t mergeTarget = 0; + std::uint16_t mergeMask = 0; + std::uint8_t mergeSamples = 1; + bool mergeDepthStencilWrites = false; + bool mergeDepthStencilTests = false; std::array sampleOrder{}; ShaderStageRuntime stage; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 1b15e694b4a..6aa4ea721e3 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1872,6 +1872,7 @@ std::shared_ptr ResourceMaterializer::ResolveImageTab } std::vector ResourceMaterializer::RuntimeImageModes(const ImageResource& image) { + if (image.targetMerge) return {image}; const bool table = image.table != NoTable; if (table && image.tableOperation == TableOperation::Unsupported) return {}; std::vector modes; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index d9c6fbcc3d6..1ab728cc970 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -1,4 +1,5 @@ #include "Optimization/ShaderInfoCollector.hpp" +#include "Optimization/ResourceMaterializer.hpp" #include #include #include @@ -555,6 +556,25 @@ std::uint16_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage s return static_cast(use); } +void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) { + if (info.runtimeImageModes.size() != info.images.size() || std::ranges::any_of(info.images, [](const ImageResource& image) { return image.targetMerge; })) { + return Fail("color target merge image cannot join the prepared images"); + } + ImageResource image; + image.source = std::numeric_limits::max(); + image.resourceClass = ImageResourceClass::Storage; + image.numericClass = IrTextureNumericClass::Uint; + image.dimension = pixel.mergeSamples > 1u ? RdnaImageDimension::Dim2DMsaa : RdnaImageDimension::Dim2D; + image.read = true; + image.written = true; + image.fmaskCompatible = false; + image.targetMerge = true; + info.images.push_back(image); + info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); + if (pixel.mergeSamples > 1u) AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID"); +} + void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { for (auto& sampler : info.samplers) { sampler.uses = 0; @@ -627,6 +647,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo } CollectBuiltinInputs(program, next); CollectOutputs(program, inputInfo, next); + if (program.Resources().stage == IrShaderStage::Pixel && inputInfo.pixel->mergeMask != 0u) AddTargetMerge(*inputInfo.pixel, next); if ((program.Resources().stage == IrShaderStage::Vertex || program.Resources().stage == IrShaderStage::Mesh) && inputInfo.vertex != nullptr && inputInfo.vertex->pointList) AddOutput(next, StageOutputKind::PointSize, 0, 0, "gl_PointSize"); CollectSamplerUses(program, next); program.Info() = std::move(next); diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ef28af0ff8a..79eac0d4d18 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -987,7 +987,24 @@ std::shared_ptr specializeModule(const CompiledShaderAr return entry->module; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) { +const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults, ResourceSnapshot& merged) { + const auto& images = variant.info.info.images; + const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); + if (image == images.end() || registrationDefaults) return snapshot; + if (!request.context.pixel || request.context.pixel->targetMerge.mask == 0u) throw std::runtime_error("ShaderRecompiler: a color target merge variant was materialized without the merged target"); + if (image + 1 != images.end() || snapshot.images.size() + 1u != images.size()) throw std::runtime_error("ShaderRecompiler: the captured images do not precede the color target merge image"); + merged = snapshot; + DescriptorValue descriptor; + std::ranges::copy(request.context.pixel->targetMerge.descriptor, descriptor.dwords.begin()); + descriptor.dwordCount = 8u; + descriptor.availableDwordMask = 0xffu; + merged.images.push_back(descriptor); + return merged; +} + +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false) { + ResourceSnapshot merged; + const auto& snapshot = mergeTargetImage(variant, request, captured, registrationDefaults, merged); RecompileResult result; const auto& artifact = variant.artifact; static_cast(result) = { @@ -1420,6 +1437,11 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh mix(request.context.pixel->reversedBlend); mix(static_cast(request.context.pixel->rasterPrimitive)); mix(request.context.pixel->layerExported); + const auto& merge = request.context.pixel->targetMerge; + mix(merge.target); + mix(merge.mask); + mix(merge.samples); + for (const auto word : merge.descriptor) mix(word); } if (request.context.vertex) { const auto& vertex = *request.context.vertex; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 01a51aef33f..5cb8324a959 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -164,6 +164,17 @@ inline constexpr std::uint32_t One = 3u; } +struct TargetMerge { + std::uint8_t target = 0; + std::uint16_t mask = 0; + std::uint8_t samples = 1; + std::array descriptor{}; + bool depthStencilWrites = false; + bool depthStencilTests = false; + + bool operator==(const TargetMerge&) const = default; +}; + struct ShaderPixelStageInfo { std::uint32_t interpolatorCount; std::array interpolatorSettings; @@ -204,6 +215,7 @@ struct ShaderPixelStageInfo { bool iteratesSamples = false; PixelPositionLocation positionLocation = PixelPositionLocation::Center; std::array sampleLocations{}; + TargetMerge targetMerge{}; std::array sampleOrder{}; }; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 481c5d232bd..b37a4a99dd6 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -141,7 +141,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 24, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 41, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 42, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); @@ -763,6 +763,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.emulatedCompare); out.Value(image.table); out.Value(image.tableOperation); + out.Value(image.targetMerge); }); writer.Value(!info.runtimeImageModes.empty()); writer.List(info.samplers, [](Writer& out, const SamplerResource& sampler) { @@ -869,6 +870,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.emulatedCompare); in.Value(image.table); in.Value(image.tableOperation); + in.Value(image.targetMerge); }); bool preparedImageModes = false; reader.Value(preparedImageModes); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index abd799b2059..d007fc126a7 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 45; +inline constexpr std::uint32_t FormatVersion = 46; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 34213a41b69..65ebd7e7eb4 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -70,6 +70,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); std::uint32_t ExecutionModelForStage(IrShaderStage stage); std::uint32_t ConstantI32(SpirvEmitterState& state, std::int32_t value); @@ -100,6 +101,7 @@ DppTargetLane EmitDppMirrorTargetLane(SpirvEmitterState& state, std::uint32_t su DppTargetLane EmitDppTargetLane(SpirvEmitterState& state, std::uint32_t control); std::uint32_t InputVariableForKind(const SpirvEmitterState& state, StageInputKind kind); bool OrderedPixelShader(const SpirvEmitterState& state); +void FlushColorTargetMerge(SpirvEmitterState& state); const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location); std::uint32_t EmitVertexParameterComponentU32(SpirvEmitterState& state, const SpirvInputBinding& input, std::uint32_t component); std::uint32_t EmitInputComponentU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 38b321e595f..8ff9a532297 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -230,6 +230,8 @@ struct SpirvEmitterState { std::uint32_t currentLabel = 0; const IrBlock* currentBlock = nullptr; std::uint32_t pixelValidMaskVariable = 0; + std::uint32_t mergeWordVariable = 0; + std::uint32_t mergePendingVariable = 0; std::uint32_t quadPixelUncovered = 0; std::uint32_t sampleMaskInputVariable = 0; std::uint32_t quadSampleCoverage = 0; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index b38633af2ec..5ff71524180 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -21,6 +21,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); [[noreturn]] void ExitDescriptorBindingFailure(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, const char* reason); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 0134281e44d..86a4b44112a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -90,6 +90,7 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { RecordBdaFaultWords(state, ConstantU32(state, static_cast(hash)), ConstantU32(state, static_cast(hash >> 32u)), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit); }); } + FlushColorTargetMerge(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 946baa60682..7a5fe34450c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -233,6 +233,27 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3 return DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "storage image descriptor array was not emitted"); } +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample) { + const auto& image = state.program.Info().images.at(resource); + if (image.resourceClass != ImageResourceClass::Storage || image.table != NoTable || image.mipMode == ImageMipMode::DynamicStorage || image.atomic) { + FailEmit("storage image read requested for an image that is not one plain storage descriptor"); + } + if (std::ranges::find(state.supportedCapabilities, spv::CapabilityStorageImageReadWithoutFormat) == state.supportedCapabilities.end()) FailEmit("storage image reads without a format are unavailable on the target device"); + state.module.EmitCapability(spv::CapabilityStorageImageReadWithoutFormat); + const auto imageType = ImageType(state, image); + const auto descriptor = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, imageType, descriptor, StorageImageDescriptorPointer(state, resource)); + const auto texel = state.module.AllocateId(); + const auto texelType = ImageVectorType(state, image.numericClass, 4u); + if (RdnaImageDimensionInfoFor(image.dimension).multisampled != 0u) { + if (sample == 0u) FailEmit("multisampled storage image read requires a sample"); + state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord, spv::ImageOperandsSampleMask, sample); + } else { + state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord); + } + return texel; +} + void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample) { const auto& image = state.runtimeImage != nullptr ? *state.runtimeImage : state.program.Info().images.at(resource); if (image.resourceClass != ImageResourceClass::Storage) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index a090a685414..f71d15cbca6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -552,6 +552,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto& image = *selected; const auto name = "image_" + std::to_string(static_cast(binding.kind)); state.imageVariables.at(ImageBindingIndex(binding.kind)) = Define(ArrayType(ImageType(state, image)), name.c_str(), spv::StorageClassUniformConstant); + if (std::ranges::any_of(binding.resources, [&](std::uint32_t resource) { return state.program.Info().images.at(resource).targetMerge; })) state.module.AddAnnotation(spv::OpDecorate, state.imageVariables.at(ImageBindingIndex(binding.kind)), spv::DecorationCoherent); if (image.dimension == RdnaImageDimension::Dim1D || image.dimension == RdnaImageDimension::Dim1DArray) { state.module.EmitCapability(image.resourceClass == ImageResourceClass::Sampled ? spv::CapabilitySampled1D : spv::CapabilityImage1D); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 645d3d16778..ed3215659bb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -156,7 +156,7 @@ namespace ShaderRecompiler } bool OrderedPixelShader(const SpirvEmitterState& state) { - return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psOrderedPixelShader; + return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && (state.inputInfo.pixel->psOrderedPixelShader || state.inputInfo.pixel->mergeMask != 0u); } const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index ea47f0b3aff..27b431282b7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -208,6 +208,25 @@ std::uint32_t IndexedWindowSlots(const IrProgram& program) { return static_cast(reached); } +bool WritesGuestMemory(const IrProgram& program) { + const auto& info = program.Info(); + const auto writtenImage = [](const ImageResource& image) { return !image.targetMerge && (image.written || image.atomic || image.atomic64); }; + const auto writtenModes = [&](const std::vector& modes) { return std::ranges::any_of(modes, writtenImage); }; + const auto writtenBuffer = [](const BufferResource& buffer) { return buffer.written || buffer.atomic; }; + if (info.bdaWrites || std::ranges::any_of(info.buffers, writtenBuffer) || std::ranges::any_of(info.images, writtenImage) || std::ranges::any_of(info.runtimeImageModes, writtenModes)) return true; + const auto& memory = program.Resources().memoryInfo; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + const auto access = SharedAccessOf(inst->Opcode()); + if (access == SharedAccess::None || access == SharedAccess::Read) continue; + const auto index = inst->Flags().index; + if (index >= memory.size()) FailEmit("a shared memory access has no memory metadata"); + if (memory[index].kind == ResourceKind::Gds) return true; + } + } + return false; +} + } void DefineModule(SpirvEmitterState& state) { @@ -386,12 +405,22 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitExtension("SPV_EXT_shader_stencil_export"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } - if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) { + const bool merge = pixel.mergeMask != 0u; + if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { + throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); + } + if (merge && pixel.mergeSamples > 1u && WritesGuestMemory(state.program)) { + throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, which a shader that writes guest memory (stores, atomics or GDS) cannot use: each covered sample would repeat its writes"); + } + if (merge && !pixel.psEarlyZ && pixel.mergeDepthStencilTests && WritesGuestMemory(state.program)) { + throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); + } + if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } - if (pixel.psOrderedPixelShader) { + if (pixel.psOrderedPixelShader || merge) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFragmentShaderPixelInterlockEXT)) == state.supportedCapabilities.end()) { - throw std::runtime_error("a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); + throw std::runtime_error(merge ? "a partial write of a packed color target needs the fragmentShaderPixelInterlock feature, which the device lacks" : "a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); } state.module.EmitCapability(spv::CapabilityFragmentShaderPixelInterlockEXT); state.module.EmitExtension("SPV_EXT_fragment_shader_interlock"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index ed2edb7466e..30966684452 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -460,6 +460,39 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target return result; } +struct MergeTexel { + std::uint32_t resource; + std::uint32_t coord; + std::uint32_t sample; +}; + +MergeTexel MergeTargetTexel(SpirvEmitterState& state) { + const auto& pixel = PixelInfo(state); + const auto& images = state.program.Info().images; + const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); + if (image == images.end()) FailEmit("the color target merge image was not collected"); + if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u || (pixel.mergeSamples > 1u && InputVariableForKind(state, StageInputKind::SampleId) == 0u)) FailEmit("the color target merge inputs were not declared"); + if (state.mergeWordVariable == 0u || state.mergePendingVariable == 0u) FailEmit("the color target merge variables were not declared"); + const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); }; + const auto x = position(0u); + const auto y = position(1u); + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), coord, x, y); + return {static_cast(image - images.begin()), coord, pixel.mergeSamples > 1u ? EmitBuiltinU32(state, StageInputKind::SampleId, 0u) : 0u}; +} + +void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { + const auto& pixel = PixelInfo(state); + const auto [resource, coord, sample] = MergeTargetTexel(state); + const auto stored = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord, sample), 0u); + const auto word = PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color); + const auto kept = EmitBinaryU32(state, spv::OpBitwiseAnd, stored, ConstantU32(state, ~static_cast(pixel.mergeMask))); + const auto merged = EmitOrU32(state, kept, EmitBinaryU32(state, spv::OpBitwiseAnd, word, ConstantU32(state, pixel.mergeMask))); + state.module.AddFunction(spv::OpStore, state.mergeWordVariable, merged); + state.module.AddFunction(spv::OpStore, state.mergePendingVariable, ConstantU32(state, 1u)); +} + bool AlphaToCoverage(const SpirvEmitterState& state) { return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage; } @@ -774,6 +807,26 @@ std::uint32_t ConvertPositionToClipSpace(SpirvEmitterState& state, std::uint32_t } +void FlushColorTargetMerge(SpirvEmitterState& state) { + if (state.mergePendingVariable == 0u) return; + const auto pending = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), pending, state.mergePendingVariable); + auto write = Binary(state, spv::OpINotEqual, TypeBool(state), pending, ConstantU32(state, 0u)); + if (state.pixelValidMaskVariable != 0u) { + const auto valid = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), valid, state.pixelValidMaskVariable); + write = Binary(state, spv::OpLogicalAnd, TypeBool(state), write, Binary(state, spv::OpINotEqual, TypeBool(state), valid, ConstantU32(state, 0u))); + } + EmitIfCondition(state, write, [&] { + const auto [resource, coord, sample] = MergeTargetTexel(state); + const auto merged = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), merged, state.mergeWordVariable); + const auto texel = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); + EmitStorageImageWrite(state, resource, 0u, coord, texel, sample); + }); +} + namespace { std::uint32_t MeshArray(SpirvEmitterState& state, std::uint32_t storage, std::uint32_t type, std::uint32_t count) { @@ -1528,6 +1581,10 @@ void EmitProgram(SpirvEmitterState& state) { state.pixelValidMaskVariable = state.module.AllocateId(); state.module.AddName(state.pixelValidMaskVariable, "pixel_valid_mask_active"); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->mergeMask != 0u) { + state.mergeWordVariable = state.module.AllocateId(); + state.mergePendingVariable = state.module.AllocateId(); + } for (const IrBlock* block : program.BlockOrder()) { const auto label = state.module.AllocateId(); state.labels.emplace(block, label); @@ -1576,6 +1633,10 @@ void EmitProgram(SpirvEmitterState& state) { if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.pixelValidMaskVariable, spv::StorageClassFunction); } + if (state.mergePendingVariable != 0u) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergeWordVariable, spv::StorageClassFunction); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergePendingVariable, spv::StorageClassFunction, ConstantU32(state, 0u)); + } for (std::uint32_t half = 0; half < state.laneCount; half++) { auto& lane = half == 0u ? ctx : high; if (lane.scratchU32Variable != 0u) { @@ -1672,6 +1733,7 @@ void EmitProgram(SpirvEmitterState& state) { if (state.quadSampleCoverage != 0u) StoreCoverage(state, ConstantI32(state, -1)); } if (program.BlockOrder().empty()) { + FlushColorTargetMerge(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } @@ -1959,6 +2021,10 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { value = mapped; if (exp.index == 0u && !uintOutput && AlphaToCoverage(state)) StoreAlphaCoverage(state, value); const auto mode = MrtOutputMode(state, exp); + if (PixelInfo(state).mergeMask != 0u && exp.index == PixelInfo(state).mergeTarget) { + if (uintOutput || mode == 5u || mode == 6u) FailEmit("a partial write of a packed color target cannot take an integer or 16-bit normalized export"); + MergeColorTarget(state, value); + } if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value); } if (exp.kind == ExportTargetKind::Position) { diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 4a3849fbd6c..b15814b78ed 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -367,6 +367,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO program.Metadata().pixelIteratesSamples = iterated; const bool offsets = options.interpolationFunctions && options.fragmentShaderBarycentricEnabled; const bool centers = iterated && offsets; + if (ps->mergeSamples > 1u && !offsets && ps->SampleLocationsOffCenter() && (loaded(PixelInput::PerspectiveCenter) || loaded(PixelInput::LinearCenter))) { + throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, and without interpolation functions and fragment shader barycentrics its centre-located barycentrics would follow the sample"); + } const bool sampleOffsets = offsets && ps->SampleLocationsOffCenter(); const auto smoothSample = sampleOffsets ? StageInputKind::BaryCoordSmoothSample : StageInputKind::BaryCoordSmooth; const auto linearSample = sampleOffsets ? StageInputKind::BaryCoordNoPerspectiveSample : StageInputKind::BaryCoordNoPerspective; diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index f1f7de5a925..23ec1158947 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -167,6 +167,12 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psIteratesSamples = pixel.iteratesSamples; pixelStorage.psPositionLocation = pixel.positionLocation; pixelStorage.sampleLocations = pixel.sampleLocations; + if (pixel.targetMerge.mask != 0u && (pixel.targetMerge.target >= 8u || (pixel.targetMerge.samples != 1u && pixel.targetMerge.samples != 2u && pixel.targetMerge.samples != 4u && pixel.targetMerge.samples != 8u))) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target merge"); + pixelStorage.mergeTarget = pixel.targetMerge.target; + pixelStorage.mergeMask = pixel.targetMerge.mask; + pixelStorage.mergeSamples = pixel.targetMerge.samples; + pixelStorage.mergeDepthStencilWrites = pixel.targetMerge.depthStencilWrites; + pixelStorage.mergeDepthStencilTests = pixel.targetMerge.depthStencilTests; pixelStorage.sampleOrder = pixel.sampleOrder; ShaderStageInputInfo result; result.pixel = &pixelStorage; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index bc8a0a0afba..8229447e9ea 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -110,9 +110,29 @@ void InsertMeshBarycentricFlag(std::vector& bytes) { bytes.insert(at + static_cast(draw.size()), 0u); } -void InsertPixelDefaultsAfterFrontFace(std::vector& bytes, bool sampleOrder = true) { +// Pixel info bytes that follow the stencil operation value flag: the sample order (version 42), +// then the default color target merge (version 46, 40 bytes with samples = 1 at offset 5). +std::vector PixelTailDefaults(bool sampleOrder, bool targetMerge) { + std::vector tail(sampleOrder ? 16u : 0u, 0u); + if (targetMerge) { + tail.resize(tail.size() + 40u, 0u); + tail[tail.size() - 35u] = 1u; + } + return tail; +} + +void EraseDefaultTargetMerge(std::vector& bytes, std::ptrdiff_t offset) { + const auto merge = PixelTailDefaults(false, true); + const auto at = bytes.begin() + offset; + Require(bytes.end() - at > static_cast(merge.size()) && std::equal(merge.begin(), merge.end(), at)); + bytes.erase(at, at + static_cast(merge.size())); +} + +void InsertPixelDefaultsAfterFrontFace(std::vector& bytes, bool sampleOrder = true, bool targetMerge = true) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); - bytes.insert(bytes.begin() + 282u, sampleOrder ? 51u : 35u, 0u); + bytes.insert(bytes.begin() + 282u, 35u, 0u); + const auto tail = PixelTailDefaults(sampleOrder, targetMerge); + bytes.insert(bytes.begin() + 317u, tail.begin(), tail.end()); } void AppendVertexTail(std::vector& bytes) { @@ -278,7 +298,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 45u; + upgraded[4] = 46u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); @@ -321,7 +341,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 45u; + layered[4] = 46u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); @@ -350,7 +370,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 45u; + bytes[4] = 46u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); @@ -481,10 +501,13 @@ int main() { main38[4] = 38u; InsertMeshBarycentricFlag(main38); if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); - if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false); + if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false, false); auto upgraded = main38; - upgraded[4] = 45u; - if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); + upgraded[4] = 46u; + if (fixture.pixel) { + const auto tail = PixelTailDefaults(true, true); + upgraded.insert(upgraded.begin() + 317u, tail.begin(), tail.end()); + } if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); AppendVertexTail(upgraded); @@ -506,8 +529,13 @@ int main() { pointed.context.vertex->pointSize = 3.0f; pointed.context.vertex->pointSizeMin = 1.0f; pointed.context.vertex->pointSizeMax = 8.0f; - auto version45 = DecodeBase64(serializer.Serialize(pointed)); - Require(version45.size() > 15u && version45[4] == 45u); + auto version46 = DecodeBase64(serializer.Serialize(pointed)); + Require(version46.size() > 15u && version46[4] == 46u); + const auto replay46 = serializer.Deserialize(EncodeBase64(version46)); + Require(replay46.request.graphics->tessellation->mergedHull == merged && replay46.request.context.vertex->pointList && replay46.request.context.vertex->pointSize == 3.0f && replay46.request.context.vertex->pointSizeMin == 1.0f && replay46.request.context.vertex->pointSizeMax == 8.0f); + auto version45 = version46; + if (fixture.pixel) EraseDefaultTargetMerge(version45, 333); + version45[4] = 45u; auto version44 = version45; version44.resize(version44.size() - 4u); version44.pop_back(); @@ -516,6 +544,7 @@ int main() { version43.pop_back(); version43[4] = 43u; const auto replay43 = serializer.Deserialize(EncodeBase64(version43)); + Require(serializer.Serialize(replay43.request) == EncodeBase64(version46)); Require(replay43.request.graphics->tessellation->mergedHull == merged && replay43.request.context.vertex->pointList && replay43.request.context.vertex->pointSize == 3.0f && replay43.request.context.vertex->pointSizeMin == 1.0f && replay43.request.context.vertex->pointSizeMax == 8.0f); auto version42 = version43; version42.resize(version42.size() - 13u); @@ -542,7 +571,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 45u; + main25[4] = 46u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -570,7 +599,7 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve && replay.request.target.pixelFunctionStorageDwords == request.target.pixelFunctionStorageDwords); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 45u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 46u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; version40.resize(version40.size() - sizeof(std::uint32_t)); version40.pop_back(); @@ -622,13 +651,21 @@ int main() { request.context.pixel = pixel; const auto orderEncoded = serializer.Serialize(request); Require(serializer.Deserialize(orderEncoded).request.context.pixel->sampleOrder == pixel.sampleOrder); - auto version41 = DecodeBase64(orderEncoded); - const auto order = std::search(version41.begin(), version41.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) + pixel.sampleLocations.size() + 1; - Require(version41.end() - order > static_cast(sizeof(pixel.sampleOrder)) && *order == 0x10u); - version41.erase(order, order + sizeof(pixel.sampleOrder)); - version41.resize(version41.size() - 4u); - version41.pop_back(); - version41.pop_back(); + auto version42 = DecodeBase64(orderEncoded); + const auto orderOffset = std::search(version42.begin(), version42.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) - version42.begin() + static_cast(pixel.sampleLocations.size() + 1u); + Require(version42.end() - version42.begin() > orderOffset + static_cast(sizeof(pixel.sampleOrder)) && version42[static_cast(orderOffset)] == 0x10u); + EraseDefaultTargetMerge(version42, orderOffset + static_cast(sizeof(pixel.sampleOrder))); + version42.resize(version42.size() - 4u); + version42.pop_back(); + version42.pop_back(); + version42[4] = 42u; + const auto replay42 = serializer.Deserialize(EncodeBase64(version42)); + Require(replay42.request.context.pixel.has_value() && replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && replay42.request.context.pixel->targetMerge == ShaderRecompiler::TargetMerge{}); + auto upgraded42 = replay42.request; + upgraded42.target = request.target; + Require(serializer.Serialize(upgraded42) == orderEncoded); + auto version41 = version42; + version41.erase(version41.begin() + orderOffset, version41.begin() + orderOffset + static_cast(sizeof(pixel.sampleOrder))); version41[4] = 41u; const auto replay41 = serializer.Deserialize(EncodeBase64(version41)); Require(replay41.request.context.pixel.has_value() && replay41.request.context.pixel->sampleLocations == pixel.sampleLocations && replay41.request.context.pixel->sampleOrder == std::array{}); @@ -645,6 +682,7 @@ int main() { const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto pixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); Require(version35.end() - stencilOperationValue > pixelTail && std::all_of(stencilOperationValue, stencilOperationValue + pixelTail, [](std::uint8_t value) { return value == 0u; })); + EraseDefaultTargetMerge(version35, stencilOperationValue - version35.begin() + pixelTail); version35.erase(stencilOperationValue, stencilOperationValue + pixelTail); version35[4] = 35u; version35.pop_back(); @@ -698,7 +736,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 45u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 20u : version >= 35u ? 21u : 26u)); + Require(upgradedLegacy[4] == 46u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 20u : version >= 35u ? 21u : 26u)); } } @@ -706,11 +744,21 @@ int main() { pointRequest.context.pixel = pixel; const auto sampledPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); Require(sampledPoint.request.context.vertex->pointList && sampledPoint.request.context.pixel->sampleOrder == pixel.sampleOrder); - auto legacy42 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy42.resize(legacy42.size() - 19u); + auto legacy43 = DecodeBase64(serializer.Serialize(pointRequest)); + const auto mergeOffset = std::search(legacy43.begin(), legacy43.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) - legacy43.begin() + static_cast(pixel.sampleLocations.size() + 1u + sizeof(pixel.sampleOrder)); + EraseDefaultTargetMerge(legacy43, mergeOffset); + legacy43.resize(legacy43.size() - 4u); + legacy43.pop_back(); + legacy43.pop_back(); + legacy43[4] = 43u; + const auto pointReplay43 = serializer.Deserialize(EncodeBase64(legacy43)); + Require(pointReplay43.request.context.pixel->sampleOrder == pixel.sampleOrder && pointReplay43.request.context.vertex->pointList && pointReplay43.request.context.vertex->pointSize == 4.0f); + Require(serializer.Serialize(pointReplay43.request) == serializer.Serialize(pointRequest)); + auto legacy42 = legacy43; + legacy42.resize(legacy42.size() - 13u); legacy42[4] = 42u; - const auto replay42 = serializer.Deserialize(EncodeBase64(legacy42)); - Require(replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f); + const auto pointReplay42 = serializer.Deserialize(EncodeBase64(legacy42)); + Require(pointReplay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !pointReplay42.request.context.vertex->pointList && pointReplay42.request.context.vertex->pointSize == 1.0f); pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); @@ -718,6 +766,7 @@ int main() { const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto legacyPixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); + EraseDefaultTargetMerge(legacy34, stencil34 - legacy34.begin() + legacyPixelTail); legacy34.erase(stencil34, stencil34 + legacyPixelTail); legacy34[4] = 34u; const auto replay34 = serializer.Deserialize(EncodeBase64(legacy34)); @@ -729,6 +778,8 @@ int main() { request.target.largePixelFunctionLdsBuffer = false; auto bytes = DecodeBase64(serializer.Serialize(request)); Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 6u] == (fused ? 1u : 0u) && bytes[bytes.size() - 5u] == 0u); + const auto fusedMerge = std::search(bytes.begin(), bytes.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) - bytes.begin() + static_cast(pixel.sampleLocations.size() + 1u + sizeof(pixel.sampleOrder)); + EraseDefaultTargetMerge(bytes, fusedMerge); bytes.resize(bytes.size() - 4u); bytes.pop_back(); bytes[4] = 44u; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3d7bbfd4746..05d99dec9e6 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -189,7 +189,10 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; off-centre samples make centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. +- [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. +- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample merges and the 2-, 4- and 8-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp. On a PS5 (firmware 13.40, measured 2026-10-19 on the GPU through `sceAgcDriverSubmitDcb` from inside a retail title) `SRC_SEL` 9 and the `RELEASE_MEM` DATA_SEL 3 timestamp were the same counter (0x1C and 0x5C ticks apart within one buffer), and it ran at 98.0 and 99.8 MHz over two 1.1 s intervals timed by the CPU, i.e. 100 MHz within the submission jitter, as the 10 ns ticks here; only its starting value is the host's. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index cd42d5cc9fe..69b99135e05 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -27,6 +27,7 @@ - [GCN instructions](../../../core/shader/recompiler/RdnaDecoder/src/GcnInstructionDecoder.cpp) (shader, PS4) - PS4 (Sea Islands) instructions are re-encoded to their RDNA encodings with opcode tables built from shadPS4's `opcodes.h` and decoded by the RDNA decoder; an encoding without an RDNA counterpart throws. MIMG has no DIM field on GCN: the dimension is taken as 2D, or 2D array when DA is set, and the [materializer](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) rejects a T# whose type needs another address count. MIMG word 1 bits [31:26] are reserved on GCN (A16 and D16 on RDNA). - [GCN buffer and image descriptors](../../../core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp) (shader, PS4) - a GCN V# word 3 (DATA_FORMAT [18:15], NUM_FORMAT [14:12], names read from the most significant field) is rewritten to the unified format of the PS5 layout with OOB_SELECT 1 for a strided buffer and 3 otherwise, following the GFX6-7 NUM_RECORDS rules in Mesa's `ac_descriptors`; ATC, HASH, HEAP and MTYPE are dropped. The V# base is 44 bits, as in shadPS4's `AmdGpu::Buffer`: Limbo leaves 0x7 in word 1 bits [15:12]. A GCN T# is rewritten to the PS5 layout with SW_MODE linear, keeping TILING_INDEX [4:0], POW2_PAD [5] and PITCH - 1 [19:6] in word 7. None of it is checked on a console. - [PS4 legacy vertex shaders](../../../core/shader/recompiler/Recompiler.cpp) (shader, PS4) - a PS4 vertex program at the VS slot takes its user data from s0, its vertex index in v0 and its instance index in v1-v3, and a fetch shader it calls through the user pair (`s_swappc_b64 s[0:1], s[0:1]`) returns through the same pair. These rules apply only while GCN shaders are decoded. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, kills pixels under late tests in a draw that writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and a non-keep operation), writes guest memory (stores, atomics, GDS writes) under late tests in a draw with a depth, stencil or depth-bounds test, as the forced early tests would drop the writes of rejected pixels, or kills pixels under late tests after the game has read an occlusion counter, as the early tests would count them. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so the depth write cannot move after the shader there. ### Functional