From ad6a55121ebb5285f65601fd37eb4fd87a0dc8a0 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 20 Oct 2026 11:57:56 +0000 Subject: [PATCH] fix(agc): read FMASK of up to 8 samples in every storage width Since the synthetic FMASK mode was removed, a direct FMASK load reads the stored word and only Fmask32_S8_F8 is accepted. A load from any other FMASK format of up to 8 samples now reads the 8-, 16- or 32-bit surface and expands it in the shader to 4 bits per sample, an index past the fragment count reading 8 (unknown). One runtime image mode handles every such format, reading the format from the descriptor at run time or from a specialization constant. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestTextureResource.cpp | 9 +- .../tests/GuestTextureResource.cpp | 25 +++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 15 +++- .../tests/execution/FmaskImageReads.cpp | 88 ++++++++++++++++--- core/shader/recompiler/CMakeLists.txt | 1 + .../src/DescriptorBindingBuilder.cpp | 3 + .../Optimization/src/ResourceMaterializer.cpp | 11 ++- .../recompiler/PipelineSpecialization.hpp | 1 + .../RdnaDecoder/RdnaDescriptorFormat.hpp | 12 +++ .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 15 ++++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 36 ++++++++ docs/dev/debt/shader.md | 2 +- 12 files changed, 198 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 1249e08037..866371295c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -179,12 +179,15 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(baseLevel <= lastLevel, "guest texture descriptor has a base mip level past its last mip level"); const bool fmask = ShaderRecompiler::IsFmaskTextureFormat(static_cast(format)); + const auto fmaskLayout = ShaderRecompiler::FmaskLayoutOf(static_cast(format)); if (fmask) { - Require(format == static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "guest FMASK format is not implemented"); + Require(fmaskLayout.has_value(), "guest FMASK format is not implemented"); Require(!writeCompress && (!metaCompress || metaAddr != 0u), "FMASK CMASK compression is not implemented without readable metadata"); Require(!metaCompress || metaPipeAligned, "FMASK CMASK metadata must be pipe aligned"); + Require(!metaCompress || format == static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); Require((typeRaw == 9u || typeRaw == 13u) && tileModeRaw == 24u && baseLevel == 0u && lastLevel == 0u && maxMip == 0u, "FMASK reads require a single-mip 2D or 2D-array Z_64KB_X surface"); - for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); + const bool identity = format != static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8) && dstSelX == 4u && dstSelY == 5u && dstSelZ == 6u && dstSelW == 7u; + for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(identity || selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); } const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + ", tile " + std::to_string(tileModeRaw) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; }; @@ -265,7 +268,7 @@ GuestTextureResource DecodeTextureResource(std::span words) result.lastLevel = lastLevel; result.tileMode = tileMode; result.dimension = dimension; - result.format = fmask ? static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt) : format; + result.format = !fmask ? format : static_cast(fmaskLayout->storageBits == 8u ? ShaderRecompiler::IrBufferFormat::Format8UInt : fmaskLayout->storageBits == 16u ? ShaderRecompiler::IrBufferFormat::Format16UInt : ShaderRecompiler::IrBufferFormat::Format32UInt); result.dstSelX = static_cast(dstSelX); result.dstSelY = static_cast(dstSelY); result.dstSelZ = static_cast(dstSelZ); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 0cc0c41ed7..ab7db93b7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -184,8 +184,29 @@ void RunGuestTextureResourceTests() { fmaskArray.baseArray = 1u; const auto fmaskArrayResource = DecodeTextureResource(pack(fmaskArray)); Require(fmaskArrayResource.format == 20u && fmaskArrayResource.baseArray == 1u && fmaskArrayResource.depthOrLastArray == 2u, "FMASK normalization changed the array view"); - for (std::uint32_t format = 156u; format <= 168u; ++format) { - if (format == 166u) continue; + constexpr std::array, 8> narrowFormats{{{156u, 5u}, {157u, 5u}, {158u, 5u}, {159u, 5u}, {160u, 5u}, {161u, 5u}, {163u, 11u}, {165u, 20u}}}; + for (const auto& [format, surface] : narrowFormats) { + auto narrow = fmask; + narrow.format = format; + const auto narrowResource = DecodeTextureResource(pack(narrow)); + Require(narrowResource.format == surface && narrowResource.tileMode == TextureTileMode::kZ64KBX, "FMASK " + std::to_string(format) + " did not use an integer Z surface of its storage width"); + narrow.dstSelX = 4u; + narrow.dstSelY = 5u; + narrow.dstSelZ = 6u; + narrow.dstSelW = 7u; + Require(DecodeTextureResource(pack(narrow)).format == surface, "an expanded FMASK " + std::to_string(format) + " view with identity selectors was refused"); + auto compressedNarrow = narrow; + compressedNarrow.metaCompress = compressedNarrow.metaPipeAligned = true; + compressedNarrow.metaAddr = 0x123456789aull; + rejectFields(compressedNarrow, "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); + } + auto identityWord = fmask; + identityWord.dstSelX = 4u; + identityWord.dstSelY = 5u; + identityWord.dstSelZ = 6u; + identityWord.dstSelW = 7u; + rejectFields(identityWord, "FMASK destination channel selector is not implemented"); + for (const auto format : {162u, 164u, 167u, 168u}) { auto unsupported = fmask; unsupported.format = format; rejectFields(unsupported, "FMASK format is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index effc0dc179..63fcc75d84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -7577,8 +7577,19 @@ void verifyFmaskImageMode() { const auto modes = ResourceMaterializer::RuntimeImageModes(image); const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); require(modes[selected].numericClass == IrTextureNumericClass::Uint && modes[selected].packedFormat == IrBufferFormat::Invalid && !modes[selected].constantSwizzle, "FMASK did not select a bound integer image mode"); - for (std::uint32_t format = 156u; format <= 168u; ++format) { - if (format == 166u) continue; + for (const auto format : {156u, 157u, 158u, 159u, 160u, 161u, 163u, 165u}) { + descriptor.dwords[1] = format << 20u; + const auto expanded = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(expanded != selected && modes[expanded].numericClass == IrTextureNumericClass::Uint && modes[expanded].conversionFormat == FmaskExpansion && modes[expanded].dimension == RdnaImageDimension::Dim2D, "an FMASK format of up to 8 samples did not select the FMASK expansion mode"); + } + require(std::ranges::count_if(modes, [](const ImageResource& mode) { return mode.conversionFormat == FmaskExpansion; }) == 1, "FMASK expansion needs exactly one runtime image mode"); + for (const auto format : {161u, 166u}) { + descriptor.dwords[1] = format << 20u; + descriptor.dwords[3] = 0x91800005u; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK destination channel selector is not implemented", "an FMASK load selecting Y was accepted"); + } + descriptor.dwords[3] = 0x91800004u; + for (const auto format : {162u, 164u, 167u, 168u}) { descriptor.dwords[1] = format << 20u; expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK format is not implemented", "an unsupported FMASK format was accepted"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp index b76ffe2a6d..e4b54fc027 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp @@ -32,9 +32,9 @@ std::array Buffer(const void* data, std::uint32_t bytes) { return {static_cast(address), static_cast(address >> 32u) & 0xffffu, bytes, 0x31016facu}; } -std::array Image(bool layered, std::uint32_t pipeXor, std::uint32_t swizzle = 4u) { +std::array Image(bool layered, std::uint32_t pipeXor, std::uint32_t swizzle = 4u, std::uint32_t format = 166u) { const auto address = reinterpret_cast(Texels.data()) + pipeXor; - return {static_cast(address >> 8u), static_cast(address >> 40u) | (166u << 20u) | (3u << 30u), 31u | ((Height - 1u) << 14u), swizzle | (24u << 20u) | ((layered ? 13u : 9u) << 28u), layered ? 2u | (1u << 16u) : 0u, 0u, 0u, 0u}; + return {static_cast(address >> 8u), static_cast(address >> 40u) | (format << 20u) | (3u << 30u), 31u | ((Height - 1u) << 14u), swizzle | (24u << 20u) | ((layered ? 13u : 9u) << 28u), layered ? 2u | (1u << 16u) : 0u, 0u, 0u, 0u}; } std::uint32_t Mapping(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t round) { @@ -56,15 +56,16 @@ std::uint64_t EquationOffset(const TextureSwizzleEquation& equation, std::uint32 return offset; } -void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { +template +void FillWith(bool layered, std::uint32_t pipeXor, std::uint32_t surfaceFormat, std::uint32_t bytes, TValue valueAt) { auto words = Image(layered, pipeXor); - words[1] = (words[1] & ~(0x1ffu << 20u)) | (20u << 20u); + words[1] = (words[1] & ~(0x1ffu << 20u)) | (surfaceFormat << 20u); const auto resource = DecodeTextureResource(words); const auto geometry = DescribeSurface(resource); Require(geometry.guestBytes <= Texels.size(), "FMASK fixture exceeds its allocation"); const auto& mip = geometry.mips.at(0); - const auto block = ThinBlockLayout(resource.tileMode, 4u); - const auto* equation = FindTextureSwizzleEquation(24u, 4u); + const auto block = ThinBlockLayout(resource.tileMode, bytes); + const auto* equation = FindTextureSwizzleEquation(24u, bytes); Require(equation != nullptr, "FMASK fixture has no Z swizzle equation"); std::ranges::fill(Texels, std::byte{0}); for (std::uint32_t layer = 0; layer < geometry.layers; ++layer) { @@ -73,14 +74,18 @@ void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { auto offset = geometry.GuestLayerOffset(layer) + mip.tiledOffset; if (mip.tail) offset += EquationOffset(*equation, x + mip.tailX, y + mip.tailY, layer) ^ pipeXor; else offset += (static_cast(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + (EquationOffset(*equation, x, y, layer) ^ pipeXor); - const auto value = Mapping(x, y, layer, round); - Require(offset + sizeof(value) <= Texels.size(), "FMASK fixture addresses outside its allocation"); - std::memcpy(Texels.data() + offset, &value, sizeof(value)); + const std::uint32_t value = valueAt(x, y, layer); + Require(offset + bytes <= Texels.size(), "FMASK fixture addresses outside its allocation"); + std::memcpy(Texels.data() + offset, &value, bytes); } } } } +void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { + FillWith(layered, pipeXor, 20u, 4u, [round](std::uint32_t x, std::uint32_t y, std::uint32_t layer) { return Mapping(x, y, layer, round); }); +} + ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span image, bool registration) { std::vector userData(16u); const auto input = Buffer(Input.data(), sizeof(Input)); @@ -140,6 +145,68 @@ void Run(AgcDriver::VulkanDevice& device, bool layered, bool registration, std:: } } +struct NarrowFmask { + std::uint32_t format; + std::uint32_t surfaceFormat; + std::uint32_t bytes; + std::uint32_t samples; + std::uint32_t fragments; + std::uint32_t sampleBits; + std::uint32_t identity; + std::uint32_t expandedIdentity; +}; + +constexpr std::array NarrowFormats{{ + {156u, 5u, 1u, 2u, 1u, 1u, 0x02u, 0x80u}, + {157u, 5u, 1u, 4u, 1u, 1u, 0x0eu, 0x8880u}, + {158u, 5u, 1u, 8u, 1u, 1u, 0xfeu, 0x88888880u}, + {159u, 5u, 1u, 2u, 2u, 1u, 0x02u, 0x10u}, + {160u, 5u, 1u, 4u, 2u, 2u, 0xa4u, 0x8810u}, + {161u, 5u, 1u, 4u, 4u, 2u, 0xe4u, 0x3210u}, + {163u, 11u, 2u, 8u, 2u, 2u, 0xaaa4u, 0x88888810u}, + {165u, 20u, 4u, 8u, 4u, 4u, 0x44443210u, 0x88883210u}, +}}; + +std::uint32_t Stored(const NarrowFmask& fmask, std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t round) { + if (round == 0u) return fmask.identity; + const auto value = (x * 0x9e3779b1u) ^ (y * 0x85ebca77u) ^ (layer * 0xc2b2ae3du) ^ (fmask.format * 0x27d4eb2fu); + return fmask.bytes == 4u ? value : value & ((1u << (fmask.bytes * 8u)) - 1u); +} + +std::uint32_t Expanded(const NarrowFmask& fmask, std::uint32_t stored) { + std::uint32_t expanded = 0; + for (std::uint32_t sample = 0; sample < fmask.samples; ++sample) { + const auto fragment = (stored >> (sample * fmask.sampleBits)) & ((1u << fmask.sampleBits) - 1u); + expanded |= (fragment >= fmask.fragments ? 8u : fragment) << (sample * 4u); + } + return expanded; +} + +void RunNarrow(AgcDriver::VulkanDevice& device, const NarrowFmask& fmask, bool layered, bool registration) { + auto code = Code; + if (layered) code[4] = 0xf0001f28u; + Require(Expanded(fmask, fmask.identity) == fmask.expandedIdentity, "FMASK " + std::to_string(fmask.format) + " reference does not expand PAL's identity value"); + for (std::uint32_t round = 0; round < 2u; ++round) { + FillWith(layered, 0u, fmask.surfaceFormat, fmask.bytes, [&](std::uint32_t x, std::uint32_t y, std::uint32_t layer) { return Stored(fmask, x, y, layer, round); }); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Input[lane * 4u] = (lane * 37u + 11u) % Width; + Input[lane * 4u + 1u] = (lane * 23u + 5u) % Height; + Input[lane * 4u + 2u] = layered ? lane & 1u : 0u; + Input[lane * 4u + 3u] = 0u; + } + Output.fill(Sentinel); + const auto shader = Compile(device, code, Image(layered, 0u, 4u, fmask.format), registration); + device.Dispatch(shader, 1u, 1u, 1u); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto stored = Stored(fmask, Input[lane * 4u], Input[lane * 4u + 1u], layered ? Input[lane * 4u + 2u] + 1u : 0u, round); + const std::array expected{Expanded(fmask, stored), 0u, 0u, 0u}; + for (std::uint32_t component = 0; component < 4u; ++component) Require(Output[lane * 4u + component] == expected[component], "FMASK " + std::to_string(fmask.format) + " load did not expand its mapping: round " + std::to_string(round) + ", lane " + std::to_string(lane) + ", component " + std::to_string(component) + ", read " + std::to_string(Output[lane * 4u + component]) + ", expected " + std::to_string(expected[component])); + } + Require(std::all_of(Output.begin() + Threads * 4u, Output.end(), [](auto value) { return value == Sentinel; }), "FMASK load wrote outside its output"); + } +} + } int main() { @@ -149,9 +216,10 @@ int main() { for (const bool registration : {false, true}) { for (const bool layered : {false, true}) { for (const auto pipeXor : {0u, 0xf00u}) Run(*device, layered, registration, pipeXor); + for (const auto& fmask : NarrowFormats) RunNarrow(*device, fmask, layered, registration); } } - std::cout << "FMASK image reads passed: 40 dispatches, 5120 component checks, 8 compression refusals\n"; + std::cout << "FMASK image reads passed: 104 dispatches, 13312 component checks, 8 compression refusals\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 0f83ea58a8..07dee3a0d9 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -249,6 +249,7 @@ function(add_shader_recompiler target runtimeTarget) ImageTableAbi::PoisonCode ImageTableAbi::PoisonReasonName ImageTableAbi::TableHeader + FmaskLayoutOf IsFmaskTextureFormat PipelineSpecialization::IsWorkgroupOffset PipelineSpecialization::WorkgroupOffsetBase diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 170b97d6b8..51b8c966e9 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -1,5 +1,6 @@ #include "Optimization/DescriptorBindingBuilder.hpp" #include "Optimization/ResourceMaterializer.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include @@ -355,6 +356,8 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) { if (info.images[index].table != NoTable) continue; + const auto& modes = info.runtimeImageModes.at(index); + if (std::ranges::any_of(modes, [](const ImageResource& mode) { return mode.conversionFormat == FmaskExpansion; })) plan.specialization.push_back({PipelineSpecialization::FmaskFormatBase + index, modes.at(imageModes[index]).conversionFormat == FmaskExpansion ? (snapshot.images.at(index).dwords[1] >> 20u) & 0x1ffu : 0u}); const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 5478136a1a..6514f474aa 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -232,9 +232,14 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag if (decoded.fmask && (storage || base.depthCompare || table)) { return rejectImage(PoisonReason::Fmask, "FMASK requires a direct sampled image load"); } - if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); + if (decoded.fmask && !FmaskLayoutOf(format).has_value()) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); if (decoded.fmask && ((descriptor.dwords[6] & (1u << 20u)) != 0u || ((descriptor.dwords[6] & (1u << 21u)) != 0u && (descriptor.dwords[6] >> 24u) == 0u && descriptor.dwords[7] == 0u))) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented without readable metadata"); + if (decoded.fmask && (descriptor.dwords[6] & (1u << 21u)) != 0u && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); if (decoded.fmask && (base.packed || base.byElements != 0u)) return rejectImage(PoisonReason::Fmask, "packed FMASK image reads are not implemented"); + for (std::uint32_t component = 0; decoded.fmask && component < 4u; ++component) { + const auto selector = (descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u; + if (selector != 0u && selector != 1u && selector != 4u) return rejectImage(PoisonReason::Fmask, "FMASK destination channel selector is not implemented"); + } if (base.packed) { if (table || (!storage && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle)) { return rejectImage(PoisonReason::InvalidFormat, "packed image access requires a direct image, with identity swizzle when sampled"); @@ -271,6 +276,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag if (((descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u) == 7u) return rejectImage(PoisonReason::Conversion, "selects a channel the converted image format does not have"); } } + if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) decoded.conversionFormat = FmaskExpansion; decoded.srgbDecode = !storage && !base.depthCompare && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; if (decoded.srgbDecode && !base.srgbDecodeCompatible) { return rejectImage(PoisonReason::SrgbUnsupported, "samples or gathers an sRGB image the device cannot sample, which is not implemented"); @@ -1882,7 +1888,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.shaderSwizzle = ShaderImageIdentitySwizzle; if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; modes.push_back(mode); - const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible; + const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible && conversion != FmaskExpansion; const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; const bool volumePlaneBias = image.dimension == RdnaImageDimension::Dim2D && image.singleMipPlaneBiasCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; const bool volumeArraySample = image.dimension == RdnaImageDimension::Dim2DArray && image.volumeArraySampleCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; @@ -1982,6 +1988,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, false); append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, true); } + if (image.fmaskCompatible && !table) append(IrTextureNumericClass::Uint, FmaskExpansion, IrBufferFormat::Invalid, false, false); } } } diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index ae9ec178cf..2adcd7e04a 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -42,6 +42,7 @@ inline constexpr std::uint32_t DescriptorIndexBase = 16384u; inline constexpr std::uint32_t DescriptorIndexStride = 128u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; inline constexpr std::uint32_t UnnormalizedBaseLevelBase = 57408u; +inline constexpr std::uint32_t FmaskFormatBase = 57472u; inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint32_t element) { return DescriptorIndexBase + binding * DescriptorIndexStride + element; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index e36217d958..83c1f217aa 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -4,6 +4,7 @@ #include "IntermediateRepresentation/IrMetadata.hpp" #include #include +#include #include namespace ShaderRecompiler { @@ -21,6 +22,17 @@ enum class ImageType : std::uint32_t { [[nodiscard]] IrTextureNumericClass VertexInputNumericClass(IrBufferFormat format); [[nodiscard]] bool IsFmaskTextureFormat(IrBufferFormat format); + +struct FmaskLayout { + std::uint32_t samples; + std::uint32_t fragments; + std::uint32_t sampleBits; + std::uint32_t storageBits; +}; + +inline constexpr IrBufferFormat FmaskExpansion = IrBufferFormat::Fmask8_S2_F1; + +[[nodiscard]] std::optional FmaskLayoutOf(IrBufferFormat format); [[nodiscard]] IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapTextureFormat(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapStorageTextureFormat(IrBufferFormat format); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 763deb31fb..acdbad34d0 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -112,6 +112,21 @@ bool IsFmaskTextureFormat(IrBufferFormat format) { return format >= IrBufferFormat::Fmask8_S2_F1 && format <= IrBufferFormat::Fmask64_S16_F8; } +std::optional FmaskLayoutOf(IrBufferFormat format) { + switch (format) { + case IrBufferFormat::Fmask8_S2_F1: return FmaskLayout{2u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S4_F1: return FmaskLayout{4u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S8_F1: return FmaskLayout{8u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S2_F2: return FmaskLayout{2u, 2u, 1u, 8u}; + case IrBufferFormat::Fmask8_S4_F2: return FmaskLayout{4u, 2u, 2u, 8u}; + case IrBufferFormat::Fmask8_S4_F4: return FmaskLayout{4u, 4u, 2u, 8u}; + case IrBufferFormat::Fmask16_S8_F2: return FmaskLayout{8u, 2u, 2u, 16u}; + case IrBufferFormat::Fmask32_S8_F4: return FmaskLayout{8u, 4u, 4u, 32u}; + case IrBufferFormat::Fmask32_S8_F8: return FmaskLayout{8u, 8u, 4u, 32u}; + default: return std::nullopt; + } +} + IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format) { const FormatInfo* info = FindFormatInfo(format); if (info == nullptr || !info->sampled) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 804aeda284..6b4c1b2d0d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -558,11 +558,47 @@ void RequireConvertedUnormAccess(SpirvValueEmitContext& ctx, const ImageEmitAcce } } +std::uint32_t ExpandFmaskTexel(SpirvEmitterState& state, const ImageEmitAccess& access, std::uint32_t texel) { + const auto resource = access.mem.resource; + const auto u32 = TypeU32(state); + const auto format = state.registrationModules + ? Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpShiftRightLogical, u32, RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 1u), ConstantU32(state, 20u)), ConstantU32(state, 0x1ffu)) + : state.module.SpecializationConstant(u32, PipelineSpecialization::FmaskFormatBase + resource, 0u); + auto layout = ConstantU32(state, 0u); + for (auto value = static_cast(IrBufferFormat::Fmask8_S2_F1); value <= static_cast(IrBufferFormat::Fmask64_S16_F8); ++value) { + const auto fmask = FmaskLayoutOf(static_cast(value)); + if (!fmask.has_value()) continue; + layout = Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), format, ConstantU32(state, value)), ConstantU32(state, fmask->sampleBits | (fmask->samples << 8u) | (fmask->fragments << 16u)), layout); + } + const auto sampleBits = Binary(state, spv::OpBitwiseAnd, u32, layout, ConstantU32(state, 0xffu)); + const auto samples = Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpShiftRightLogical, u32, layout, ConstantU32(state, 8u)), ConstantU32(state, 0xffu)); + const auto fragments = Binary(state, spv::OpShiftRightLogical, u32, layout, ConstantU32(state, 16u)); + const auto packed = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, packed, texel, 0u); + auto expanded = ConstantU32(state, 0u); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto field = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitFieldUExtract, u32, field, packed, Binary(state, spv::OpIMul, u32, sampleBits, ConstantU32(state, sample)), sampleBits); + const auto fragment = Select(state, u32, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), field, fragments), ConstantU32(state, 8u), field); + const auto nibble = Select(state, u32, Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, sample), samples), fragment, ConstantU32(state, 0u)); + expanded = Binary(state, spv::OpBitwiseOr, u32, expanded, Binary(state, spv::OpShiftLeftLogical, u32, nibble, ConstantU32(state, sample * 4u))); + } + const std::array components{expanded, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)}; + std::uint32_t selected[4] = {}; + for (std::uint32_t component = 0; component < 4u; component++) selected[component] = RuntimeSwizzledComponent(state, RuntimeImageSwizzle(state, resource, component), components); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), result, selected[0], selected[1], selected[2], selected[3]); + return result; +} + std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t texel) { auto& state = ctx.state; if (access.image.srgbDecode) { return DecodeSrgbTexel(state, texel); } + if (access.image.conversionFormat == FmaskExpansion) { + return ExpandFmaskTexel(state, access, texel); + } const auto info = ImageConversionFormat(access.image); if (info.format == IrBufferFormat::Invalid) { return texel; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index df60e02b6b..448f187bd3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -46,7 +46,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. -- Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read uncompressed `Fmask32_S8_F8` through a single-mip 2D or 2D-array `R32_UINT` snapshot with `Z_64KB_X` addressing. Destination selectors may name X, zero or one. Other FMASK formats, CMASK compression, packed accesses and other layouts throw. The layout follows PAL's [FMASK-to-surface conversion](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877) and [32-bit view selectors](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Device.cpp#L4973). Native RDNA/PS5 image-load results and title rendering are unmeasured; compressed color sampling remains unsupported. +- Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) of 16 samples, CMASK compression (bar the 8-fragment native owner path), packed accesses and other layouts throw. Uncompressed FMASK of up to 8 samples reads a single-mip 2D or 2D-array 8-, 16- or 32-bit `UINT` snapshot with `Z_64KB_X` addressing; selectors may name X, zero or one. `Fmask32_S8_F8` returns the stored word; others expand to 4 bits per sample, an index at or past the fragment count reading 8 (unknown), samples past the count 0. Layouts follow PAL's [expanded values](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MaskRam.cpp#L5005) and [FMASK surfaces](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877). Console results, unused nibbles included, are unmeasured; compressed color sampling is unsupported. - A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels.