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..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" @@ -34,6 +36,10 @@ 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::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; @@ -86,6 +92,24 @@ 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 UnnormalizedBilinearSampler{0x00008092u, 0x00fff000u, 0x00500000u, 0u}; +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}; constexpr std::array StoreValues{ 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3eaaaaabu, 0x80000000u, 0xbf800000u, @@ -249,14 +273,110 @@ 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 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, 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]; + }; + 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 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 { - 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,7 +399,17 @@ 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"); - RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_sample_lz"); + CheckFloatStore(*device, texels + FloatOffset); + CheckFloatLoad(*device, texels + FloatOffset); + 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); + 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"); 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..58a0db8f32d 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -54,6 +54,34 @@ 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 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; +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 AddressZ(std::uint32_t state) { return (state >> ClampZShift) & 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 +107,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/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 08c9b15958b..363e80f5863 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,53 @@ 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); + 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 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; + 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) | (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; + } + 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 +666,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 +756,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 +1088,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/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/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ed87d113e6d..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 @@ -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 9c8599186e8..8052ac07e03 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); @@ -957,9 +960,13 @@ 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 (!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); return {dimensionInfo, layout, access.image.numericClass, dref, coord}; } @@ -1175,6 +1182,173 @@ 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 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; + 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); + 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 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 (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); + 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 (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); + 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); + 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)}); + 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); + 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); + 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 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) { + 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; @@ -1183,6 +1357,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) {