From a64b8565b70573f024127b032acceafe547424d5 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 7 Oct 2026 21:37:01 -0400 Subject: [PATCH 1/5] feat(agc): load and store R10_G11_B11 float images Format 43 packs R as a 10-bit and G and B as 11-bit unsigned floats, which Vulkan has no format for. Keep it in an R32_UINT image like the other packed formats without a host equivalent, and convert texels in the shader on image loads and stores; sampling it still throws. --- .../Graphics/src/TextureFormat.cpp | 2 +- .../tests/execution/ImageConvertedUnorm.cpp | 57 ++++++++++++++++++- .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 5 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 16 ++++-- 4 files changed, 69 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 04fda0eda3b..d5978cf767c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -96,7 +96,7 @@ constexpr auto MakeFormatLookupTable() { constexpr auto kFormatLookupTable = MakeFormatLookupTable(); std::uint32_t remapGuestFormat(std::uint32_t guestFormat) { - return guestFormat == 30 || guestFormat == 34 ? 20 : guestFormat; + return guestFormat == 30 || guestFormat == 34 || guestFormat == 43 ? 20 : guestFormat; } const FormatEntry& findFormatEntry(std::uint32_t guestFormat) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index 37647310153..9db8517c668 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -34,6 +34,8 @@ constexpr std::uint32_t LoadGroups = LoadWidth / Threads; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t UnormFormat = 30; constexpr std::uint32_t UintFormat = 34; +constexpr std::uint32_t FloatFormat = 43; +constexpr std::size_t FloatOffset = 40960; constexpr std::uint32_t SwizzleXYZ1 = 0x3acu; constexpr std::uint32_t SwizzleXZY1 = 0x374u; constexpr std::uint32_t SwizzleZYX1 = 0x32eu; @@ -249,14 +251,60 @@ void CheckStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span } } -void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array& extra, const std::string& reason, const std::string& what) { +constexpr std::array FloatValues{0.0f, 1.0f, 0.5f, 2.0f, 0.25f, 3.0f, 1.5f, 10.0f, 0.75f, 6.5f, 0.125f}; + +std::uint32_t SmallFloat(float value, std::uint32_t mantissaBits) { + if (value == 0.0f) return 0u; + const auto bits = std::bit_cast(value); + const auto exponent = ((bits >> 23u) & 0xffu) - 127u + 15u; + return (exponent << mantissaBits) | ((bits & 0x7fffffu) >> (23u - mantissaBits)); +} + +std::uint32_t FloatTexel(std::uint32_t index) { + return SmallFloat(FloatValues[index % 11u], 5u) | (SmallFloat(FloatValues[(index + 4u) % 11u], 6u) << 10u) | (SmallFloat(FloatValues[(index + 8u) % 11u], 6u) << 21u); +} + +void CheckFloatLoad(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + for (std::uint32_t index = 0; index < LoadWidth; ++index) { + const auto texel = FloatTexel(index); + std::memcpy(texels + index * 4u, &texel, 4u); + } + Run(device, LoadXyzw, TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, LoadWidth), {}, LoadGroups); + for (std::uint32_t index = 0; index < LoadWidth; ++index) { + const std::array expected{FloatValues[index % 11u], FloatValues[(index + 4u) % 11u], FloatValues[(index + 8u) % 11u], 1.0f}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto actual = Output[index * 4u + component]; + Require(actual == std::bit_cast(expected[component]), "image_load of R10_G11_B11_FLOAT: texel " + std::to_string(index) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(std::bit_cast(expected[component]))); + } + } +} + +void CheckFloatStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + std::fill(texels, texels + Threads * 4u, static_cast(0xa5u)); + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 4u + 0u] = std::bit_cast(FloatValues[tid % 11u]); + Input[tid * 4u + 1u] = std::bit_cast(FloatValues[(tid + 4u) % 11u]); + Input[tid * 4u + 2u] = std::bit_cast(FloatValues[(tid + 8u) % 11u]); + } + Run(device, StoreXyz, TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, Threads), BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)), 1); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), Threads * 4u, nullptr, "test"); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + std::uint32_t actual = 0; + std::memcpy(&actual, texels + tid * 4u, 4u); + Require(actual == FloatTexel(tid), "image_store of R10_G11_B11_FLOAT: texel " + std::to_string(tid) + " is " + Hex(actual) + ", expected " + Hex(FloatTexel(tid))); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array& extra, const std::string& reason, const std::string& what, std::uint32_t format = UnormFormat) { std::string refusal; try { - Run(device, code, TextureDescriptor(texels, UnormFormat, swizzle, LoadWidth), extra, 1); + Run(device, code, TextureDescriptor(texels, format, swizzle, LoadWidth), extra, 1); } catch (const std::exception& error) { refusal = error.what(); } - Require(refusal.find(reason) != std::string::npos, what + " of a 10_11_11_UNORM image was not refused: " + refusal); + Require(refusal.find(reason) != std::string::npos, what + " of a converted image was not refused: " + refusal); } } @@ -279,6 +327,9 @@ int main() { CheckRoundTrip(*device, texels); CheckStore(*device, texels + StoreOffset, StoreXyz, 0x7u, "image_store dmask:0x7"); CheckStore(*device, texels + StoreOffset, StoreXz, 0x5u, "image_store dmask:0x5"); + CheckFloatStore(*device, texels + FloatOffset); + CheckFloatLoad(*device, texels + FloatOffset); + RequireRefused(*device, SampleLz, texels + FloatOffset, SwizzleXYZ1, PointSampler, "samples or gathers a converted float image", "image_sample_lz of R10_G11_B11_FLOAT", FloatFormat); RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_sample_lz"); RequireRefused(*device, Gather4Lz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_gather4_lz"); RequireRefused(*device, GetLod, texels, SwizzleXYZ1, PointSampler, "queries the level of detail of a converted unorm image", "image_get_lod"); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 21faa359af1..a9cc292e6aa 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -13,7 +13,7 @@ struct FormatInfo { bool sint32; }; -constexpr std::array kFormatInfoTable {{ +constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format8UNorm, true, false, false}, {IrBufferFormat::Format8SNorm, false, false, false}, {IrBufferFormat::Format8UInt, true, true, false}, @@ -38,6 +38,7 @@ constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format11_11_10UNorm, true, true, false}, {IrBufferFormat::Format11_11_10UInt, true, true, false}, {IrBufferFormat::Format11_11_10Float, true, false, false}, + {IrBufferFormat::Format10_11_11Float, true, true, false}, {IrBufferFormat::Format10_10_10_2UNorm, true, false, false}, {IrBufferFormat::Format10_10_10_2UInt, true, true, false}, {IrBufferFormat::Format8_8_8_8UNorm, true, false, false}, @@ -120,7 +121,7 @@ IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format) { } IrBufferFormat RemapTextureFormat(IrBufferFormat format) { - return format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format11_11_10UInt ? IrBufferFormat::Format32UInt : format; + return format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format11_11_10UInt || format == IrBufferFormat::Format10_11_11Float ? IrBufferFormat::Format32UInt : format; } std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 9c8599186e8..9c112c83172 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -477,15 +477,15 @@ SpirvBufferFormatInfo ImageConversionFormat(const ImageResource& image) { return {}; } const auto info = GetFormatInfo(format); - if (SampledTextureNumericClass(format) != IrTextureNumericClass::Uint || RemapTextureFormat(format) == format || (info.type != SpirvFormatComponentType::Uint && info.type != SpirvFormatComponentType::Unorm) || !info.packedBitfield || info.byteSize != sizeof(std::uint32_t) || info.componentCount == 0u || info.componentCount > 4u) { - throw std::runtime_error("image conversion format is not a packed 32-bit unsigned integer or unorm format"); + if (SampledTextureNumericClass(format) != IrTextureNumericClass::Uint || RemapTextureFormat(format) == format || (info.type != SpirvFormatComponentType::Uint && info.type != SpirvFormatComponentType::Unorm && info.type != SpirvFormatComponentType::Float) || !info.packedBitfield || info.byteSize != sizeof(std::uint32_t) || info.componentCount == 0u || info.componentCount > 4u) { + throw std::runtime_error("image conversion format is not a packed 32-bit unsigned integer, unorm or float format"); } return info; } void RequireConvertedUnormAccess(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SpirvBufferFormatInfo& info) { if (access.mem.dataBits == 16u) { - ctx.Fail(access.inst, "reads or writes a converted unorm image with 16-bit data, which is not implemented"); + ctx.Fail(access.inst, info.type == SpirvFormatComponentType::Float ? "reads or writes a converted float image with 16-bit data, which is not implemented" : "reads or writes a converted unorm image with 16-bit data, which is not implemented"); } for (std::uint32_t component = 0; component < 4u; component++) { const auto selector = (access.image.shaderSwizzle >> (component * 3u)) & 7u; @@ -504,7 +504,7 @@ std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess if (info.format == IrBufferFormat::Invalid) { return texel; } - const bool unorm = info.type == SpirvFormatComponentType::Unorm; + const bool unorm = info.type == SpirvFormatComponentType::Unorm || info.type == SpirvFormatComponentType::Float; if (unorm) { RequireConvertedUnormAccess(ctx, access, info); } @@ -674,7 +674,7 @@ std::uint32_t PackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& if (info.format == IrBufferFormat::Invalid) { return texel; } - const bool unorm = info.type == SpirvFormatComponentType::Unorm; + const bool unorm = info.type == SpirvFormatComponentType::Unorm || info.type == SpirvFormatComponentType::Float; if (unorm) { RequireConvertedUnormAccess(ctx, access, info); } @@ -838,6 +838,9 @@ void EmitQueryLodOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { ctx.Fail(access.inst, "queries the level of detail of a converted unorm image, which is not implemented"); } + if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Float) { + ctx.Fail(access.inst, "queries the level of detail of a converted float image, which is not implemented"); + } state.module.EmitCapability(spv::CapabilityImageQuery); const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler, access.slot); const auto coord = CoordF32(ctx, access, 0, RdnaImageDimensionInfoFor(access.image.dimension).spatialComponents, AddressDimension(access).spatialComponents); @@ -960,6 +963,9 @@ SampleSetup MakeSampleSetup(SpirvValueEmitContext& ctx, const ImageEmitAccess& a if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { ctx.Fail(access.inst, "samples or gathers a converted unorm image, which needs filtering in the shader and is not implemented"); } + if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Float) { + ctx.Fail(access.inst, "samples or gathers a converted float image, which needs filtering in the shader and is not implemented"); + } const auto coord = CoordF32(ctx, access, layout.coord, dimensionInfo.coordinateComponents, AddressDimension(access).coordinateComponents); return {dimensionInfo, layout, access.image.numericClass, dref, coord}; } From b1ec4a7b55b1faa07b2234c2d61f1078f3e5ff2f Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 7 Oct 2026 21:57:55 -0400 Subject: [PATCH 2/5] feat(agc): filter converted unorm and float images in the shader Images kept in an R32_UINT view (10/11-bit unorm and float formats) cannot be filtered by the host sampler, so sampling them threw. Fetch and unpack the texels in the shader and filter them there: point or bilinear, with wrap, mirror, clamp-to-edge, clamp-to-half-border or clamp-to-border addressing and the built-in border colors, on single-level images, with _lz, or with an explicit LOD selecting or blending mip levels. These images no longer need a point-filtered copy of their sampler. --- .../tests/execution/ImageConvertedUnorm.cpp | 41 ++++- .../IrMetadata/Resources.hpp | 26 +++ .../Optimization/ResourceMaterializer.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 51 +++++- core/shader/recompiler/ShaderDiskCache.cpp | 3 + core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 155 +++++++++++++++++- 7 files changed, 272 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index 9db8517c668..5b7a6eae5d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -88,6 +88,15 @@ alignas(256) constexpr auto StoreD16 = StoreCode(0x7u, D16); alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u); alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u); alignas(256) constexpr auto GetLod = SamplerCode(0xf1800308u); +alignas(256) constexpr std::array SpreadSampleLz{ + 0x34060084u, 0x7e280d00u, 0x062828ffu, 0x3e800000u, 0x102828ffu, 0x3d000000u, 0x7e2a02f0u, 0xf09c0f08u, + 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u, +}; +constexpr std::array WrapBilinearSampler{0x00000000u, 0x00fff000u, 0x00500000u, 0u}; +constexpr std::array EdgeBilinearSampler{0x00000092u, 0x00fff000u, 0x00500000u, 0u}; +constexpr std::array MirrorBilinearSampler{0x00000049u, 0x00fff000u, 0x00500000u, 0u}; +constexpr std::array HalfBorderBilinearSampler{0x00000124u, 0x00fff000u, 0x00500000u, 0x80000000u}; +constexpr std::array MirrorOnceSampler{0x000000dbu, 0x00fff000u, 0u, 0u}; constexpr std::array StoreValues{ 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3eaaaaabu, 0x80000000u, 0xbf800000u, @@ -297,6 +306,29 @@ void CheckFloatStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { } } +void CheckFloatSample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const std::array& sampler, bool linear, bool wrap, const std::string& what, bool whiteBorder = false) { + for (std::uint32_t index = 0; index < Threads; ++index) { + const auto texel = FloatTexel(index); + std::memcpy(texels + index * 4u, &texel, 4u); + } + Run(device, SpreadSampleLz, TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, Threads), sampler, 1); + const auto value = [](std::int32_t index, std::uint32_t component) { + return component == 3u ? 1.0f : FloatValues[(static_cast(index) + component * 4u) % 11u]; + }; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + for (std::uint32_t component = 0; component < 4u; ++component) { + float expected = value(static_cast(tid), component); + if (linear) { + const auto left = tid == 0u ? (wrap ? static_cast(Threads) - 1 : 0) : static_cast(tid) - 1; + const float leftValue = tid == 0u && whiteBorder ? 1.0f : value(left, component); + expected = leftValue * 0.25f + value(static_cast(tid), component) * 0.75f; + } + const float actual = std::bit_cast(Output[tid * 4u + component]); + Require(std::fabs(actual - expected) <= 1e-5f * std::max(1.0f, std::fabs(expected)), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } + } +} + void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array& extra, const std::string& reason, const std::string& what, std::uint32_t format = UnormFormat) { std::string refusal; try { @@ -329,8 +361,13 @@ int main() { CheckStore(*device, texels + StoreOffset, StoreXz, 0x5u, "image_store dmask:0x5"); CheckFloatStore(*device, texels + FloatOffset); CheckFloatLoad(*device, texels + FloatOffset); - RequireRefused(*device, SampleLz, texels + FloatOffset, SwizzleXYZ1, PointSampler, "samples or gathers a converted float image", "image_sample_lz of R10_G11_B11_FLOAT", FloatFormat); - RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_sample_lz"); + CheckFloatSample(*device, texels + FloatOffset, PointSampler, false, false, "image_sample_lz of R10_G11_B11_FLOAT, point, clamp to edge"); + CheckFloatSample(*device, texels + FloatOffset, EdgeBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, clamp to edge"); + CheckFloatSample(*device, texels + FloatOffset, WrapBilinearSampler, true, true, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, wrap"); + RequireRefused(*device, Gather4Lz, texels + FloatOffset, SwizzleXYZ1, PointSampler, "samples or gathers a converted float image", "image_gather4_lz of R10_G11_B11_FLOAT", FloatFormat); + CheckFloatSample(*device, texels + FloatOffset, MirrorBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, mirror"); + CheckFloatSample(*device, texels + FloatOffset, HalfBorderBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, half border, white", true); + RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, MirrorOnceSampler, "clamp-to-half-border or clamp-to-border addressing", "image_sample_lz with mirror-once addressing"); RequireRefused(*device, Gather4Lz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_gather4_lz"); RequireRefused(*device, GetLod, texels, SwizzleXYZ1, PointSampler, "queries the level of detail of a converted unorm image", "image_get_lod"); RequireRefused(*device, LoadD16, texels, SwizzleXYZ1, {}, "converted unorm image with 16-bit data", "image_load d16"); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index a25930f11fb..40768f70e5f 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -54,6 +54,31 @@ inline constexpr std::uint32_t ReferenceSnorm = 2u; [[nodiscard]] inline std::uint32_t Reference(std::uint32_t state) { return (state >> ReferenceShift) & 0x3u; } } +namespace EmulatedFilter { +inline constexpr std::uint32_t Enabled = 1u << 0u; +inline constexpr std::uint32_t Linear = 1u << 1u; +inline constexpr std::uint32_t ClampXShift = 2u; +inline constexpr std::uint32_t ClampYShift = 5u; +inline constexpr std::uint32_t SingleLevel = 1u << 8u; +inline constexpr std::uint32_t MipShift = 9u; +inline constexpr std::uint32_t BorderShift = 11u; +inline constexpr std::uint32_t AddressWrap = 0u; +inline constexpr std::uint32_t AddressEdge = 1u; +inline constexpr std::uint32_t AddressMirror = 2u; +inline constexpr std::uint32_t AddressBorder = 3u; +inline constexpr std::uint32_t AddressHalfBorder = 4u; +inline constexpr std::uint32_t MipBase = 0u; +inline constexpr std::uint32_t MipPoint = 1u; +inline constexpr std::uint32_t MipLinear = 2u; +inline constexpr std::uint32_t BorderTransparentBlack = 0u; +inline constexpr std::uint32_t BorderOpaqueBlack = 1u; +inline constexpr std::uint32_t BorderOpaqueWhite = 2u; +[[nodiscard]] inline std::uint32_t Mip(std::uint32_t state) { return (state >> MipShift) & 0x3u; } +[[nodiscard]] inline std::uint32_t AddressX(std::uint32_t state) { return (state >> ClampXShift) & 0x7u; } +[[nodiscard]] inline std::uint32_t AddressY(std::uint32_t state) { return (state >> ClampYShift) & 0x7u; } +[[nodiscard]] inline std::uint32_t Border(std::uint32_t state) { return (state >> BorderShift) & 0x3u; } +} + struct ImageResource { static constexpr std::uint32_t NoIndirectImage = std::numeric_limits::max(); @@ -79,6 +104,7 @@ struct ImageResource { bool packed = false; IrBufferFormat packedFormat = IrBufferFormat::Invalid; std::uint32_t emulatedCompare = 0; + std::uint32_t emulatedFilter = 0; std::uint32_t indirectRoot = NoIndirectImage; std::uint32_t indirectMappingOffset = 0; std::uint32_t indirectSearchIterations = 0; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index aed4b1d8330..06a8f38595f 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -33,6 +33,7 @@ struct ResourceSpecialization { bool depthUnorm16 = false; IrBufferFormat packedFormat = IrBufferFormat::Invalid; std::uint32_t emulatedCompare = 0; + std::uint32_t emulatedFilter = 0; bool srgbDecode = false; bool operator==(const Image& other) const; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 08c9b15958b..cf5efacdbfd 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -201,7 +201,7 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag } bool requiresPointSampler(const ResourceSpecialization::Image& image) { - return image.numericClass == IrTextureNumericClass::Sint || image.conversionFormat != IrBufferFormat::Invalid || image.depthBits; + return image.numericClass == IrTextureNumericClass::Sint || (image.conversionFormat != IrBufferFormat::Invalid && (image.emulatedFilter & EmulatedFilter::Enabled) == 0u) || image.depthBits; } constexpr std::uint32_t TableEntryBytes = 32; @@ -572,6 +572,51 @@ std::uint32_t emulatedCompareState(const IrResourcePlan& plan, const ResourceSna return *samplerState | (reference << EmulatedCompare::ReferenceShift) | (singleLevel ? EmulatedCompare::SingleLevel : 0u); } +std::uint32_t emulatedFilterState(const IrResourcePlan& plan, const ResourceSnapshot& snapshot, std::uint32_t index, IrBufferFormat conversionFormat) { + if (conversionFormat != IrBufferFormat::Format11_11_10UNorm && conversionFormat != IrBufferFormat::Format10_11_11Float) return 0u; + std::optional filterState; + for (const auto& pair : plan.info.sampledPairs) { + if (pair.image != index) continue; + if (pair.sampler >= snapshot.samplers.size() || snapshot.samplers[pair.sampler].dwordCount != 4u) throw std::runtime_error("filtering a converted image in the shader has no sampler descriptor"); + const auto& words = snapshot.samplers[pair.sampler].dwords; + const auto clampX = words[0] & 0x7u; + const auto clampY = (words[0] >> 3u) & 0x7u; + const bool unnormalized = ((words[0] >> 15u) & 0x1u) != 0u; + if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("filtering a converted image in the shader through a min or max reduction sampler is not implemented"); + const auto magFilter = (words[2] >> 20u) & 0x3u; + const auto minFilter = (words[2] >> 22u) & 0x3u; + const auto addressMode = [](std::uint32_t clamp) { + if (clamp == 0u) return EmulatedFilter::AddressWrap; + if (clamp == 1u) return EmulatedFilter::AddressMirror; + if (clamp == 2u) return EmulatedFilter::AddressEdge; + if (clamp == 4u) return EmulatedFilter::AddressHalfBorder; + if (clamp == 6u) return EmulatedFilter::AddressBorder; + throw std::runtime_error("filtering a converted image in the shader is implemented only with wrap, mirror, clamp-to-edge, clamp-to-half-border or clamp-to-border addressing"); + }; + const auto addressX = addressMode(clampX); + const auto addressY = addressMode(clampY); + if (magFilter != minFilter || magFilter > 1u) throw std::runtime_error("filtering a converted image in the shader is implemented only with equal point or bilinear minification and magnification filters"); + if (unnormalized) throw std::runtime_error("filtering a converted image in the shader does not implement unnormalized coordinates"); + const auto borderType = (words[3] >> 30u) & 0x3u; + const bool border = addressX == EmulatedFilter::AddressBorder || addressX == EmulatedFilter::AddressHalfBorder || addressY == EmulatedFilter::AddressBorder || addressY == EmulatedFilter::AddressHalfBorder; + if (border && borderType == 3u) throw std::runtime_error("filtering a converted image in the shader with a border color table is not implemented"); + const auto mipFilter = (words[2] >> 24u) & 0x3u; + const auto mip = mipFilter == 1u ? EmulatedFilter::MipPoint : mipFilter == 2u ? EmulatedFilter::MipLinear : EmulatedFilter::MipBase; + const auto& descriptor = snapshot.images[index]; + const auto baseLevel = (descriptor.dwords[3] >> 12u) & 0xfu; + const auto lastLevel = (descriptor.dwords[3] >> 16u) & 0xfu; + const bool lodClamped = (words[2] & 0x3fffu) != 0u || (words[1] & 0xfffu) != 0u || ((words[1] >> 12u) & 0xfffu) < (lastLevel - baseLevel) * 256u || ((descriptor.dwords[1] >> 8u) & 0xfffu) > baseLevel * 256u; + if (lastLevel > baseLevel && mip != EmulatedFilter::MipBase && lodClamped) throw std::runtime_error("filtering a converted image in the shader across mip levels with a LOD bias or clamp is not implemented"); + const auto state = EmulatedFilter::Enabled | (magFilter == 1u ? EmulatedFilter::Linear : 0u) | (addressX << EmulatedFilter::ClampXShift) | (addressY << EmulatedFilter::ClampYShift) | (mip << EmulatedFilter::MipShift) | ((border ? borderType : 0u) << EmulatedFilter::BorderShift); + if (filterState.has_value() && *filterState != state) throw std::runtime_error("filtering a converted image in the shader through samplers that disagree is not implemented"); + filterState = state; + } + if (!filterState.has_value()) return 0u; + const auto& descriptor = snapshot.images[index]; + const bool singleLevel = ((descriptor.dwords[3] >> 12u) & 0xfu) == ((descriptor.dwords[3] >> 16u) & 0xfu); + return *filterState | (singleLevel ? EmulatedFilter::SingleLevel : 0u); +} + void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& snapshot, const std::vector& tables, ResourceSpecialization& specialization) { ResourceSpecialization result; result.buffers.reserve(plan.info.buffers.size()); @@ -619,6 +664,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.depthUnorm16 = decoded.depthUnorm16; entry.packedFormat = decoded.packedFormat; entry.emulatedCompare = emulatedCompareState(plan, snapshot, i); + entry.emulatedFilter = emulatedFilterState(plan, snapshot, i, decoded.conversionFormat); entry.srgbDecode = decoded.srgbDecode; result.images.push_back(entry); } @@ -708,6 +754,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio image.depthUnorm16 = source.depthUnorm16; image.packedFormat = source.packedFormat; image.emulatedCompare = source.emulatedCompare; + image.emulatedFilter = source.emulatedFilter; image.srgbDecode = source.srgbDecode; if ((source.emulatedCompare & EmulatedCompare::Enabled) != 0u) image.depthCompare = false; image.indirectResources.clear(); @@ -1039,7 +1086,7 @@ bool ResourceSpecialization::Buffer::operator==(const Buffer& other) const { } bool ResourceSpecialization::Image::operator==(const Image& other) const { - return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits && depthUnorm16 == other.depthUnorm16 && packedFormat == other.packedFormat && emulatedCompare == other.emulatedCompare && srgbDecode == other.srgbDecode; + return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits && depthUnorm16 == other.depthUnorm16 && packedFormat == other.packedFormat && emulatedCompare == other.emulatedCompare && emulatedFilter == other.emulatedFilter && srgbDecode == other.srgbDecode; } bool ResourceSpecialization::operator==(const ResourceSpecialization& other) const { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ed87d113e6d..6d889ac173f 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -419,6 +419,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(image.packed); out.Value(image.packedFormat); out.Value(image.emulatedCompare); + out.Value(image.emulatedFilter); out.Value(image.indirectRoot); out.Value(image.indirectMappingOffset); out.Value(image.indirectSearchIterations); @@ -512,6 +513,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.packed); in.Value(image.packedFormat); in.Value(image.emulatedCompare); + in.Value(image.emulatedFilter); in.Value(image.indirectRoot); in.Value(image.indirectMappingOffset); in.Value(image.indirectSearchIterations); @@ -829,6 +831,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, c out.Value(image.depthUnorm16); out.Value(image.packedFormat); out.Value(image.emulatedCompare); + out.Value(image.emulatedFilter); out.Value(image.srgbDecode); }); writer.Values(std::span(specialization.boundDescriptors)); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 01a2e20bbfc..29e244b46db 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 7; +inline constexpr std::uint32_t FormatVersion = 8; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 9c112c83172..8685f87dc7e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -960,10 +960,11 @@ SampleSetup MakeSampleSetup(SpirvValueEmitContext& ctx, const ImageEmitAccess& a if (dref && access.image.conversionFormat != IrBufferFormat::Invalid) { ctx.Fail(access.inst, "uses depth comparison with a packed integer image"); } - if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { + const bool filtered = (access.image.emulatedFilter & EmulatedFilter::Enabled) != 0u && access.inst.Opcode() != IrOpcode::ImageGatherRaw; + if (!filtered && ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { ctx.Fail(access.inst, "samples or gathers a converted unorm image, which needs filtering in the shader and is not implemented"); } - if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Float) { + if (!filtered && ImageConversionFormat(access.image).type == SpirvFormatComponentType::Float) { ctx.Fail(access.inst, "samples or gathers a converted float image, which needs filtering in the shader and is not implemented"); } const auto coord = CoordF32(ctx, access, layout.coord, dimensionInfo.coordinateComponents, AddressDimension(access).coordinateComponents); @@ -1181,6 +1182,152 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, result, setup.numericClass, true, false))); } +void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { + auto& state = ctx.state; + const auto& mem = access.mem; + const auto& image = access.image; + const auto filter = image.emulatedFilter; + if (access.slot != 0) ctx.Fail(access.inst, "filters a converted image through a bindless image table, which is not implemented"); + const bool baseOnly = HasFlag(mem, RdnaImageSampleFlagLevelZero) || (filter & EmulatedFilter::SingleLevel) != 0u || EmulatedFilter::Mip(filter) == EmulatedFilter::MipBase; + if (!baseOnly && (!HasFlag(mem, RdnaImageSampleFlagLod) || setup.layout.clamp != NoImageComponent)) ctx.Fail(access.inst, "filters a converted image across mip levels without an explicit, unclamped LOD, which is not implemented"); + const bool arrayed = image.dimension == RdnaImageDimension::Dim2DArray; + if (image.dimension != RdnaImageDimension::Dim2D && !arrayed) ctx.Fail(access.inst, "filters a converted image that is not a 2D or 2D array view, which is not implemented"); + const auto f32 = TypeF32(state); + const auto i32 = TypeI32(state); + const auto u32 = TypeU32(state); + const auto f32x4 = TypeF32Vector(state, 4); + const auto ext = [&](std::uint32_t type, std::uint32_t op, std::initializer_list args) { + const auto value = state.module.AllocateId(); + std::vector words{spv::OpExtInst, type, value, GlslStd450(state), op}; + words.insert(words.end(), args.begin(), args.end()); + state.module.AddFunction(std::span(words)); + return value; + }; + const auto extract = [&](std::uint32_t type, std::uint32_t composite, std::uint32_t index) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, type, value, composite, index); + return value; + }; + const auto descriptor = LoadSampledImageDescriptor(state, mem.resource, access.slot); + state.module.EmitCapability(spv::CapabilityImageQuery); + const auto levelSize = [&](std::uint32_t level) { + const auto size = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageQuerySizeLod, ImageViewSizeType(state, image.dimension), size, descriptor, level); + return size; + }; + auto layer = ConstantU32(state, 0u); + if (arrayed) { + const auto layers = Unary(state, spv::OpBitcast, i32, extract(u32, levelSize(ConstantU32(state, 0u)), 2u)); + const auto rounded = Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {Binary(state, spv::OpFAdd, f32, extract(f32, setup.coord, 2u), ConstantF32(state, 0x3f000000u))})); + layer = Unary(state, spv::OpBitcast, u32, ext(i32, GLSLstd450SClamp, {rounded, ConstantI32(state, 0), Binary(state, spv::OpISub, i32, layers, ConstantI32(state, 1))})); + } + const auto address = [&](std::uint32_t index, std::uint32_t extent, std::uint32_t mode) { + if (mode == EmulatedFilter::AddressWrap) return Binary(state, spv::OpSMod, i32, index, extent); + if (mode == EmulatedFilter::AddressMirror) { + const auto period = Binary(state, spv::OpIAdd, i32, extent, extent); + const auto folded = Binary(state, spv::OpSMod, i32, index, period); + const auto reflected = Binary(state, spv::OpISub, i32, Binary(state, spv::OpISub, i32, period, ConstantI32(state, 1)), folded); + return Select(state, i32, Binary(state, spv::OpSLessThan, TypeBool(state), folded, extent), folded, reflected); + } + return ext(i32, GLSLstd450SClamp, {index, ConstantI32(state, 0), Binary(state, spv::OpISub, i32, extent, ConstantI32(state, 1))}); + }; + const bool linear = (filter & EmulatedFilter::Linear) != 0u; + const auto bordered = [&](std::uint32_t mode) { return mode == EmulatedFilter::AddressBorder || (linear && mode == EmulatedFilter::AddressHalfBorder); }; + const auto borderValue = [&]() { + const auto one = ConstantF32(state, 0x3f800000u); + const auto zero = ConstantF32(state, 0u); + const auto colour = EmulatedFilter::Border(filter) == EmulatedFilter::BorderOpaqueWhite ? one : zero; + const auto alpha = EmulatedFilter::Border(filter) == EmulatedFilter::BorderTransparentBlack ? zero : one; + const auto vector = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, f32x4, vector, colour, colour, colour, alpha); + return vector; + }; + const auto inside = [&](std::uint32_t index, std::uint32_t extent) { + const auto notBelow = Binary(state, spv::OpSGreaterThanEqual, TypeBool(state), index, ConstantI32(state, 0)); + const auto below = Binary(state, spv::OpSLessThan, TypeBool(state), index, extent); + return Binary(state, spv::OpLogicalAnd, TypeBool(state), notBelow, below); + }; + const auto fetch = [&](std::uint32_t level, std::uint32_t width, std::uint32_t height, std::uint32_t x, std::uint32_t y) { + const auto ux = Unary(state, spv::OpBitcast, u32, address(x, width, EmulatedFilter::AddressX(filter))); + const auto uy = Unary(state, spv::OpBitcast, u32, address(y, height, EmulatedFilter::AddressY(filter))); + const auto coord = state.module.AllocateId(); + if (arrayed) state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3), coord, ux, uy, layer); + else state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2), coord, ux, uy); + const auto texel = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageFetch, ImageVectorType(state, image.numericClass, 4), texel, descriptor, coord, spv::ImageOperandsLodMask, level); + const auto value = Unary(state, spv::OpBitcast, f32x4, UnpackImageTexel(ctx, access, texel)); + std::uint32_t outside = 0u; + if (bordered(EmulatedFilter::AddressX(filter))) outside = Unary(state, spv::OpLogicalNot, TypeBool(state), inside(x, width)); + if (bordered(EmulatedFilter::AddressY(filter))) { + const auto outsideY = Unary(state, spv::OpLogicalNot, TypeBool(state), inside(y, height)); + outside = outside == 0u ? outsideY : Binary(state, spv::OpLogicalOr, TypeBool(state), outside, outsideY); + } + if (outside == 0u) return value; + const auto condition = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeBoolVector(state, 4), condition, outside, outside, outside, outside); + return Select(state, f32x4, condition, borderValue(), value); + }; + const auto splat = [&](std::uint32_t value) { + const auto vector = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, f32x4, vector, value, value, value, value); + return vector; + }; + auto offsetX = ConstantI32(state, 0); + auto offsetY = ConstantI32(state, 0); + if (setup.layout.offset != NoImageComponent) { + const auto offset = PackedOffset(ctx, access, setup.layout); + offsetX = extract(i32, offset, 0u); + offsetY = extract(i32, offset, 1u); + } + const auto filtered = [&](std::uint32_t level) { + const auto size = levelSize(level); + const auto width = Unary(state, spv::OpBitcast, i32, extract(u32, size, 0u)); + const auto height = Unary(state, spv::OpBitcast, i32, extract(u32, size, 1u)); + auto scaledU = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 0u), Unary(state, spv::OpConvertSToF, f32, width)); + auto scaledV = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 1u), Unary(state, spv::OpConvertSToF, f32, height)); + if (EmulatedFilter::AddressX(filter) == EmulatedFilter::AddressHalfBorder) scaledU = ext(f32, GLSLstd450FClamp, {scaledU, ConstantF32(state, 0u), Unary(state, spv::OpConvertSToF, f32, width)}); + if (EmulatedFilter::AddressY(filter) == EmulatedFilter::AddressHalfBorder) scaledV = ext(f32, GLSLstd450FClamp, {scaledV, ConstantF32(state, 0u), Unary(state, spv::OpConvertSToF, f32, height)}); + if (!linear) { + const auto x = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledU})), offsetX); + const auto y = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledV})), offsetY); + return fetch(level, width, height, x, y); + } + const auto half = ConstantF32(state, 0x3f000000u); + const auto centreU = Binary(state, spv::OpFSub, f32, scaledU, half); + const auto centreV = Binary(state, spv::OpFSub, f32, scaledV, half); + const auto floorU = ext(f32, GLSLstd450Floor, {centreU}); + const auto floorV = ext(f32, GLSLstd450Floor, {centreV}); + const auto weightU = splat(Binary(state, spv::OpFSub, f32, centreU, floorU)); + const auto weightV = splat(Binary(state, spv::OpFSub, f32, centreV, floorV)); + const auto x0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorU), offsetX); + const auto y0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorV), offsetY); + const auto x1 = Binary(state, spv::OpIAdd, i32, x0, ConstantI32(state, 1)); + const auto y1 = Binary(state, spv::OpIAdd, i32, y0, ConstantI32(state, 1)); + const auto top = ext(f32x4, GLSLstd450FMix, {fetch(level, width, height, x0, y0), fetch(level, width, height, x1, y0), weightU}); + const auto bottom = ext(f32x4, GLSLstd450FMix, {fetch(level, width, height, x0, y1), fetch(level, width, height, x1, y1), weightU}); + return ext(f32x4, GLSLstd450FMix, {top, bottom, weightV}); + }; + std::uint32_t result; + if (baseOnly) { + result = filtered(ConstantU32(state, 0u)); + } else { + const auto levels = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageQueryLevels, u32, levels, descriptor); + const auto top = Unary(state, spv::OpConvertUToF, f32, Binary(state, spv::OpISub, u32, levels, ConstantU32(state, 1u))); + const auto lod = ext(f32, GLSLstd450FClamp, {AddressF32(ctx, access, setup.layout.lod), ConstantF32(state, 0u), top}); + if (EmulatedFilter::Mip(filter) == EmulatedFilter::MipPoint) { + result = filtered(Unary(state, spv::OpConvertFToU, u32, ext(f32, GLSLstd450Floor, {Binary(state, spv::OpFAdd, f32, lod, ConstantF32(state, 0x3f000000u))}))); + } else { + const auto lower = ext(f32, GLSLstd450Floor, {lod}); + const auto upper = ext(f32, GLSLstd450FMin, {Binary(state, spv::OpFAdd, f32, lower, ConstantF32(state, 0x3f800000u)), top}); + const auto weight = splat(Binary(state, spv::OpFSub, f32, lod, lower)); + result = ext(f32x4, GLSLstd450FMix, {filtered(Unary(state, spv::OpConvertFToU, u32, lower)), filtered(Unary(state, spv::OpConvertFToU, u32, upper)), weight}); + } + } + const auto bits = Unary(state, spv::OpBitcast, TypeU32Vector(state, 4), result); + ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, bits, setup.numericClass, false, false))); +} + void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { auto& state = ctx.state; const auto& mem = access.mem; @@ -1189,6 +1336,10 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con EmitEmulatedCompareSample(ctx, access, setup); return; } + if ((image.emulatedFilter & EmulatedFilter::Enabled) != 0u) { + EmitEmulatedFilterSample(ctx, access, setup); + return; + } const bool explicitLod = ImageSampleExplicitLod(mem.imageSampleFlags, state.program.Resources().stage); std::uint32_t opcode = spv::OpImageSampleImplicitLod; if (explicitLod) { From d9cd95ee21e73b3e9f375dc361c103dce8976566 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 7 Oct 2026 22:19:44 -0400 Subject: [PATCH 3/5] fix(agc): update ResourceSpecialization::Image size assert for emulatedFilter Co-Authored-By: Claude Sonnet 5.5 --- core/shader/recompiler/ShaderDiskCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 6d889ac173f..11c5c885869 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -80,7 +80,7 @@ static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: u static_assert(sizeof(BindingAllocationResult) == 120, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(ResourceSpecialization) == 72, "ResourceSpecialization changed: update BuildKey"); static_assert(sizeof(ResourceSpecialization::Buffer) == 16, "ResourceSpecialization::Buffer changed: update BuildKey"); -static_assert(sizeof(ResourceSpecialization::Image) == 48, "ResourceSpecialization::Image changed: update BuildKey"); +static_assert(sizeof(ResourceSpecialization::Image) == 52, "ResourceSpecialization::Image changed: update BuildKey"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); #endif From a88f9f5acd15aa2a04391b4a6267b1a377310c58 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 7 Oct 2026 23:07:10 -0400 Subject: [PATCH 4/5] feat(agc): filter converted images sampled with unnormalized coordinates Unnormalized coordinates are in texels of the base level, so the in-shader filter uses them without scaling by the image size and samples level 0. Images filtered in the shader are left out of the unnormalized sampler checks, which only apply to host-sampled views. --- .../tests/execution/ImageConvertedUnorm.cpp | 10 ++++++++-- .../IrMetadata/Resources.hpp | 1 + .../Optimization/src/DescriptorBindingBuilder.cpp | 3 +++ .../Optimization/src/ResourceMaterializer.cpp | 3 +-- .../recompiler/SpirvBackend/src/SpirvImageEmitter.cpp | 7 ++++--- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index 5b7a6eae5d9..bae1f1e82f9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -95,6 +95,11 @@ alignas(256) constexpr std::array SpreadSampleLz{ constexpr std::array WrapBilinearSampler{0x00000000u, 0x00fff000u, 0x00500000u, 0u}; constexpr std::array EdgeBilinearSampler{0x00000092u, 0x00fff000u, 0x00500000u, 0u}; constexpr std::array MirrorBilinearSampler{0x00000049u, 0x00fff000u, 0x00500000u, 0u}; +constexpr std::array UnnormalizedBilinearSampler{0x00008092u, 0x00fff000u, 0x00500000u, 0u}; +alignas(256) constexpr std::array TexelSpreadSampleLz{ + 0x34060084u, 0x7e280d00u, 0x062828ffu, 0x3e800000u, 0x7e2a02f0u, 0xf09c0f08u, 0x00610a14u, 0xbf8c3f70u, + 0xe0781000u, 0x80000a03u, 0xbf810000u, +}; constexpr std::array HalfBorderBilinearSampler{0x00000124u, 0x00fff000u, 0x00500000u, 0x80000000u}; constexpr std::array MirrorOnceSampler{0x000000dbu, 0x00fff000u, 0u, 0u}; @@ -306,12 +311,12 @@ void CheckFloatStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { } } -void CheckFloatSample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const std::array& sampler, bool linear, bool wrap, const std::string& what, bool whiteBorder = false) { +void CheckFloatSample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const std::array& sampler, bool linear, bool wrap, const std::string& what, bool whiteBorder = false, std::span code = SpreadSampleLz) { for (std::uint32_t index = 0; index < Threads; ++index) { const auto texel = FloatTexel(index); std::memcpy(texels + index * 4u, &texel, 4u); } - Run(device, SpreadSampleLz, TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, Threads), sampler, 1); + Run(device, code, TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, Threads), sampler, 1); const auto value = [](std::int32_t index, std::uint32_t component) { return component == 3u ? 1.0f : FloatValues[(static_cast(index) + component * 4u) % 11u]; }; @@ -367,6 +372,7 @@ int main() { RequireRefused(*device, Gather4Lz, texels + FloatOffset, SwizzleXYZ1, PointSampler, "samples or gathers a converted float image", "image_gather4_lz of R10_G11_B11_FLOAT", FloatFormat); CheckFloatSample(*device, texels + FloatOffset, MirrorBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, mirror"); CheckFloatSample(*device, texels + FloatOffset, HalfBorderBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, half border, white", true); + CheckFloatSample(*device, texels + FloatOffset, UnnormalizedBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, unnormalized coordinates", false, TexelSpreadSampleLz); RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, MirrorOnceSampler, "clamp-to-half-border or clamp-to-border addressing", "image_sample_lz with mirror-once addressing"); RequireRefused(*device, Gather4Lz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_gather4_lz"); RequireRefused(*device, GetLod, texels, SwizzleXYZ1, PointSampler, "queries the level of detail of a converted unorm image", "image_get_lod"); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 40768f70e5f..cef7da526af 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -62,6 +62,7 @@ inline constexpr std::uint32_t ClampYShift = 5u; inline constexpr std::uint32_t SingleLevel = 1u << 8u; inline constexpr std::uint32_t MipShift = 9u; inline constexpr std::uint32_t BorderShift = 11u; +inline constexpr std::uint32_t Unnormalized = 1u << 13u; inline constexpr std::uint32_t AddressWrap = 0u; inline constexpr std::uint32_t AddressEdge = 1u; inline constexpr std::uint32_t AddressMirror = 2u; diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 6cd17b6b385..ceb79c0af4e 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -218,6 +218,9 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh continue; } const auto& image = info.images.at(pair.image); + if ((image.emulatedFilter & EmulatedFilter::Enabled) != 0u) { + continue; + } if (image.indirectRoot != ImageResource::NoIndirectImage) { failUnnormalized("samples an image selected at run time"); } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index cf5efacdbfd..54ad2207f8d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -596,7 +596,6 @@ std::uint32_t emulatedFilterState(const IrResourcePlan& plan, const ResourceSnap const auto addressX = addressMode(clampX); const auto addressY = addressMode(clampY); if (magFilter != minFilter || magFilter > 1u) throw std::runtime_error("filtering a converted image in the shader is implemented only with equal point or bilinear minification and magnification filters"); - if (unnormalized) throw std::runtime_error("filtering a converted image in the shader does not implement unnormalized coordinates"); const auto borderType = (words[3] >> 30u) & 0x3u; const bool border = addressX == EmulatedFilter::AddressBorder || addressX == EmulatedFilter::AddressHalfBorder || addressY == EmulatedFilter::AddressBorder || addressY == EmulatedFilter::AddressHalfBorder; if (border && borderType == 3u) throw std::runtime_error("filtering a converted image in the shader with a border color table is not implemented"); @@ -607,7 +606,7 @@ std::uint32_t emulatedFilterState(const IrResourcePlan& plan, const ResourceSnap const auto lastLevel = (descriptor.dwords[3] >> 16u) & 0xfu; const bool lodClamped = (words[2] & 0x3fffu) != 0u || (words[1] & 0xfffu) != 0u || ((words[1] >> 12u) & 0xfffu) < (lastLevel - baseLevel) * 256u || ((descriptor.dwords[1] >> 8u) & 0xfffu) > baseLevel * 256u; if (lastLevel > baseLevel && mip != EmulatedFilter::MipBase && lodClamped) throw std::runtime_error("filtering a converted image in the shader across mip levels with a LOD bias or clamp is not implemented"); - const auto state = EmulatedFilter::Enabled | (magFilter == 1u ? EmulatedFilter::Linear : 0u) | (addressX << EmulatedFilter::ClampXShift) | (addressY << EmulatedFilter::ClampYShift) | (mip << EmulatedFilter::MipShift) | ((border ? borderType : 0u) << EmulatedFilter::BorderShift); + const auto state = EmulatedFilter::Enabled | (magFilter == 1u ? EmulatedFilter::Linear : 0u) | (addressX << EmulatedFilter::ClampXShift) | (addressY << EmulatedFilter::ClampYShift) | (mip << EmulatedFilter::MipShift) | ((border ? borderType : 0u) << EmulatedFilter::BorderShift) | (unnormalized ? EmulatedFilter::Unnormalized : 0u); if (filterState.has_value() && *filterState != state) throw std::runtime_error("filtering a converted image in the shader through samplers that disagree is not implemented"); filterState = state; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 8685f87dc7e..bb7d42dbc7b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1188,7 +1188,8 @@ void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& const auto& image = access.image; const auto filter = image.emulatedFilter; if (access.slot != 0) ctx.Fail(access.inst, "filters a converted image through a bindless image table, which is not implemented"); - const bool baseOnly = HasFlag(mem, RdnaImageSampleFlagLevelZero) || (filter & EmulatedFilter::SingleLevel) != 0u || EmulatedFilter::Mip(filter) == EmulatedFilter::MipBase; + const bool unnormalized = (filter & EmulatedFilter::Unnormalized) != 0u; + const bool baseOnly = unnormalized || HasFlag(mem, RdnaImageSampleFlagLevelZero) || (filter & EmulatedFilter::SingleLevel) != 0u || EmulatedFilter::Mip(filter) == EmulatedFilter::MipBase; if (!baseOnly && (!HasFlag(mem, RdnaImageSampleFlagLod) || setup.layout.clamp != NoImageComponent)) ctx.Fail(access.inst, "filters a converted image across mip levels without an explicit, unclamped LOD, which is not implemented"); const bool arrayed = image.dimension == RdnaImageDimension::Dim2DArray; if (image.dimension != RdnaImageDimension::Dim2D && !arrayed) ctx.Fail(access.inst, "filters a converted image that is not a 2D or 2D array view, which is not implemented"); @@ -1283,8 +1284,8 @@ void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& const auto size = levelSize(level); const auto width = Unary(state, spv::OpBitcast, i32, extract(u32, size, 0u)); const auto height = Unary(state, spv::OpBitcast, i32, extract(u32, size, 1u)); - auto scaledU = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 0u), Unary(state, spv::OpConvertSToF, f32, width)); - auto scaledV = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 1u), Unary(state, spv::OpConvertSToF, f32, height)); + auto scaledU = unnormalized ? extract(f32, setup.coord, 0u) : Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 0u), Unary(state, spv::OpConvertSToF, f32, width)); + auto scaledV = unnormalized ? extract(f32, setup.coord, 1u) : Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 1u), Unary(state, spv::OpConvertSToF, f32, height)); if (EmulatedFilter::AddressX(filter) == EmulatedFilter::AddressHalfBorder) scaledU = ext(f32, GLSLstd450FClamp, {scaledU, ConstantF32(state, 0u), Unary(state, spv::OpConvertSToF, f32, width)}); if (EmulatedFilter::AddressY(filter) == EmulatedFilter::AddressHalfBorder) scaledV = ext(f32, GLSLstd450FClamp, {scaledV, ConstantF32(state, 0u), Unary(state, spv::OpConvertSToF, f32, height)}); if (!linear) { From 0db5218e05194a52996befb21e5c04fb6cdcb14e Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 7 Oct 2026 23:32:21 -0400 Subject: [PATCH 5/5] feat(agc): filter converted 3D images in the shader Volumes of converted formats are sampled like the 2D images: the in-shader filter takes the sampler's Z addressing mode, fetches the two nearest slices and blends them by the W weight (or picks one slice with point filtering). --- .../tests/execution/ImageConvertedUnorm.cpp | 36 +++++++++++++++++++ .../IrMetadata/Resources.hpp | 2 ++ .../Optimization/src/ResourceMaterializer.cpp | 7 ++-- .../SpirvBackend/src/SpirvImageEmitter.cpp | 34 ++++++++++++++---- 4 files changed, 70 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp index bae1f1e82f9..2cef79f2224 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -1,3 +1,5 @@ +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -36,6 +38,8 @@ constexpr std::uint32_t UnormFormat = 30; constexpr std::uint32_t UintFormat = 34; constexpr std::uint32_t FloatFormat = 43; constexpr std::size_t FloatOffset = 40960; +constexpr std::size_t VolumeOffset = 49152; +constexpr std::uint32_t Type3D = 10; constexpr std::uint32_t SwizzleXYZ1 = 0x3acu; constexpr std::uint32_t SwizzleXZY1 = 0x374u; constexpr std::uint32_t SwizzleZYX1 = 0x32eu; @@ -100,6 +104,10 @@ alignas(256) constexpr std::array TexelSpreadSampleLz{ 0x34060084u, 0x7e280d00u, 0x062828ffu, 0x3e800000u, 0x7e2a02f0u, 0xf09c0f08u, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u, }; +alignas(256) constexpr std::array VolumeSampleLz{ + 0x34060084u, 0x7e280d00u, 0x062828ffu, 0x3e800000u, 0x102828ffu, 0x3d000000u, 0x7e2a02f0u, 0x7e2c02f0u, + 0xf09c0f10u, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u, +}; constexpr std::array HalfBorderBilinearSampler{0x00000124u, 0x00fff000u, 0x00500000u, 0x80000000u}; constexpr std::array MirrorOnceSampler{0x000000dbu, 0x00fff000u, 0u, 0u}; @@ -334,6 +342,33 @@ void CheckFloatSample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, con } } +void CheckFloatVolumeSample(AgcDriver::VulkanDevice& device, std::uint8_t* texels) { + auto texture = TextureDescriptor(texels, FloatFormat, SwizzleXYZ1, Threads); + texture[3] = (texture[3] & ~(0xfu << 28u)) | (Type3D << 28u); + texture[4] = 1u; + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(texture)); + const auto& mip = geometry.mips.at(0); + const auto value = [](std::uint32_t slice, std::int32_t index, std::uint32_t component) { + return component == 3u ? 1.0f : FloatValues[(static_cast(index) + component * 4u + slice * 2u) % 11u]; + }; + for (std::uint32_t slice = 0; slice < 2u; ++slice) { + for (std::uint32_t index = 0; index < Threads; ++index) { + const auto texel = SmallFloat(value(slice, static_cast(index), 0u), 5u) | (SmallFloat(value(slice, static_cast(index), 1u), 6u) << 10u) | (SmallFloat(value(slice, static_cast(index), 2u), 6u) << 21u); + std::memcpy(texels + geometry.GuestLayerOffset(slice) + mip.tiledOffset + index * 4u, &texel, 4u); + } + } + Run(device, VolumeSampleLz, texture, EdgeBilinearSampler, 1); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto left = tid == 0u ? 0 : static_cast(tid) - 1; + for (std::uint32_t component = 0; component < 4u; ++component) { + float expected = 0.0f; + for (std::uint32_t slice = 0; slice < 2u; ++slice) expected += 0.5f * (value(slice, left, component) * 0.25f + value(slice, static_cast(tid), component) * 0.75f); + const float actual = std::bit_cast(Output[tid * 4u + component]); + Require(std::fabs(actual - expected) <= 1e-5f * std::max(1.0f, std::fabs(expected)), "image_sample_lz 3d of R10_G11_B11_FLOAT, bilinear between two slices: thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } + } +} + void RequireRefused(AgcDriver::VulkanDevice& device, std::span code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array& extra, const std::string& reason, const std::string& what, std::uint32_t format = UnormFormat) { std::string refusal; try { @@ -373,6 +408,7 @@ int main() { CheckFloatSample(*device, texels + FloatOffset, MirrorBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, mirror"); CheckFloatSample(*device, texels + FloatOffset, HalfBorderBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, half border, white", true); CheckFloatSample(*device, texels + FloatOffset, UnnormalizedBilinearSampler, true, false, "image_sample_lz of R10_G11_B11_FLOAT, bilinear, unnormalized coordinates", false, TexelSpreadSampleLz); + CheckFloatVolumeSample(*device, texels + VolumeOffset); RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, MirrorOnceSampler, "clamp-to-half-border or clamp-to-border addressing", "image_sample_lz with mirror-once addressing"); RequireRefused(*device, Gather4Lz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_gather4_lz"); RequireRefused(*device, GetLod, texels, SwizzleXYZ1, PointSampler, "queries the level of detail of a converted unorm image", "image_get_lod"); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index cef7da526af..58a0db8f32d 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -63,6 +63,7 @@ inline constexpr std::uint32_t SingleLevel = 1u << 8u; inline constexpr std::uint32_t MipShift = 9u; inline constexpr std::uint32_t BorderShift = 11u; inline constexpr std::uint32_t Unnormalized = 1u << 13u; +inline constexpr std::uint32_t ClampZShift = 14u; inline constexpr std::uint32_t AddressWrap = 0u; inline constexpr std::uint32_t AddressEdge = 1u; inline constexpr std::uint32_t AddressMirror = 2u; @@ -77,6 +78,7 @@ inline constexpr std::uint32_t BorderOpaqueWhite = 2u; [[nodiscard]] inline std::uint32_t Mip(std::uint32_t state) { return (state >> MipShift) & 0x3u; } [[nodiscard]] inline std::uint32_t AddressX(std::uint32_t state) { return (state >> ClampXShift) & 0x7u; } [[nodiscard]] inline std::uint32_t AddressY(std::uint32_t state) { return (state >> ClampYShift) & 0x7u; } +[[nodiscard]] inline std::uint32_t AddressZ(std::uint32_t state) { return (state >> ClampZShift) & 0x7u; } [[nodiscard]] inline std::uint32_t Border(std::uint32_t state) { return (state >> BorderShift) & 0x3u; } } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 54ad2207f8d..363e80f5863 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -595,9 +595,12 @@ std::uint32_t emulatedFilterState(const IrResourcePlan& plan, const ResourceSnap }; const auto addressX = addressMode(clampX); const auto addressY = addressMode(clampY); + const bool volume = rawImageType(snapshot.images[index]) == ImageType::Color3D; + const auto addressZ = volume ? addressMode((words[0] >> 6u) & 0x7u) : EmulatedFilter::AddressWrap; if (magFilter != minFilter || magFilter > 1u) throw std::runtime_error("filtering a converted image in the shader is implemented only with equal point or bilinear minification and magnification filters"); const auto borderType = (words[3] >> 30u) & 0x3u; - const bool border = addressX == EmulatedFilter::AddressBorder || addressX == EmulatedFilter::AddressHalfBorder || addressY == EmulatedFilter::AddressBorder || addressY == EmulatedFilter::AddressHalfBorder; + const auto borderMode = [](std::uint32_t mode) { return mode == EmulatedFilter::AddressBorder || mode == EmulatedFilter::AddressHalfBorder; }; + const bool border = borderMode(addressX) || borderMode(addressY) || borderMode(addressZ); if (border && borderType == 3u) throw std::runtime_error("filtering a converted image in the shader with a border color table is not implemented"); const auto mipFilter = (words[2] >> 24u) & 0x3u; const auto mip = mipFilter == 1u ? EmulatedFilter::MipPoint : mipFilter == 2u ? EmulatedFilter::MipLinear : EmulatedFilter::MipBase; @@ -606,7 +609,7 @@ std::uint32_t emulatedFilterState(const IrResourcePlan& plan, const ResourceSnap const auto lastLevel = (descriptor.dwords[3] >> 16u) & 0xfu; const bool lodClamped = (words[2] & 0x3fffu) != 0u || (words[1] & 0xfffu) != 0u || ((words[1] >> 12u) & 0xfffu) < (lastLevel - baseLevel) * 256u || ((descriptor.dwords[1] >> 8u) & 0xfffu) > baseLevel * 256u; if (lastLevel > baseLevel && mip != EmulatedFilter::MipBase && lodClamped) throw std::runtime_error("filtering a converted image in the shader across mip levels with a LOD bias or clamp is not implemented"); - const auto state = EmulatedFilter::Enabled | (magFilter == 1u ? EmulatedFilter::Linear : 0u) | (addressX << EmulatedFilter::ClampXShift) | (addressY << EmulatedFilter::ClampYShift) | (mip << EmulatedFilter::MipShift) | ((border ? borderType : 0u) << EmulatedFilter::BorderShift) | (unnormalized ? EmulatedFilter::Unnormalized : 0u); + const auto state = EmulatedFilter::Enabled | (magFilter == 1u ? EmulatedFilter::Linear : 0u) | (addressX << EmulatedFilter::ClampXShift) | (addressY << EmulatedFilter::ClampYShift) | (mip << EmulatedFilter::MipShift) | ((border ? borderType : 0u) << EmulatedFilter::BorderShift) | (unnormalized ? EmulatedFilter::Unnormalized : 0u) | (addressZ << EmulatedFilter::ClampZShift); if (filterState.has_value() && *filterState != state) throw std::runtime_error("filtering a converted image in the shader through samplers that disagree is not implemented"); filterState = state; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index bb7d42dbc7b..8052ac07e03 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1192,7 +1192,8 @@ void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& const bool baseOnly = unnormalized || HasFlag(mem, RdnaImageSampleFlagLevelZero) || (filter & EmulatedFilter::SingleLevel) != 0u || EmulatedFilter::Mip(filter) == EmulatedFilter::MipBase; if (!baseOnly && (!HasFlag(mem, RdnaImageSampleFlagLod) || setup.layout.clamp != NoImageComponent)) ctx.Fail(access.inst, "filters a converted image across mip levels without an explicit, unclamped LOD, which is not implemented"); const bool arrayed = image.dimension == RdnaImageDimension::Dim2DArray; - if (image.dimension != RdnaImageDimension::Dim2D && !arrayed) ctx.Fail(access.inst, "filters a converted image that is not a 2D or 2D array view, which is not implemented"); + const bool volume = image.dimension == RdnaImageDimension::Dim3D; + if (image.dimension != RdnaImageDimension::Dim2D && !arrayed && !volume) ctx.Fail(access.inst, "filters a converted image that is not a 2D, 2D array or 3D view, which is not implemented"); const auto f32 = TypeF32(state); const auto i32 = TypeI32(state); const auto u32 = TypeU32(state); @@ -1248,11 +1249,12 @@ void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& const auto below = Binary(state, spv::OpSLessThan, TypeBool(state), index, extent); return Binary(state, spv::OpLogicalAnd, TypeBool(state), notBelow, below); }; - const auto fetch = [&](std::uint32_t level, std::uint32_t width, std::uint32_t height, std::uint32_t x, std::uint32_t y) { + const auto fetch = [&](std::uint32_t level, std::uint32_t width, std::uint32_t height, std::uint32_t depth, std::uint32_t x, std::uint32_t y, std::uint32_t z) { const auto ux = Unary(state, spv::OpBitcast, u32, address(x, width, EmulatedFilter::AddressX(filter))); const auto uy = Unary(state, spv::OpBitcast, u32, address(y, height, EmulatedFilter::AddressY(filter))); const auto coord = state.module.AllocateId(); - if (arrayed) state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3), coord, ux, uy, layer); + if (volume) state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3), coord, ux, uy, Unary(state, spv::OpBitcast, u32, address(z, depth, EmulatedFilter::AddressZ(filter)))); + else if (arrayed) state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 3), coord, ux, uy, layer); else state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2), coord, ux, uy); const auto texel = state.module.AllocateId(); state.module.AddFunction(spv::OpImageFetch, ImageVectorType(state, image.numericClass, 4), texel, descriptor, coord, spv::ImageOperandsLodMask, level); @@ -1263,6 +1265,10 @@ void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& const auto outsideY = Unary(state, spv::OpLogicalNot, TypeBool(state), inside(y, height)); outside = outside == 0u ? outsideY : Binary(state, spv::OpLogicalOr, TypeBool(state), outside, outsideY); } + if (volume && bordered(EmulatedFilter::AddressZ(filter))) { + const auto outsideZ = Unary(state, spv::OpLogicalNot, TypeBool(state), inside(z, depth)); + outside = outside == 0u ? outsideZ : Binary(state, spv::OpLogicalOr, TypeBool(state), outside, outsideZ); + } if (outside == 0u) return value; const auto condition = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeBoolVector(state, 4), condition, outside, outside, outside, outside); @@ -1275,15 +1281,20 @@ void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& }; auto offsetX = ConstantI32(state, 0); auto offsetY = ConstantI32(state, 0); + auto offsetZ = ConstantI32(state, 0); if (setup.layout.offset != NoImageComponent) { const auto offset = PackedOffset(ctx, access, setup.layout); offsetX = extract(i32, offset, 0u); offsetY = extract(i32, offset, 1u); + if (volume) offsetZ = extract(i32, offset, 2u); } const auto filtered = [&](std::uint32_t level) { const auto size = levelSize(level); const auto width = Unary(state, spv::OpBitcast, i32, extract(u32, size, 0u)); const auto height = Unary(state, spv::OpBitcast, i32, extract(u32, size, 1u)); + const auto depth = volume ? Unary(state, spv::OpBitcast, i32, extract(u32, size, 2u)) : ConstantI32(state, 1); + auto scaledW = volume ? (unnormalized ? extract(f32, setup.coord, 2u) : Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 2u), Unary(state, spv::OpConvertSToF, f32, depth))) : ConstantF32(state, 0x3f000000u); + if (volume && EmulatedFilter::AddressZ(filter) == EmulatedFilter::AddressHalfBorder) scaledW = ext(f32, GLSLstd450FClamp, {scaledW, ConstantF32(state, 0u), Unary(state, spv::OpConvertSToF, f32, depth)}); auto scaledU = unnormalized ? extract(f32, setup.coord, 0u) : Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 0u), Unary(state, spv::OpConvertSToF, f32, width)); auto scaledV = unnormalized ? extract(f32, setup.coord, 1u) : Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 1u), Unary(state, spv::OpConvertSToF, f32, height)); if (EmulatedFilter::AddressX(filter) == EmulatedFilter::AddressHalfBorder) scaledU = ext(f32, GLSLstd450FClamp, {scaledU, ConstantF32(state, 0u), Unary(state, spv::OpConvertSToF, f32, width)}); @@ -1291,7 +1302,8 @@ void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& if (!linear) { const auto x = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledU})), offsetX); const auto y = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledV})), offsetY); - return fetch(level, width, height, x, y); + const auto z = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledW})), offsetZ); + return fetch(level, width, height, depth, x, y, z); } const auto half = ConstantF32(state, 0x3f000000u); const auto centreU = Binary(state, spv::OpFSub, f32, scaledU, half); @@ -1304,9 +1316,17 @@ void EmitEmulatedFilterSample(SpirvValueEmitContext& ctx, const ImageEmitAccess& const auto y0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorV), offsetY); const auto x1 = Binary(state, spv::OpIAdd, i32, x0, ConstantI32(state, 1)); const auto y1 = Binary(state, spv::OpIAdd, i32, y0, ConstantI32(state, 1)); - const auto top = ext(f32x4, GLSLstd450FMix, {fetch(level, width, height, x0, y0), fetch(level, width, height, x1, y0), weightU}); - const auto bottom = ext(f32x4, GLSLstd450FMix, {fetch(level, width, height, x0, y1), fetch(level, width, height, x1, y1), weightU}); - return ext(f32x4, GLSLstd450FMix, {top, bottom, weightV}); + const auto slice = [&](std::uint32_t z) { + const auto top = ext(f32x4, GLSLstd450FMix, {fetch(level, width, height, depth, x0, y0, z), fetch(level, width, height, depth, x1, y0, z), weightU}); + const auto bottom = ext(f32x4, GLSLstd450FMix, {fetch(level, width, height, depth, x0, y1, z), fetch(level, width, height, depth, x1, y1, z), weightU}); + return ext(f32x4, GLSLstd450FMix, {top, bottom, weightV}); + }; + if (!volume) return slice(ConstantI32(state, 0)); + const auto centreW = Binary(state, spv::OpFSub, f32, scaledW, half); + const auto floorW = ext(f32, GLSLstd450Floor, {centreW}); + const auto z0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorW), offsetZ); + const auto z1 = Binary(state, spv::OpIAdd, i32, z0, ConstantI32(state, 1)); + return ext(f32x4, GLSLstd450FMix, {slice(z0), slice(z1), splat(Binary(state, spv::OpFSub, f32, centreW, floorW))}); }; std::uint32_t result; if (baseOnly) {