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 f4a26ead03..5b1840964e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -126,6 +126,8 @@ 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.alphaToCoverage = product->state.alphaToCoverage; + product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state); + product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state); if (product->pixel.ancillary) product->pixel.rasterPrimitive = Graphics::RasterPrimitiveOf(product->state); if (product->pixel.ancillary) product->pixel.layerExported = product->state.layerExports != 0; product->pixel.ditheredAlphaToCoverage = product->state.ditheredAlphaToCoverage; 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 65e73ed84a..eb1ca2f599 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -73,6 +73,8 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); + mix(pixel.quadPixelMask); + for (const auto mask : pixel.quadSampleMask) mix(mask); std::lock_guard cacheLock(drawCacheMutex); ++drawEntryCounters.lookups; const auto found = drawCache.find(drawKey); 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 8782ecce15..0ba464a4d8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -144,7 +144,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.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask) return false; + if (s.samples != t.samples || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 854f99cb3b..9a0d342051 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -283,6 +283,7 @@ struct VulkanDevice::State { bool float64DenormPreserve = false; bool float16DenormPreserve = false; bool demoteToHelperInvocation = false; + bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -1153,6 +1154,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : } demoteFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DEMOTE_TO_HELPER_INVOCATION_FEATURES_EXT}; demoteFeatures.shaderDemoteToHelperInvocation = VK_TRUE; + if (hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) { + VkPhysicalDeviceMaintenance5PropertiesKHR maintenance5{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_PROPERTIES_KHR}; + VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &maintenance5}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); + state->earlyFragmentSampleMaskTestBeforeSampleCounting = maintenance5.earlyFragmentSampleMaskTestBeforeSampleCounting == VK_TRUE; + } std::vector deviceExtensions; if (window != nullptr) deviceExtensions.assign(presentationExtensions.begin(), presentationExtensions.end()); if (window != nullptr && hasExtension(VK_EXT_HDR_METADATA_EXTENSION_NAME)) { @@ -3449,6 +3456,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.placeholderImages = state->placeholderImages.get(); context.viewportIndexLayer = state->viewportIndexLayer; context.demoteToHelperInvocation = state->demoteToHelperInvocation; + context.earlyFragmentSampleMaskTestBeforeSampleCounting = state->earlyFragmentSampleMaskTestBeforeSampleCounting; context.primitiveListRestart = state->primitiveListRestart; context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 26b1a62234..8f1e1dd6f4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -215,6 +215,7 @@ struct Context { const PlaceholderImages* placeholderImages = nullptr; bool viewportIndexLayer = false; bool demoteToHelperInvocation = false; + bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; bool primitiveListRestart = false; bool imageViewMinLod = false; bool pipelineExecutableInfo = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 657e3cf5ef..a195283b06 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -123,6 +123,8 @@ bool ValidateSampleLocations(const Context& context, const State& state); void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment); bool ShadersWriteMemory(std::span shaders); +bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program); +void ValidateSampleMask(const Context& context, const State& state, std::span shaders); // Returns the color attachment locations the pixel shader writes. std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp index 6ae3f8ab60..b53834e9d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp @@ -11,6 +11,7 @@ namespace AgcDriver::Graphics { ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array& exportMappings, bool nullProgram = false); +bool PixelEarlyZ(std::uint32_t shaderControl); ShaderRecompiler::ShaderPixelStageInfo DefaultUnexportedInputs(ShaderRecompiler::ShaderPixelStageInfo pixel, std::span parameterExports); ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span header); // A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read: diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 997fa78941..1ad4b4f826 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -129,6 +129,8 @@ struct State { bool customSampleLocations = false; bool sampleLocationGrid = false; std::uint32_t paClVsOutCntl = 0; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + bool pixelEarlyZ = false; }; // PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two @@ -142,6 +144,11 @@ VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); +bool PartialSampleMask(const State& state); +bool UniformSampleMask(const State& state); +VkSampleMask HostSampleMask(const State& state); +std::uint8_t QuadPixelMask(const State& state); +std::array QuadSampleMasks(const State& state); ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot, bool allowMultisampleDcc = false); std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height); std::uint32_t ColorWriteMask(const Registers& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 266027e5fd..a4a0189916 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1059,6 +1059,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P // Viewport and scissor are dynamic pipeline state, so their limits are checked here per draw. ValidateViewport(context, state.viewport); ValidateDepthBounds(context, state); + ValidateSampleMask(context, state, shaders); timer.phase(PhaseValidate); inputs.maxIndex = draw.indexed ? 0u : draw.firstVertex + draw.indexCount - 1u; if (draw.indexed) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 7ef9781948..1ceb718b79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -158,6 +158,20 @@ bool ShadersWriteMemory(std::span shaders) { return false; } +void ValidateSampleMask(const Context& context, const State& state, std::span shaders) { + if (!PartialSampleMask(state)) return; + for (const auto& shader : shaders) { + if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue; + Require(shader.program != nullptr, "missing compiled fragment shader"); + if (UniformSampleMask(state)) { + Require(context.earlyFragmentSampleMaskTestBeforeSampleCounting || !DeclaresEarlyFragmentTests(*shader.program), "a sample mask with early fragment tests needs VkPhysicalDeviceMaintenance5Properties::earlyFragmentSampleMaskTestBeforeSampleCounting, which the device lacks"); + continue; + } + const bool tested = state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest); + Require(!state.pixelEarlyZ || !tested || !ShadersWriteMemory(std::span(&shader, 1)), "a sample mask that differs between the pixels of a quad, from a pixel program with early depth or stencil tests that writes memory, is unsupported: the masked pixels are removed in the shader, after the tests"); + } +} + namespace { thread_local bool prebuilding = false; @@ -242,6 +256,8 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); + appendKey(keys.fragmentShader, HostSampleMask(state)); + appendKey(keys.fragmentOutput, HostSampleMask(state)); if (!resources.split) { appendKey(keys.layout, pushStagesOf(context)); for (const auto word : resources.mainKey) appendKey(keys.layout, word); @@ -460,6 +476,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput depthStencil.stencilTestEnable = state.stencilTest; depthStencil.front = state.stencilFront; depthStencil.back = state.stencilBack; + const VkSampleMask sampleMask = HostSampleMask(state); + if (PartialSampleMask(state) && UniformSampleMask(state)) samples.pSampleMask = &sampleMask; VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; const bool alternateAlpha = SplitsAlternateAlpha(state); std::vector firstChannels(state.blends.begin(), state.blends.end()); @@ -866,6 +884,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat append(key, tessellation.primitiveGroupSize); append(key, tessellation.breakAtInstance); } + append(key, HostSampleMask(state)); } std::uint64_t hashKey(const std::vector& key) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 7c51b6a75a..814b5179fd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -144,11 +144,6 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const bool depthExportEnable = (shaderControl & 0x1u) != 0; const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; - const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0; - const auto zOrder = (shaderControl >> 4u) & 0x3u; - const auto aaMask = read(context, 0x30e, RegisterBank::Context); - const auto aaMaskBottom = read(context, 0x30f, RegisterBank::Context); - const auto quadPixelMask = static_cast((aaMask & 1u) | (((aaMask >> 16u) & 1u) << 1u) | ((aaMaskBottom & 1u) << 2u) | (((aaMaskBottom >> 16u) & 1u) << 3u)); const auto loaded = [&](PixelInput input) { return (activeInputs & PixelInputBit(input)) != 0; }; return ShaderRecompiler::ShaderPixelStageInfo{ .interpolatorCount = inputNum, @@ -170,17 +165,26 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .pixelKillEnable = pixelKillEnable, .depthExportEnable = depthExportEnable, .sampleMaskExportEnable = sampleMaskExportEnable, - .earlyZ = depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable), + .earlyZ = PixelEarlyZ(shaderControl), .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, .conservativeZExport = static_cast(conservativeZExport), .orderedPixelShader = ((shaderControl >> 16u) & 0x1u) != 0, .targetOutputMode = targetOutputMode, .targetExportMapping = nullProgram ? std::array{} : exportMappings, - .stencilRefExportEnable = stencilRefExportEnable, - .quadPixelMask = quadPixelMask + .stencilRefExportEnable = stencilRefExportEnable }; } +bool PixelEarlyZ(std::uint32_t shaderControl) { + const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0; + const bool depthExportEnable = (shaderControl & 0x1u) != 0; + const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0; + const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0; + const bool depthBeforeShader = ((shaderControl >> 12u) & 0x1u) != 0; + const auto zOrder = (shaderControl >> 4u) & 0x3u; + return depthBeforeShader || ((zOrder & 1u) != 0u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable && !stencilRefExportEnable); +} + ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span header, std::uint64_t headerAddress, std::span userData, std::vector* reads, bool staticAbi, std::uint32_t hiddenUserWords) { if (header.empty()) return {}; if (header.size() < sizeof(Shader)) throw std::runtime_error("AGC graphics: shader header is smaller than the fixed AGC header"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index da1ff261de..31f279d3b7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -146,7 +146,7 @@ struct Module { Require(value == spv::BuiltInPosition && signature == "f32x4" && !position, "unsupported or duplicate vertex built-in output"); position = true; } else { - Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); + Require((storage == spv::StorageClassInput && ((value == spv::BuiltInFragCoord && signature == "f32x4") || ((value == spv::BuiltInFrontFacing || value == spv::BuiltInHelperInvocation) && signature == "bool") || ((value == spv::BuiltInSampleId || value == spv::BuiltInLayer) && signature == "i32") || (value == spv::BuiltInSampleMask && signature == "i32[1]"))) || (storage == spv::StorageClassOutput && ((value == spv::BuiltInFragDepth && signature == "f32") || (value == spv::BuiltInFragStencilRefEXT && signature == "i32") || (value == spv::BuiltInSampleMask && (signature == "i32[1]" || signature == "u32[1]")))), "unsupported fragment built-in"); } } }; @@ -601,6 +601,20 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } +bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program) { + const auto& words = program.spirv.Words(); + Require(words.size() >= 5 && words[0] == spv::MagicNumber, "the fragment shader is not a SPIR-V module"); + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + Require(count != 0 && count <= words.size() - cursor, "truncated SPIR-V instruction"); + const auto op = static_cast(words[cursor] & 0xffffu); + if (op == spv::OpFunction) return false; + if (op == spv::OpExecutionMode && count >= 3 && words[cursor + 2] == spv::ExecutionModeEarlyFragmentTests) return true; + cursor += count; + } + return false; +} + std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation) { using Stage = ShaderRecompiler::ShaderStage; const bool tessellation = state.stages.path == ShaderPath::Tessellation; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index f673966763..6ba435e2d8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1,5 +1,6 @@ #include #include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureSwizzleEquations.hpp" @@ -410,6 +411,40 @@ bool coveredCentroidSupported(const QueueState& queue) { return ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) == 0; } +std::array sampleMaskFields(std::uint32_t top, std::uint32_t bottom) { + return {top & 0xffffu, top >> 16u, bottom & 0xffffu, bottom >> 16u}; +} + +std::string sampleMaskRejection(const QueueState& queue, std::uint32_t config, std::uint32_t top, std::uint32_t bottom) { + const auto log2 = config & 7u; + if (log2 > 3u) return {}; + const auto full = (1u << (1u << log2)) - 1u; + auto masks = sampleMaskFields(top, bottom); + for (auto& mask : masks) mask &= full; + if (std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == full; })) return {}; + const bool uniform = std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == masks[0]; }); + const bool wholePixels = std::ranges::all_of(masks, [&](std::uint32_t mask) { return mask == 0u || mask == full; }); + char detail[96]; + std::snprintf(detail, sizeof(detail), " (PA_SC_AA_MASK_X0Y0_X1Y0=0x%08x, PA_SC_AA_MASK_X0Y1_X1Y1=0x%08x)", top, bottom); + const auto conservative = find(queue.context, 0x313); + if (conservative != queue.context.end() && (conservative->second & ~0x001f6000u) != 0) return std::string("AGC graphics: sample masks with conservative rasterization are unsupported") + detail; + if (!wholePixels && uniform && !PixelProgramSkipped(queue)) { + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + if (ena != queue.context.end() && addr != queue.context.end() && (ena->second & addr->second & CentroidInputs) != 0) return std::string("AGC graphics: sample masks that remove some samples of a pixel with centroid interpolation are unsupported") + detail; + } + return {}; +} + +std::array decodeQuadSampleMask(const Registers& cx, std::uint32_t samples) { + const bool singleSample = (read(cx, 0x2f8) & 7u) == 0u; + const auto full = (1u << samples) - 1u; + const auto fields = sampleMaskFields(read(cx, 0x30e), read(cx, 0x30f)); + std::array masks{}; + for (std::size_t pixel = 0; pixel < masks.size(); ++pixel) masks[pixel] = static_cast(singleSample ? ((fields[pixel] & 1u) != 0u ? full : 0u) : fields[pixel] & full); + return masks; +} + std::uint32_t decodeSamples(const Registers& cx) { const auto config = read(cx, 0x2f8); zero(cx, 0x2f8, ~(0x7u | 0x1e000u | 0x700000u | CoveredCentroidIsCenter), "coverage conversion or nonstandard multisampling"); @@ -816,7 +851,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(read(cx, 0x83) == 0xffffu, "clip rectangles are unsupported"); Require((read(cx, 0x8c) & 0xfu) == 0xau, "nonstandard triangle edge rules are unsupported"); Require(read(cx, 0x2f9) == 0x2du, "nonstandard pixel center or vertex quantization is unsupported"); - Require((read(cx, 0x2f8) & 7u) == 0 || (read(cx, 0x30e) == 0xffffffffu && read(cx, 0x30f) == 0xffffffffu), "sample masks of multisampled draws are unsupported"); + if (auto reason = sampleMaskRejection(queue, read(cx, 0x2f8), read(cx, 0x30e), read(cx, 0x30f)); !reason.empty()) throw std::runtime_error(reason); const auto viewportControl = read(cx, 0x206); if (viewportControl != 0x43fu) throw std::runtime_error(vteMessage(viewportControl)); zero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); @@ -903,6 +938,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height); Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes"); } + result.quadSampleMask = decodeQuadSampleMask(cx, result.samples); + result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203)); if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) { static bool reported = false; if (!reported) { @@ -1037,6 +1074,43 @@ std::array ExportPackings(const State& return packings; } +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; }); +} + +bool UniformSampleMask(const State& state) { + const auto full = (1u << state.samples) - 1u; + return std::ranges::all_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) == (state.quadSampleMask[0] & full); }); +} + +VkSampleMask HostSampleMask(const State& state) { + const auto full = (1u << state.samples) - 1u; + return UniformSampleMask(state) ? state.quadSampleMask[0] & full : full; +} + +std::uint8_t QuadPixelMask(const State& state) { + if (!PartialSampleMask(state) || UniformSampleMask(state)) return 0xfu; + const auto full = (1u << state.samples) - 1u; + std::uint8_t pixels = 0; + for (std::uint32_t pixel = 0; pixel < state.quadSampleMask.size(); ++pixel) { + if ((state.quadSampleMask[pixel] & full) != 0u) pixels = static_cast(pixels | (1u << pixel)); + } + return pixels; +} + +std::array QuadSampleMasks(const State& state) { + std::array masks{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + if (!PartialSampleMask(state) || UniformSampleMask(state)) return masks; + const auto full = (1u << state.samples) - 1u; + if (std::ranges::all_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) == 0u || (mask & full) == full; })) return masks; + for (std::size_t pixel = 0; pixel < masks.size(); ++pixel) { + const auto mask = state.quadSampleMask[pixel] & full; + masks[pixel] = static_cast(mask == full ? 0xffffu : mask); + } + return masks; +} + ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state) { if (state.rectList) return ShaderRecompiler::RasterPrimitive::Rectangle; if (state.stages.mesh.has_value() || state.stages.tessellation.has_value()) return ShaderRecompiler::RasterPrimitive::Triangle; @@ -1390,8 +1464,10 @@ std::string DrawRejection(const QueueState& queue, bool indexed) { if (value(cx, 0x8c, word) && (word & 0xfu) != 0xau) return require(false, "nonstandard triangle edge rules are unsupported"); if (value(cx, 0x2f9, word) && word != 0x2du) return require(false, "nonstandard pixel center or vertex quantization is unsupported"); if (value(cx, 0x313, word) && (word & ~0x001f6000u) != 0 && word != 0x6001u) return conservativeMessage(word); - std::uint32_t other = 0; - if (value(cx, 0x2f8, word) && (word & 7u) != 0 && value(cx, 0x30e, word) && value(cx, 0x30f, other) && (word != 0xffffffffu || other != 0xffffffffu)) return require(false, "sample masks of multisampled draws are unsupported"); + std::uint32_t config = 0, other = 0; + if (value(cx, 0x2f8, config) && value(cx, 0x30e, word) && value(cx, 0x30f, other)) { + if (auto reason = sampleMaskRejection(queue, config, word, other); !reason.empty()) return reason; + } if (value(cx, 0x206, word) && word != 0x43fu) return vteMessage(word); if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason; std::uint32_t targetMask = 0, shaderMask = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 82c9b731e9..ef6223d78a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2781,18 +2781,149 @@ void framebufferLayerTests() { } void quadPixelMaskTests() { - auto queue = makeState(); - queue.context[0x30e] = 0xffff0000u; - queue.context[0x30f] = 0u; - queue.context[0x1b3] = 2; - queue.context[0x1b4] = 2; - static_cast(AgcDriver::Graphics::DecodeState(queue)); - Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "a single-sample quad pixel mask was refused by the precheck"); - Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0x2u, "PA_SC_AA_MASK did not decode to the covered quad pixel"); - queue = makeState(); - queue.context[0x1b3] = 2; - queue.context[0x1b4] = 2; - Require(AgcDriver::Graphics::DecodePixelStageInfo(queue.context, {}).quadPixelMask == 0xfu, "full sample masks did not cover the whole quad"); + using AgcDriver::Graphics::DecodeState; + using AgcDriver::Graphics::DrawRejection; + using AgcDriver::Graphics::HostSampleMask; + using AgcDriver::Graphics::PartialSampleMask; + using AgcDriver::Graphics::QuadPixelMask; + using AgcDriver::Graphics::UniformSampleMask; + using Masks = std::array; + const auto masked = [](std::uint32_t top, std::uint32_t bottom, std::uint32_t log2 = 0u) { + auto queue = makeState(); + queue.context[0x30e] = top; + queue.context[0x30f] = bottom; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + if (log2 != 0u) { + queue.context[0x2f8] = (log2 << 20u) | log2; + queue.context[0x292] = 3; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x31d] = (log2 << 12u) | (log2 << 15u); + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) queue.context[0x2fe + pixel * 4u] = log2 == 1u ? 0xcc44u : 0x622ae6aeu; + } + return queue; + }; + const auto accepted = [](const AgcDriver::QueueState& queue, const std::string& what) { + std::vector reads; + AgcDriver::Graphics::RegisterReadLog() = &reads; + const auto state = DecodeState(queue); + const auto rejection = DrawRejection(queue, false); + AgcDriver::Graphics::RegisterReadLog() = nullptr; + Require(rejection.empty(), what + " was refused by the precheck: " + rejection); + for (const auto read : reads) Require(AgcDriver::Graphics::DrawKeyCovers(read), "DrawKeyRegisters lacks a register decoded for " + what); + return state; + }; + const auto refused = [](const AgcDriver::QueueState& queue, std::string_view reason) { + expectFailure([&] { DecodeState(queue); }, reason); + Require(DrawRejection(queue, false).find(reason) != std::string::npos, "the precheck accepted a draw DecodeState refuses for " + std::string(reason)); + }; + auto state = accepted(masked(0x0000ffffu, 0u), "the X0Y0 single-sample mask"); + Require(state.quadSampleMask == Masks{1, 0, 0, 0} && PartialSampleMask(state) && !UniformSampleMask(state) && QuadPixelMask(state) == 0x1u, "PA_SC_AA_MASK 0x0000ffff/0 did not cover only the even-x, even-y pixel of each quad"); + state = accepted(masked(0xffff0000u, 0u), "the X1Y0 single-sample mask"); + Require(QuadPixelMask(state) == 0x2u, "PA_SC_AA_MASK 0xffff0000/0 did not cover only the odd-x, even-y pixel of each quad"); + state = accepted(masked(0xfffe0001u, 0x0001fffeu), "a single-sample mask with bits above sample 0"); + Require(state.quadSampleMask == Masks{1, 0, 0, 1} && QuadPixelMask(state) == 0x9u, "a single-sample mask did not keep only bit 0 of each pixel's field"); + state = accepted(masked(0x00010001u, 0x00010001u), "a single-sample mask with only sample 0 set"); + Require(!PartialSampleMask(state) && QuadPixelMask(state) == 0xfu && HostSampleMask(state) == 1u, "PA_SC_AA_MASK 0x00010001 at one sample is not a full mask"); + state = accepted(masked(0u, 0u), "an empty single-sample mask"); + Require(PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0u && QuadPixelMask(state) == 0xfu, "an empty mask equal for the whole quad did not become a host sample mask of 0"); + Require(AgcDriver::Graphics::DecodePixelStageInfo(masked(0x0000ffffu, 0u).context, {}).quadPixelMask == 0xfu, "the pixel stage decode read the sample mask registers instead of the decoded state"); + auto queue = masked(0x0000ffffu, 0u); + queue.context[0x203] = 0x10u; + Require(accepted(queue, "an early-Z masked draw").pixelEarlyZ, "DB_SHADER_CONTROL Z_ORDER EARLY_Z_THEN_LATE_Z did not decode as early Z"); + queue.context[0x203] = 0x50u; + Require(!accepted(queue, "a killing masked draw").pixelEarlyZ, "a pixel program with kill decoded as early Z"); + queue.context[0x313] = 0x6001u; + refused(queue, "sample masks with conservative rasterization"); + queue = masked(0xffffffffu, 0xffffffffu); + queue.context[0x313] = 0x6001u; + Require(!PartialSampleMask(accepted(queue, "a full mask with conservative rasterization")), "a full mask with conservative rasterization decoded as partial"); + state = accepted(masked(0x00050005u, 0x00050005u, 2u), "a 4x mask equal for the whole quad"); + Require(state.samples == 4 && PartialSampleMask(state) && UniformSampleMask(state) && HostSampleMask(state) == 0x5u && QuadPixelMask(state) == 0xfu, "a 4x mask 0x5 equal for the whole quad did not become the host sample mask 0x5"); + state = accepted(masked(0xfff5fff5u, 0xfff5fff5u, 2u), "a 4x mask with bits above sample 3"); + Require(HostSampleMask(state) == 0x5u && state.quadSampleMask == Masks{5, 5, 5, 5}, "a 4x mask kept bits of samples the rasterizer does not have"); + state = accepted(masked(0x0000000fu, 0u, 2u), "a 4x mask that covers whole pixels"); + Require(!UniformSampleMask(state) && QuadPixelMask(state) == 0x1u && state.quadSampleMask == Masks{0xf, 0, 0, 0}, "a 4x mask covering all samples of one quad pixel did not become a quad pixel mask"); + state = accepted(masked(0x00000003u, 0u, 1u), "a 2x mask that covers whole pixels"); + Require(QuadPixelMask(state) == 0x1u, "a 2x mask covering both samples of one quad pixel did not become a quad pixel mask"); + using AgcDriver::Graphics::QuadSampleMasks; + constexpr Masks NoSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; + state = accepted(masked(0x00020001u, 0x00080004u, 2u), "a 4x mask with one sample per quad pixel"); + Require(QuadPixelMask(state) == 0xfu && QuadSampleMasks(state) == Masks{1, 2, 4, 8}, "a 4x mask of sample q for quad pixel q did not become per-pixel sample masks"); + state = accepted(masked(0x0000000fu, 0x00030000u, 2u), "a 4x mask mixing full, empty and partial pixels"); + Require(QuadPixelMask(state) == 0x9u && QuadSampleMasks(state) == Masks{0xffff, 0, 0, 3}, "a 4x mask mixing full, empty and partial pixels did not keep full pixels as all samples"); + Require(QuadSampleMasks(accepted(masked(0x0000000fu, 0u, 2u), "a whole-pixel 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x00050005u, 0x00050005u, 2u), "a uniform 4x mask")) == NoSampleMask && QuadSampleMasks(accepted(masked(0x0000ffffu, 0u), "a single-sample mask")) == NoSampleMask, "a mask without partial pixels that differ produced per-pixel sample masks"); + for (const auto centroid : {4u, 64u}) { + queue = masked(0x00020001u, 0x00080004u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + Require(QuadSampleMasks(accepted(queue, "centroid interpolation under per-pixel sample masks")) == Masks{1, 2, 4, 8}, "centroid interpolation refused per-pixel sample masks"); + } + for (const auto centroid : {4u, 64u}) { + queue = masked(0x00050005u, 0x00050005u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + refused(queue, "sample masks that remove some samples of a pixel with centroid interpolation"); + queue.context[0x1b4] = 2u; + Require(HostSampleMask(accepted(queue, "an unaddressed centroid input")) == 0x5u, "an unaddressed centroid input refused a 4x mask"); + queue = masked(0x0000000fu, 0u, 2u); + queue.context[0x1b3] = 2u | centroid; + queue.context[0x1b4] = 2u | centroid; + Require(QuadPixelMask(accepted(queue, "centroid interpolation under a whole-pixel mask")) == 0x1u, "centroid interpolation refused a mask that covers whole pixels"); + } + queue = masked(0x0000ffffu, 0u); + queue.context[0x31d] = 0x12000; + state = accepted(queue, "a single-sample mask into a 4x target"); + Require(state.samples == 4 && state.quadSampleMask == Masks{0xf, 0, 0, 0} && QuadPixelMask(state) == 0x1u, "single-sample rasterization into a 4x target did not apply each pixel's sample 0 bit to all of its samples"); + queue.context[0x30e] = queue.context[0x30f] = 0xfffefffeu; + state = accepted(queue, "an empty single-sample mask into a 4x target"); + Require(UniformSampleMask(state) && HostSampleMask(state) == 0u, "an empty single-sample mask into a 4x target did not remove every sample"); + + AgcDriver::Graphics::Context context{}; + ShaderRecompiler::RecompileResult early; + early.spirv = std::vector{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeEarlyFragmentTests, (5u << 16u) | spv::OpFunction, 0u, 1u, 0u, 0u}; + ShaderRecompiler::RecompileResult late; + late.spirv = std::vector{spv::MagicNumber, 0x10300u, 0u, 2u, 0u, (3u << 16u) | spv::OpExecutionMode, 1u, spv::ExecutionModeOriginUpperLeft}; + Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(early) && !AgcDriver::Graphics::DeclaresEarlyFragmentTests(late), "early fragment tests were not read from the SPIR-V execution modes"); + using Stage = ShaderRecompiler::ShaderStage; + AgcDriver::Graphics::State uniform{}; + uniform.samples = 4; + uniform.quadSampleMask = {5, 5, 5, 5}; + const std::array earlyShaders{{{Stage::Fragment, &early, 0}}}; + const std::array lateShaders{{{Stage::Fragment, &late, 0}}}; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders); }, "earlyFragmentSampleMaskTestBeforeSampleCounting"); + AgcDriver::Graphics::ValidateSampleMask(context, uniform, lateShaders); + context.earlyFragmentSampleMaskTestBeforeSampleCounting = true; + AgcDriver::Graphics::ValidateSampleMask(context, uniform, earlyShaders); + ShaderRecompiler::RecompileResult stores; + ShaderRecompiler::DescriptorBinding written{}; + written.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + written.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + written.count = 1; + written.bufferWritten = {true}; + stores.bindings.push_back(written); + const std::array storing{{{Stage::Vertex, &late, 0}, {Stage::Fragment, &stores, 0}}}; + const std::array vertexStoring{{{Stage::Vertex, &stores, 0}, {Stage::Fragment, &late, 0}}}; + AgcDriver::Graphics::State varying{}; + varying.quadSampleMask = {1, 0, 0, 0}; + varying.pixelEarlyZ = true; + varying.depth = AgcDriver::Graphics::DepthTarget{}; + varying.depthTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); + AgcDriver::Graphics::ValidateSampleMask(context, varying, vertexStoring); + for (const auto change : {0, 1, 2}) { + auto relaxed = varying; + if (change == 0) relaxed.pixelEarlyZ = false; + if (change == 1) relaxed.depthTest = false; + if (change == 2) relaxed.quadSampleMask = {1, 1, 1, 1}; + AgcDriver::Graphics::ValidateSampleMask(context, relaxed, storing); + } + varying.depthTest = false; + varying.stencilTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); + varying.stencilTest = false; + varying.depthBoundsTest = true; + expectFailure([&] { AgcDriver::Graphics::ValidateSampleMask(context, varying, storing); }, "differs between the pixels of a quad"); } void DepthClipTests() { @@ -5120,6 +5251,11 @@ void earlyFragmentTestsTests() { request.context.pixel->alphaToCoverage = true; const auto replayed = serializer.Deserialize(serializer.Serialize(request)); Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(replayed.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with alpha-to-coverage declared early fragment tests"); + request = depthExportRequest(shaderControl); + request.context.pixel->quadPixelMask = 0x1u; + const auto masked = serializer.Deserialize(serializer.Serialize(request)); + Require(!declaresEarlyFragmentTests(request) && !declaresEarlyFragmentTests(masked.request), "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " with a quad pixel mask declared early fragment tests"); + Require(AgcDriver::Graphics::DeclaresEarlyFragmentTests(ShaderRecompiler::Recompile(depthExportRequest(shaderControl))), "the driver did not read early fragment tests from a recompiled pixel shader"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index bf4b264415..df0cf71b8e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -2605,6 +2605,12 @@ void verifyMeshConfiguration() { std::vector full; RecompileCacheKey::Build(fragment, full); require(masked != full, "the quad pixel mask is not part of the recompile cache key"); + fragment.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x0u, 0xffffu}; + const auto sampled = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(fragment)); + require(sampled.request.context.pixel.has_value() && sampled.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x0u, 0xffffu}, "the quad sample masks were lost in serialization"); + std::vector partial; + RecompileCacheKey::Build(fragment, partial); + require(partial != full, "the quad sample masks are not part of the recompile cache key"); } std::vector key; RecompileCacheKey::Build(request, key); @@ -2986,7 +2992,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQRwAAAA=", "new requests did not use serialization version 28"); + require(requestPrefix(encoded, 8u) == "NVNQQR0AAAA=", "new requests did not use serialization version 29"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -3101,10 +3107,10 @@ void verifyPixelRequestSerialization() { require(RecompileCacheKey::ContextHash(legacy19.request) == RecompileCacheKey::ContextHash(upgraded19.request), "upgrading version19 changed shader identity"); } constexpr std::size_t mappingOffset = 8u + 37u + 18u + 163u; - for (std::size_t bytes = 0; bytes < 25u; ++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, or quad pixel mask was accepted"); + for (std::size_t bytes = 0; bytes < 41u; ++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=", "NVNQQR0AAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQR4AAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -3229,7 +3235,7 @@ void verifyVertexInfoVersion19() { std::size_t allBitsOffset = 0; while (allBitsOffset < version26Pixel.size() && version26Pixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset; require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); - version26Pixel.erase(version26Pixel.begin() + static_cast(allBitsOffset), version26Pixel.begin() + static_cast(allBitsOffset + 2u)); + version26Pixel.erase(version26Pixel.begin() + static_cast(allBitsOffset), version26Pixel.begin() + static_cast(allBitsOffset + 2u + 16u)); version26Pixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(version26Pixel)); require(replay26.request.context.pixel.has_value() && replay26.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replay26.request.context.pixel->frontFaceAllBits, "a version 26 request lost its pixel primitive class or read a front-face all-bits flag"); @@ -3240,12 +3246,21 @@ void verifyVertexInfoVersion19() { std::size_t maskOffset = 0; while (maskOffset < legacyPixel.size() && legacyPixel[maskOffset] == maskedBytes[maskOffset]) ++maskOffset; require(maskOffset == flagOffset + 3u && maskOffset < legacyPixel.size(), "the quad pixel mask is not the byte after the front-face all-bits flag"); + constexpr std::size_t sampleMaskBytes = 16u; + auto sampledPixel = maskedPixel; + sampledPixel.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x4u, 0x8u}; + auto version28 = decodeBase64(serializer.Serialize(sampledPixel)); + require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), version28.begin()) && version28[maskOffset + 1u] == 0x1u && version28[maskOffset + 5u] == 0x2u && version28[maskOffset + 9u] == 0x4u && version28[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); + version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes)); + 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} && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or misread the fields after them"); auto version27 = maskedBytes; - version27.erase(version27.begin() + static_cast(maskOffset)); + version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or misread the fields after it"); - legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 4u)); + legacyPixel.erase(legacyPixel.begin() + static_cast(flagOffset), legacyPixel.begin() + static_cast(flagOffset + 4u + sampleMaskBytes)); legacyPixel.resize(legacyPixel.size() - sizeof(std::uint64_t)); for (const auto version : {19u, 20u, 21u, 22u}) { auto versionPixel = legacyPixel; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDepthStencil.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDepthStencil.cpp new file mode 100644 index 0000000000..412ceb0a5d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDepthStencil.cpp @@ -0,0 +1,215 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t EarlyZThenLateZ = 0x10u; +constexpr std::uint8_t Reference = 0x5au; +constexpr std::byte Kept{0x40}; +alignas(256) std::array Pixels{}; +alignas(256) std::array DepthMemory{}; +alignas(256) std::array StencilMemory{}; +alignas(256) std::array Stores{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PlainPixelCode{ + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +alignas(256) constexpr std::array StoringPixelCode{ + 0x7e040f00, 0x7e060f01, 0x34060686, 0x4a040702, 0x34040482, 0x7e080281, 0xe0705000, 0x80000402, + 0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000, +}; + +using Triangle = std::array, 3>; +alignas(256) constexpr Triangle Middle{{{-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) constexpr Triangle Near{{{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + std::array quadSampleMask{1, 1, 1, 1}; + const Triangle* triangle = &Middle; + std::span pixelCode = PlainPixelCode; + std::uint32_t shaderControl = EarlyZThenLateZ; + bool depthTest = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; + VkStencilOpState stencil{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0u, 0u}; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) { + Pixels.fill(Kept); + Stores.fill(0); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.triangle->data(), 16u, static_cast(pass.triangle->size())); + 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()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), reinterpret_cast(StencilMemory.data()), {Width, Height}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.0f, 0}; + state.depthTest = pass.depthTest; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + state.stencilTest = true; + state.stencilFront = pass.stencil; + state.stencilBack = pass.stencil; + 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}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + state.quadSampleMask = pass.quadSampleMask; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + + const AgcDriver::Registers context{{0x1b3, 0x300u}, {0x1b4, 0x300u}, {0x1b6, 0u}, {0x1c5, 9u}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.earlyZ == state.pixelEarlyZ, "the pixel stage and the decoded state disagree on early Z"); + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + std::vector pixelUserData(4, 0u); + const auto stores = BufferDescriptor(Stores.data(), 0u, static_cast(sizeof(Stores))); + std::copy(stores.begin(), stores.end(), pixelUserData.begin()); + const std::array pixelMemory{{{reinterpret_cast(pass.pixelCode.data()), std::as_bytes(pass.pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pass.pixelCode.data()), pass.pixelCode, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.triangle->size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const std::function& drawn, const std::string& what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto expected = drawn(x, y) ? std::byte{255} : Kept; + const auto actual = Pixels[(y * Width + x) * 4u]; + Require(actual == expected, what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") is " + std::to_string(static_cast(actual)) + ", expected " + (drawn(x, y) ? "drawn" : "kept")); + } + } +} + +bool Covered(std::uint8_t quadPixelMask, std::uint32_t x, std::uint32_t y) { + return ((quadPixelMask >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0u; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const VkStencilOpState replace{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_REPLACE, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, Reference}; + const VkStencilOpState equal{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_EQUAL, 0xffu, 0u, Reference}; + const VkStencilOpState always{VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_STENCIL_OP_KEEP, VK_COMPARE_OP_ALWAYS, 0xffu, 0u, 0u}; + const VkStencilOpState zero{VK_STENCIL_OP_ZERO, VK_STENCIL_OP_ZERO, VK_STENCIL_OP_ZERO, VK_COMPARE_OP_ALWAYS, 0xffu, 0xffu, 0u}; + for (const std::uint8_t quadPixelMask : {std::uint8_t{0x1}, std::uint8_t{0x2}, std::uint8_t{0x9}}) { + const auto mask = "quad pixel mask " + std::to_string(quadPixelMask); + Pass reset; + reset.triangle = &Near; + reset.depthTest = true; + reset.depthWrite = true; + reset.stencil = zero; + Draw(*device, reset); + Check([](std::uint32_t, std::uint32_t) { return true; }, mask + ", the full-screen draw that resets depth to 0.25 and stencil to 0"); + Pass gbuffer; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) gbuffer.quadSampleMask[pixel] = static_cast((quadPixelMask >> pixel) & 1u); + gbuffer.depthTest = true; + gbuffer.depthWrite = true; + gbuffer.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL; + gbuffer.stencil = replace; + Draw(*device, gbuffer); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", the early-Z GEQUAL draw with depth writes and stencil REPLACE"); + Pass stencilProbe; + stencilProbe.stencil = equal; + Draw(*device, stencilProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", stencil equal to the replaced reference"); + Pass depthProbe; + depthProbe.triangle = &Near; + depthProbe.depthTest = true; + depthProbe.depthCompare = VK_COMPARE_OP_LESS; + depthProbe.stencil = always; + Draw(*device, depthProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return Covered(quadPixelMask, x, y); }, mask + ", depth 0.25 less than the stored depth"); + depthProbe.depthCompare = VK_COMPARE_OP_GREATER_OR_EQUAL; + Draw(*device, depthProbe); + Check([&](std::uint32_t x, std::uint32_t y) { return !Covered(quadPixelMask, x, y); }, mask + ", depth 0.25 at least the stored depth"); + } + Pass storing; + storing.quadSampleMask = {1, 0, 0, 0}; + storing.pixelCode = StoringPixelCode; + storing.depthTest = true; + storing.depthCompare = VK_COMPARE_OP_ALWAYS; + storing.stencil = always; + try { + Draw(*device, storing); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("differs between the pixels of a quad") != std::string::npos, std::string("a storing early-Z pixel program under a quad mask failed for another reason: ") + error.what()); + storing.shaderControl = 0u; + Draw(*device, storing); + Check([](std::uint32_t x, std::uint32_t y) { return Covered(0x1, x, y); }, "the storing late-Z pixel program under quad pixel mask 1"); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + const auto expected = Covered(0x1, pixel % Width, pixel / Width) ? 1u : 0u; + Require(Stores[pixel] == expected, "the storing pixel program under quad pixel mask 1 stored " + std::to_string(Stores[pixel]) + " at pixel " + std::to_string(pixel) + ", expected " + std::to_string(expected)); + } + std::puts("quad sample mask depth and stencil tests passed"); + return 0; + } + Require(false, "a quad-masked early-Z pixel program that stores to memory was drawn with depth tests"); + return 1; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp new file mode 100644 index 0000000000..867590f0cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskDraw.cpp @@ -0,0 +1,213 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.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 + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t SetContextReg = 0x69u; +constexpr std::uint32_t SetShReg = 0x76u; +constexpr std::uint32_t SetUconfigReg = 0x79u; +constexpr std::uint32_t DrawIndexAuto = 0x2du; +constexpr std::size_t Block = 65536; +constexpr std::size_t ColorBytes = Width * Height * 4; +std::byte* ColorMemory = nullptr; +alignas(64) volatile std::uint32_t EndOfPipe = 0; + +std::byte* AllocateWatched(std::size_t bytes) { +#ifdef _WIN32 + void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block); + GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes); +#else + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); +#endif + return static_cast(block); +} + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80020005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array PixelCode{ + 0x7e0002f2, 0x7e0202f2, 0x7e0402f2, 0x7e0602f2, 0xf800080f, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array, 3> Vertices{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}, +}}; + +struct Step { + const char* name; + std::uint32_t top; + std::uint32_t bottom; + std::uint32_t shaderControl; + std::uint8_t quadPixels; +}; + +const std::array Steps{{ + {"PA_SC_AA_MASK 0x0000ffff/0 with early Z (GTA V, X0Y0)", 0x0000ffffu, 0u, 0x10u, 0x1u}, + {"PA_SC_AA_MASK 0x0000ffff/0", 0x0000ffffu, 0u, 0x800u, 0x1u}, + {"PA_SC_AA_MASK 0xffff0000/0 (X1Y0)", 0xffff0000u, 0u, 0x800u, 0x2u}, + {"PA_SC_AA_MASK 0/0xffff0000 (X1Y1)", 0u, 0xffff0000u, 0x800u, 0x8u}, + {"PA_SC_AA_MASK 0xfffe0001/0x0001fffe (X0Y0 and X1Y1 by bit 0)", 0xfffe0001u, 0x0001fffeu, 0x800u, 0x9u}, + {"PA_SC_AA_MASK 0x00010001/0x00010001 (full at one sample)", 0x00010001u, 0x00010001u, 0x800u, 0xfu}, + {"PA_SC_AA_MASK 0/0 (empty)", 0u, 0u, 0x800u, 0x0u}, + {"full PA_SC_AA_MASK", 0xffffffffu, 0xffffffffu, 0x800u, 0xfu}, +}}; + +struct RegisteredShader { + Shader shader; + ShaderUserData userData; +}; +RegisteredShader VertexShader{}; +RegisteredShader PixelShader{}; + +const std::vector> ContextBaseline{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b8, 0x9000000}, {0x1e0, 0}, + {0xc, 0}, + {0x81, 0x80000000}, {0x90, 0x80000000}, {0x94, 0x80000000}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x114, 0}, {0x113, std::bit_cast(1.0f)}, +}; + +void Register(RegisteredShader& registered, const void* code, std::uint32_t bytes, std::uint8_t type) { + registered.shader.file_header = 0x34333231u; + registered.shader.version = 0x18u; + registered.shader.user_data = ®istered.userData; + registered.shader.code = code; + registered.shader.header_size = sizeof(RegisteredShader); + registered.shader.shader_size = bytes; + registered.shader.type = type; + AgcDriverRegisterShader_nid_postfix(®istered.shader); +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +void SetRegister(std::vector& words, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) { + words.push_back(0xc0010000u | (opcode << 8u)); + words.push_back(offset); + words.push_back(value); +} + +std::vector Frame(const Step& step) { + const auto colorAddress = reinterpret_cast(ColorMemory); + const auto vertexAddress = reinterpret_cast(VertexCode.data()); + const auto pixelAddress = reinterpret_cast(PixelCode.data()); + std::vector words; + for (const auto& [offset, value] : ContextBaseline) SetRegister(words, SetContextReg, offset, value); + SetRegister(words, SetContextReg, 0x203, step.shaderControl); + SetRegister(words, SetContextReg, 0x30e, step.top); + SetRegister(words, SetContextReg, 0x30f, step.bottom); + SetRegister(words, SetContextReg, 0x318, static_cast(colorAddress >> 8u)); + SetRegister(words, SetContextReg, 0x390, static_cast(colorAddress >> 40u)); + SetRegister(words, SetContextReg, 0x3b0, ((Width - 1u) << 14u) | (Height - 1u)); + SetRegister(words, SetContextReg, 0x10f, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x110, std::bit_cast(Width / 2.0f)); + SetRegister(words, SetContextReg, 0x111, std::bit_cast(-(Height / 2.0f))); + SetRegister(words, SetContextReg, 0x112, std::bit_cast(Height / 2.0f)); + const auto extent = (Height << 16u) | Width; + SetRegister(words, SetContextReg, 0xd, extent); + SetRegister(words, SetContextReg, 0x82, extent); + SetRegister(words, SetContextReg, 0x91, extent); + SetRegister(words, SetContextReg, 0x95, extent); + SetRegister(words, SetUconfigReg, 0x242, 4u); + SetRegister(words, SetShReg, 0x008, static_cast(pixelAddress >> 8u)); + SetRegister(words, SetShReg, 0x009, static_cast(pixelAddress >> 40u)); + SetRegister(words, SetShReg, 0x00b, 0u); + SetRegister(words, SetShReg, 0x0c8, static_cast(vertexAddress >> 8u)); + SetRegister(words, SetShReg, 0x0c9, static_cast(vertexAddress >> 40u)); + SetRegister(words, SetShReg, 0x08b, 4u << 1u); + const auto descriptor = BufferDescriptor(Vertices.data(), 16u, static_cast(Vertices.size())); + for (std::uint32_t i = 0; i < descriptor.size(); ++i) SetRegister(words, SetShReg, 0x08c + i, descriptor[i]); + words.insert(words.end(), {0xc0000000u | (1u << 16u) | (DrawIndexAuto << 8u), static_cast(Vertices.size()), 2u}); + const auto labelAddress = reinterpret_cast(&EndOfPipe); + words.insert(words.end(), {0xc0064900u, 0x514u, (1u << 29u) | (2u << 24u), static_cast(labelAddress), static_cast(static_cast(labelAddress) >> 32u), 1u, 0u, 0u}); + return words; +} + +void Check(const Step& step, const std::vector& colors) { + const auto what = std::string("sample mask through the driver, ") + step.name; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto value = std::to_integer(colors[(y * Width + x) * 4u]); + const bool expected = ((step.quadPixels >> ((x & 1u) | ((y & 1u) << 1u))) & 1u) != 0u; + Require(value == (expected ? 255u : 0u), what + ": pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") stored " + std::to_string(value) + ", expected " + (expected ? "written" : "clear")); + } + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ColorMemory = AllocateWatched(Block); + Register(VertexShader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelShader, PixelCode.data(), sizeof(PixelCode), 1); + for (int round = 0; round < 2; ++round) { + for (const auto& step : Steps) { + std::fill_n(ColorMemory, ColorBytes, std::byte{0}); + EndOfPipe = 0; + auto words = Frame(step); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + Require(sceAgcDriverSubmitDcb(&packet) == 0, std::string("DCB submission failed for ") + step.name); + AgcDriverWaitIdle_nid_postfix(); + Require(EndOfPipe == 1u, std::string("the end-of-pipe label was not written for ") + step.name); + std::vector colors(ColorBytes); + AgcDriver::GuestMemory::Read(static_cast(reinterpret_cast(ColorMemory)), colors); + Check(step, colors); + } + } + AgcDriverShutdown_nid_postfix(); + std::puts("sample mask draws through the driver passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskMultisample.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskMultisample.cpp new file mode 100644 index 0000000000..7ec9bf0884 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMaskMultisample.cpp @@ -0,0 +1,209 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::PixelInput; +using ShaderRecompiler::PixelInputBit; +using ShaderRecompiler::ShaderStage; +using Masks = std::array; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 16; +constexpr std::uint32_t Samples = 4; +constexpr std::uint32_t WaveSize = 64; +constexpr std::uint32_t EarlyZThenLateZ = 0x10u; +constexpr Masks Full{0xf, 0xf, 0xf, 0xf}; +alignas(256) std::array Invocations{}; +alignas(256) std::array DepthMemory{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array SampleStoreCode{ + 0x7e100f04, 0x7e120f05, 0x34121286, 0x4a101308, 0x34101082, 0xd548000a, 0x02111106, 0x4a101508, + 0x34101082, 0x7e160281, 0xe0705000, 0x80000b08, 0xbf8c3f70, 0xf8001890, 0x00000000, 0xbf810000, + 0xbf810000, +}; + +alignas(256) constexpr std::array NullExportCode{0xf8001890, 0x00000000, 0xbf810000}; + +using Triangle = std::array, 3>; +alignas(256) constexpr Triangle Far{{{-1.0f, -1.0f, 0.25f, 1.0f}, {3.0f, -1.0f, 0.25f, 1.0f}, {-1.0f, 3.0f, 0.25f, 1.0f}}}; +alignas(256) constexpr Triangle Probe{{{-1.0f, -1.0f, 0.375f, 1.0f}, {3.0f, -1.0f, 0.375f, 1.0f}, {-1.0f, 3.0f, 0.375f, 1.0f}}}; +alignas(256) constexpr Triangle Middle{{{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}}}; + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, stride == 0u ? 0x31016facu : 0x01016facu}; +} + +struct Pass { + Masks quadSampleMask = Full; + bool perSample = true; + std::uint32_t shaderControl = 0u; + const Triangle* triangle = &Middle; + bool depth = false; + bool depthWrite = false; + VkCompareOp depthCompare = VK_COMPARE_OP_ALWAYS; +}; + +void Draw(AgcDriver::VulkanDevice& device, const Pass& pass) { + Invocations.fill(0); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(pass.triangle->data(), 16u, static_cast(pass.triangle->size())); + 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()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt}; + state.hasColorTarget = false; + 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}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blendConstants = {}; + state.samples = Samples; + state.quadSampleMask = pass.quadSampleMask; + state.pixelEarlyZ = AgcDriver::Graphics::PixelEarlyZ(pass.shaderControl); + if (pass.depth) { + state.depth = AgcDriver::Graphics::DepthTarget{reinterpret_cast(DepthMemory.data()), 0u, {Width, Height}, VK_FORMAT_D32_SFLOAT, 0.0f, 0}; + state.depth->samples = Samples; + state.depthTest = true; + state.depthWrite = pass.depthWrite; + state.depthCompare = pass.depthCompare; + } + + const std::uint32_t inputs = pass.perSample ? PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY) | PixelInputBit(PixelInput::Ancillary) : PixelInputBit(PixelInput::PerspectiveCenter); + const AgcDriver::Registers context{{0x1b3, inputs}, {0x1b4, inputs}, {0x1b6, 0u}, {0x1c5, 0u}, {0x203, pass.shaderControl}}; + auto pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + Require(pixel.sampleShading == pass.perSample, "the pixel inputs did not select the requested shading rate"); + pixel.quadPixelMask = AgcDriver::Graphics::QuadPixelMask(state); + pixel.quadSampleMask = AgcDriver::Graphics::QuadSampleMasks(state); + std::vector pixelUserData(4, 0u); + const auto invocations = BufferDescriptor(Invocations.data(), 0u, static_cast(sizeof(Invocations))); + std::copy(invocations.begin(), invocations.end(), pixelUserData.begin()); + const std::span code = pass.perSample ? std::span(SampleStoreCode) : std::span(NullExportCode); + const std::array pixelMemory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(code.data()), code, 0, {}}, + {WaveSize, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(pass.triangle->size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +void Check(const Masks& quadSampleMask, const std::string& what) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto mask = quadSampleMask[(x & 1u) | ((y & 1u) << 1u)]; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto expected = (mask >> sample) & 1u; + const auto ran = Invocations[(y * Width + x) * Samples + sample]; + Require(ran == expected, what + ": sample " + std::to_string(sample) + " of pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") " + (ran != 0u ? "stored" : "did not store") + " (" + std::to_string(ran) + "), the quad pixel's mask is " + std::to_string(mask)); + } + } + } +} + +void DepthWritten(AgcDriver::VulkanDevice& device, const Masks& mask, const std::string& what) { + Pass reset; + reset.perSample = false; + reset.triangle = &Far; + reset.depth = true; + reset.depthWrite = true; + Draw(device, reset); + Pass masked = reset; + masked.quadSampleMask = mask; + masked.triangle = &Middle; + Draw(device, masked); + Pass probe; + probe.shaderControl = EarlyZThenLateZ; + probe.triangle = &Probe; + probe.depth = true; + probe.depthCompare = VK_COMPARE_OP_LESS; + Draw(device, probe); + Check(mask, "the samples a pixel-rate draw with " + what + " wrote depth 0.5 into, probed by early-Z per-sample stores"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + struct Case { + const char* name; + Masks mask; + }; + const std::array cases{{ + {"no sample mask", Full}, + {"samples 0 and 2 of every pixel", {0x5, 0x5, 0x5, 0x5}}, + {"sample 3 of every pixel", {0x8, 0x8, 0x8, 0x8}}, + {"no sample of any pixel", {0x0, 0x0, 0x0, 0x0}}, + {"every sample of the even-x, even-y pixel of each quad", {0xf, 0x0, 0x0, 0x0}}, + {"every sample of the odd-x pixels of each quad", {0x0, 0xf, 0x0, 0xf}}, + {"sample q of quad pixel q", {0x1, 0x2, 0x4, 0x8}}, + {"all, none, none and samples 0 and 1", {0xf, 0x0, 0x0, 0x3}}, + {"samples 1 and 3, 0 and 2, 2 and 3, and 0", {0xa, 0x5, 0xc, 0x1}}, + }}; + for (const auto& test : cases) { + Pass pass; + pass.quadSampleMask = test.mask; + Draw(*device, pass); + Check(test.mask, std::string("4x per-sample shading with ") + test.name); + } + for (const auto& test : cases) DepthWritten(*device, test.mask, test.name); + Pass early; + early.quadSampleMask = {0x6, 0x6, 0x6, 0x6}; + early.shaderControl = EarlyZThenLateZ; + try { + Draw(*device, early); + } catch (const std::exception& error) { + Require(std::string(error.what()).find("earlyFragmentSampleMaskTestBeforeSampleCounting") != std::string::npos, std::string("a sample mask with early fragment tests failed for another reason: ") + error.what()); + std::puts("4x sample mask tests passed (early fragment tests refused: the device does not order the sample mask test before sample counting)"); + return 0; + } + Check(early.quadSampleMask, "4x per-sample shading with early fragment tests and samples 1 and 2 of every pixel"); + std::puts("4x sample mask tests passed"); + 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 da2e8369b7..9a05433674 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -192,6 +192,7 @@ private: append(key, value.stencilRefExportEnable); append(key, value.ditheredAlphaToCoverage); append(key, value.quadPixelMask); + append(key, value.quadSampleMask); append(key, value.targetOutputMode); } diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index a760eb0831..5699b039f3 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -341,6 +341,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.ditheredAlphaToCoverage); writer.WriteBool(info.frontFaceAllBits); writer.WriteU8(info.quadPixelMask); + for (const std::uint16_t mask : info.quadSampleMask) writer.WriteU32(mask); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -395,6 +396,13 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { if (version >= 25u) info.ditheredAlphaToCoverage = reader.ReadBool(); if (version >= 27u) info.frontFaceAllBits = reader.ReadBool(); if (version >= 28u) info.quadPixelMask = reader.ReadU8(); + if (version >= 29u) { + for (std::uint16_t& mask : info.quadSampleMask) { + const auto value = reader.ReadU32(); + if (value > 0xffffu) throw std::runtime_error("invalid recompile request quad sample mask"); + mask = static_cast(value); + } + } 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) | @@ -766,7 +774,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(28u); + writer.WriteU32(29u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -799,7 +807,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 > 28u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 29u) 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/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 44844c85f8..72fe2675ee 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -3,6 +3,7 @@ #include "IntermediateRepresentation/IrMetadata.hpp" #include "Recompiler.hpp" +#include #include #include #include @@ -182,12 +183,21 @@ struct ShaderPixelInputInfo { bool psAlphaToCoverage = false; bool psDitheredAlphaToCoverage = false; std::uint8_t quadPixelMask = 0xf; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; ShaderStageRuntime stage; bool HasPositionInput() const { return psPosX || psPosY || psPosZ || psPosW; } + [[nodiscard]] bool QuadSampleMaskPartial() const { + return std::ranges::any_of(quadSampleMask, [](std::uint16_t mask) { return mask != 0xffffu; }); + } + + [[nodiscard]] bool QuadMasked() const { + return quadPixelMask != 0xf || QuadSampleMaskPartial(); + } + [[nodiscard]] bool InputIsDefault(std::uint32_t input) const { return input < inputNum && input < 32u && (interpolatorSettings[input] & 0x420u) == 0x20u; } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 213cb420a6..fac95cb5d4 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -237,7 +237,7 @@ bool IsPixelParameterFlat(const ShaderPixelInputInfo& pixel, std::uint32_t input } void CollectPixelInputs(const IrProgram& program, const ShaderPixelInputInfo* pixel, ShaderInfo& info) { - if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->quadPixelMask != 0xf) { + if (pixel->HasPositionInput() || pixel->psDitheredAlphaToCoverage || pixel->QuadMasked()) { AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); } if (pixel->psFrontFace) { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index b93b873ac6..5ddc9dfb7d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -155,6 +155,7 @@ struct ShaderPixelStageInfo { bool layerExported = false; bool ditheredAlphaToCoverage = false; std::uint8_t quadPixelMask = 0xf; + std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 34ec9df6e5..b85eb66e9f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -180,6 +180,8 @@ struct SpirvEmitterState { const IrBlock* currentBlock = nullptr; std::uint32_t pixelValidMaskVariable = 0; std::uint32_t quadPixelUncovered = 0; + std::uint32_t sampleMaskInputVariable = 0; + std::uint32_t quadSampleCoverage = 0; std::uint32_t subgroupLocalInvocationIdVariable = 0; std::uint32_t perVertexVariable = 0; std::uint32_t pointSizeVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 86bba07e2f..168803014c 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -298,6 +298,11 @@ void DefineInputs(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationFlat); } } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) { + state.sampleMaskInputVariable = DefineInterfaceVariable(state, SampleMaskArrayType(state), spv::StorageClassInput, "gl_SampleMaskIn"); + state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationBuiltIn, spv::BuiltInSampleMask); + state.module.AddAnnotation(spv::OpDecorate, state.sampleMaskInputVariable, spv::DecorationFlat); + } } constexpr std::uint32_t MaxClipDistances = 8u; @@ -399,6 +404,9 @@ void DefineOutputs(SpirvEmitterState& state) { if (state.inputInfo.pixel->psSampleMaskExportEnable) throw std::runtime_error("alpha-to-coverage with a sample-mask export is unsupported"); BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadSampleMaskPartial()) { + BuiltIn(state.sampleMaskVariable, SampleMaskArrayType(state), "gl_SampleMask", spv::BuiltInSampleMask); + } } void DefineDescriptors(SpirvEmitterState& state) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 8c9d445e96..7e4da050f6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -196,7 +196,7 @@ 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.quadPixelMask == 0xf) { + if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } if (pixel.psOrderedPixelShader) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 81fae8e83c..edabae4d45 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -288,6 +288,7 @@ bool AlphaToCoverage(const SpirvEmitterState& state) { } void StoreCoverage(SpirvEmitterState& state, std::uint32_t mask) { + if (state.quadSampleCoverage != 0u) mask = Binary(state, spv::OpBitwiseAnd, TypeI32(state), mask, state.quadSampleCoverage); const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); state.module.AddFunction(spv::OpStore, pointer, mask); @@ -1185,14 +1186,29 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } EmitMemoryOffsets(state); - if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->quadPixelMask != 0xfu) { + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->QuadMasked()) { + const auto& pixel = *state.inputInfo.pixel; const auto coordinate = [&](std::uint32_t component) { const auto value = Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component)); return EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpConvertFToU, TypeU32(state), value), ConstantU32(state, 1u)); }; const auto quad = EmitBinaryU32(state, spv::OpBitwiseOr, coordinate(0u), EmitBinaryU32(state, spv::OpShiftLeftLogical, coordinate(1u), ConstantU32(state, 1u))); - const auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, state.inputInfo.pixel->quadPixelMask), quad), ConstantU32(state, 1u)); - const auto mask = static_cast(state.inputInfo.pixel->quadPixelMask); + auto bit = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, ConstantU32(state, pixel.quadPixelMask), quad), ConstantU32(state, 1u)); + if (pixel.QuadSampleMaskPartial()) { + const auto top = ConstantU32(state, static_cast(pixel.quadSampleMask[0]) | (static_cast(pixel.quadSampleMask[1]) << 16u)); + const auto bottom = ConstantU32(state, static_cast(pixel.quadSampleMask[2]) | (static_cast(pixel.quadSampleMask[3]) << 16u)); + const auto lower = EmitCompareU32Constant(state, spv::OpIEqual, EmitBinaryU32(state, spv::OpBitwiseAnd, quad, ConstantU32(state, 2u)), 2u); + const auto row = Select(state, TypeU32(state), lower, bottom, top); + const auto shift = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitBinaryU32(state, spv::OpBitwiseAnd, quad, ConstantU32(state, 1u)), ConstantU32(state, 4u)); + const auto covered = EmitBinaryU32(state, spv::OpBitwiseAnd, EmitBinaryU32(state, spv::OpShiftRightLogical, row, shift), ConstantU32(state, 0xffffu)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassInput, TypeI32(state)), pointer, state.sampleMaskInputVariable, ConstantU32(state, 0u)); + const auto rasterized = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeI32(state), rasterized, pointer); + bit = EmitBinaryU32(state, spv::OpBitwiseAnd, Unary(state, spv::OpBitcast, TypeU32(state), rasterized), covered); + state.quadSampleCoverage = Unary(state, spv::OpBitcast, TypeI32(state), covered); + } + const auto mask = static_cast(pixel.quadPixelMask); const bool demoteCapability = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDemoteToHelperInvocation)) != state.supportedCapabilities.end(); const bool demoteCore = state.spirvVersion >= 0x00010600u; const bool demoteExtension = std::find(state.supportedExtensions.begin(), state.supportedExtensions.end(), "SPV_EXT_demote_to_helper_invocation") != state.supportedExtensions.end(); @@ -1217,6 +1233,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpKill); } EmitLabel(state, mergeLabel); + if (state.quadSampleCoverage != 0u) StoreCoverage(state, ConstantI32(state, -1)); } if (program.BlockOrder().empty()) { if (OrderedPixelShader(state)) { @@ -1479,10 +1496,8 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { if ((exp.en & 4u) != 0u && state.sampleMaskVariable != 0u) { const auto raw = ExportRawComponent(ctx, data, 2); const auto value = state.module.AllocateId(); - const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpBitcast, TypeI32(state), value, raw); - state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassOutput, TypeI32(state)), pointer, state.sampleMaskVariable, ConstantU32(state, 0u)); - state.module.AddFunction(spv::OpStore, pointer, value); + StoreCoverage(state, value); } return; } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 972a73a2c4..5de4543f7b 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -149,6 +149,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psAlphaToCoverage = pixel.alphaToCoverage; pixelStorage.psDitheredAlphaToCoverage = pixel.ditheredAlphaToCoverage; pixelStorage.quadPixelMask = pixel.quadPixelMask; + pixelStorage.quadSampleMask = pixel.quadSampleMask; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e65bfc4164..720fc6ccbd 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -47,6 +47,7 @@ - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. - [Per-sample pixel shading](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE` (bit 16, [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json)) on a multisampled draw enables Vulkan sample shading with a minimum rate of 1.0, so the pixel program runs once per sample. The draw throws when the device lacks `sampleRateShading`. On a single-sample draw the bit has no effect and is dropped. That the console runs the pixel program once per covered sample, and what it does with the other multisample registers in that mode, is taken from the register name and Vulkan's definition, not measured on hardware. - [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. +- [Sample masks](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `PA_SC_AA_MASK_X0Y0_X1Y0` and `PA_SC_AA_MASK_X0Y1_X1Y1` hold one 16-bit sample mask for each pixel of the aligned 2x2 quad ([Mesa gfx10.json](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/registers/gfx10.json#L13512)); only the bits of the `MSAA_NUM_SAMPLES` rasterized samples count, and at one sample bit 0 of a pixel's field covers every sample of the target the draw is raised to. A mask equal for the four pixels is the Vulkan `pSampleMask`, the case [radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13633) and [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/gallium/drivers/radeonsi/si_state.c#L4218) program; with early fragment tests it needs `earlyFragmentSampleMaskTestBeforeSampleCounting` ([radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_physical_device.c#L2079) reports it). A mask that differs between the pixels is applied in the pixel shader without early fragment tests: a pixel whose mask covers all or none of its samples is demoted to a helper invocation by `gl_FragCoord` parity, and otherwise the pixel's mask is ANDed into `gl_SampleMask` (with alpha-to-coverage and the mask export) after demoting invocations whose `SampleMask` input it leaves empty. A pixel program with early Z that writes memory under depth, stencil or depth-bounds tests is therefore refused. That XnYm is the pixel at (x mod 2, y mod 2) = (n, m) is read from the field names; that the mask removes samples before the depth and stencil tests and occlusion counting is inferred from radv implementing the Vulkan sample mask test with it; and that it takes no part in the centroid (`AA_MASK_CENTROID_DTMN` 0) is inferred from radv setting that bit only for conservative rasterization. None of it is measured on gfx1013. A mask equal for the four pixels that removes some but not all samples of a pixel is refused with active centroid inputs, since where Vulkan puts the centroid relative to `pSampleMask` is implementation-defined, and any partial mask is refused with conservative rasterization (radv sets `PREZ_AA_MASK_ENABLE` and `POSTZ_AA_MASK_ENABLE` [there](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13653), which 0x6001 leaves clear). - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. - [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords per workgroup (the last dword is the atomic lock), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request.