From f75385c1a2fb175f2baa02644e9e3f2aed6c5001 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:38:08 +0200 Subject: [PATCH 01/14] fix(shader): gather a dim-3D image_gather4_lz from one slice as measured --- .../tests/execution/ImageGatherVolume.cpp | 188 ++++++++++++++++++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 55 +++++ 2 files changed, 243 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp new file mode 100644 index 000000000000..592cb90c65fe --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -0,0 +1,188 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 8; +constexpr std::uint32_t Height = 4; +constexpr std::uint32_t Depth = 4; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type3D = 10; +constexpr std::uint32_t ClampWrap = 0; +constexpr std::uint32_t ClampMirror = 1; +constexpr std::uint32_t ClampEdge = 2; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array GatherRed{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0110, 0x00820603, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; +alignas(256) constexpr std::array GatherBlue{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0410, 0x00820603, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; + +struct Variant { + std::span code; + bool blue; + std::uint32_t clamp; + const char* name; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor() { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), + 0xfacu | (Type3D << 28u), + Depth - 1u, + 0u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor(std::uint32_t clamp) { + return {clamp | (clamp << 3u) | (clamp << 6u), 0u, 0u, 0u}; +} + +std::uint8_t Red(std::uint32_t x, std::uint32_t y, std::uint32_t z) { + return static_cast(x + Width * y + Width * Height * z); +} + +std::uint8_t Blue(std::uint32_t x, std::uint32_t y, std::uint32_t z) { + return static_cast(255u - 2u * Red(x, y, z)); +} + +void FillTexels() { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); + Require(geometry.guestBytes <= Texels.size(), "image gather volume: the volume does not fit the texel storage"); + const auto& mip = geometry.mips.at(0); + Texels.fill(0); + for (std::uint32_t z = 0; z < Depth; ++z) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + auto* texel = &Texels[geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = Red(x, y, z); + texel[1] = 0x11; + texel[2] = Blue(x, y, z); + texel[3] = 0xff; + } + } + } +} + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 4u + 0u] = 0u; + Input[tid * 4u + 1u] = std::bit_cast(-0.23f + 0.047f * static_cast(tid)); + Input[tid * 4u + 2u] = std::bit_cast(1.29f - 0.053f * static_cast(tid)); + Input[tid * 4u + 3u] = std::bit_cast(-0.37f + 0.061f * static_cast((tid * 7u) % Threads)); + } +} + +int Address(int coordinate, std::uint32_t size, std::uint32_t clamp) { + const int extent = static_cast(size); + if (clamp == ClampEdge) return std::clamp(coordinate, 0, extent - 1); + if (clamp == ClampWrap) return ((coordinate % extent) + extent) % extent; + const int period = ((coordinate % (2 * extent)) + 2 * extent) % (2 * extent); + return period < extent ? period : 2 * extent - 1 - period; +} + +std::array Expected(std::uint32_t tid, const Variant& variant) { + const float u = std::bit_cast(Input[tid * 4u + 1u]); + const float v = std::bit_cast(Input[tid * 4u + 2u]); + const float r = std::bit_cast(Input[tid * 4u + 3u]); + const int left = static_cast(std::floor(u * static_cast(Width) - 0.5f)); + const int top = static_cast(std::floor(v * static_cast(Height) - 0.5f)); + const auto z = static_cast(Address(static_cast(std::floor(r * static_cast(Depth))), Depth, variant.clamp)); + const auto texel = [&](int x, int y) { + const auto column = static_cast(Address(x, Width, variant.clamp)); + const auto row = static_cast(Address(y, Height, variant.clamp)); + return static_cast(variant.blue ? Blue(column, row, z) : Red(column, row, z)); + }; + return {texel(left, top + 1), texel(left + 1, top + 1), texel(left + 1, top), texel(left, top)}; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const Variant& variant) { + std::vector userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(); + const auto sampler = SamplerDescriptor(variant.clamp); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(variant.code.data()), std::as_bytes(variant.code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(variant.code.data()), variant.code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, const Variant& variant) { + Output.fill(-1.0f); + const auto result = Compile(device, variant); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(variant.code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto expected = Expected(tid, variant); + for (std::uint32_t component = 0; component < 4u; ++component) { + const float actual = Output[tid * 4u + component]; + const float scaled = actual * 255.0f; + Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && static_cast(std::lround(scaled)) == expected[component], std::string(variant.name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255"); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexels(); + FillInput(); + Run(*device, {GatherRed, false, ClampEdge, "3D gather of red, clamp to edge"}); + Run(*device, {GatherBlue, true, ClampEdge, "3D gather of blue, clamp to edge"}); + Run(*device, {GatherRed, false, ClampWrap, "3D gather of red, wrap"}); + Run(*device, {GatherRed, false, ClampMirror, "3D gather of red, mirror"}); + std::puts("image 3D gather tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 58d08e6ca991..74e2168feaa7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -836,6 +836,53 @@ std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const Image return result; } +std::uint32_t EmitVolumeGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { + auto& state = ctx.state; + const auto numericClass = access.image.numericClass; + state.module.EmitCapability(spv::CapabilityImageQuery); + const auto image = LoadSampledImageDescriptor(state, access.mem.resource); + const auto size = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 3), size, image, ConstantU32(state, 0)); + std::uint32_t extents[3] = {}; + std::uint32_t starts[3] = {}; + for (std::uint32_t axis = 0; axis < 3u; axis++) { + const auto extent = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), extent, size, axis); + extents[axis] = Unary(state, spv::OpConvertUToF, TypeF32(state), extent); + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), coord, setup.coord, axis); + auto scaled = Binary(state, spv::OpFMul, TypeF32(state), coord, extents[axis]); + if (axis < 2u) { + scaled = Binary(state, spv::OpFSub, TypeF32(state), scaled, ConstantF32(state, 0x3f000000u)); + } + starts[axis] = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), starts[axis], GlslStd450(state), GLSLstd450Floor, scaled); + } + const auto centre = [&](std::uint32_t axis, std::uint32_t half) { + return Binary(state, spv::OpFDiv, TypeF32(state), Binary(state, spv::OpFAdd, TypeF32(state), starts[axis], ConstantF32(state, half)), extents[axis]); + }; + const std::uint32_t columns[2] = {centre(0u, 0x3f000000u), centre(0u, 0x3fc00000u)}; + const std::uint32_t rows[2] = {centre(1u, 0x3f000000u), centre(1u, 0x3fc00000u)}; + const auto slice = centre(2u, 0x3f000000u); + const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler); + const auto vectorType = ImageVectorType(state, numericClass, 4); + const auto scalarType = ImageScalarType(state, numericClass); + const auto component = ImageConversionFormat(access.image).format == IrBufferFormat::Invalid ? ImageGatherComponent(EffectiveDmask(access.mem)) : 0u; + constexpr std::uint32_t footprint[4][2] = {{0u, 1u}, {1u, 1u}, {1u, 0u}, {0u, 0u}}; + std::uint32_t values[4] = {}; + for (std::uint32_t index = 0; index < 4u; index++) { + const auto sampleCoord = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 3), sampleCoord, columns[footprint[index][0]], rows[footprint[index][1]], slice); + const auto texel = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageSampleExplicitLod, vectorType, texel, sampled, sampleCoord, spv::ImageOperandsLodMask, ZeroF32(state)); + values[index] = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, scalarType, values[index], texel, component); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, vectorType, result, values[0], values[1], values[2], values[3]); + return result; +} + std::uint32_t PackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t texel) { auto& state = ctx.state; const auto info = ImageConversionFormat(access.image); @@ -1426,6 +1473,14 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con ctx.Define(access.inst, ResultVector(ctx, access, UnpackImageGather(ctx, access, sample), setup.numericClass, false, true)); return; } + if (dimension == RdnaImageDimension::Dim3D) { + if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal) || setup.layout.offset != NoImageComponent) { + ctx.Fail(access.inst, "has an unsupported 3D gather variant"); + } + const auto sample = EmitVolumeGatherLz(ctx, access, setup); + ctx.Define(access.inst, ResultVector(ctx, access, UnpackImageGather(ctx, access, sample), setup.numericClass, false, true)); + return; + } const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler); const auto sample = state.module.AllocateId(); std::vector words; From e26a9c81cae3062fdf9dc33ab99cd34c26777866 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:32:41 +0200 Subject: [PATCH 02/14] fix(recompiler): address a 3D gather by the sampler's clamp modes and border color --- .../tests/execution/ImageGatherVolume.cpp | 224 +++++++++++++----- .../IrMetadata/Resources.hpp | 11 + .../Optimization/ResourceMaterializer.hpp | 1 + .../src/DescriptorBindingBuilder.cpp | 6 + .../Optimization/src/ResourceMaterializer.cpp | 28 +++ .../Optimization/src/ResourceTracker.cpp | 1 + .../recompiler/PipelineSpecialization.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 + .../SpirvBackend/src/SpirvImageEmitter.cpp | 112 +++++++-- 9 files changed, 303 insertions(+), 83 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp index 592cb90c65fe..3a8ad16b3a7b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include @@ -20,18 +22,17 @@ using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; -constexpr std::uint32_t Width = 8; -constexpr std::uint32_t Height = 4; -constexpr std::uint32_t Depth = 4; constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Type3D = 10; -constexpr std::uint32_t ClampWrap = 0; -constexpr std::uint32_t ClampMirror = 1; -constexpr std::uint32_t ClampEdge = 2; +constexpr std::uint32_t IdentitySwizzle = 0xfacu; +constexpr std::uint32_t BlueGreenRedAlpha = 0xf2eu; +constexpr std::array Positions{-9.3f, -4.8f, -1.3f, -0.7f, -0.2f, 0.3f, 0.9f, 1.6f, 2.2f, 3.7f, + -1.3f, -0.7f, -0.2f, 0.3f, 0.8f, 1.6f, 0.3f, 1.9f, -0.3f, -1.6f}; +constexpr std::array PositionExtents{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, -1, -1}; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(4096) std::array Texels{}; +alignas(4096) std::array Texels{}; alignas(256) constexpr std::array GatherRed{ 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0110, 0x00820603, 0xbf8c3f70, 0xe0781000, @@ -41,12 +42,34 @@ alignas(256) constexpr std::array GatherBlue{ 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0410, 0x00820603, 0xbf8c3f70, 0xe0781000, 0x80010601, 0xbf810000, }; +alignas(256) constexpr std::array GatherAlpha{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0810, 0x00820603, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; + +struct Volume { + std::uint32_t width; + std::uint32_t height; + std::uint32_t depth; + std::uint32_t baseLevel = 0; + std::uint32_t lastLevel = 0; + std::uint32_t swizzle = IdentitySwizzle; +}; + +struct SamplerState { + std::uint32_t clamp; + std::uint32_t border = 0; + std::uint32_t extra0 = 0; + std::uint32_t word1 = 0xfffu << 12u; + std::uint32_t word2 = 0; +}; struct Variant { std::span code; - bool blue; - std::uint32_t clamp; - const char* name; + std::uint32_t channel; + Volume volume; + SamplerState sampler; + std::string name; }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -54,88 +77,136 @@ std::array BufferDescriptor(const void* data, std::uint32_t by return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; } -std::array TextureDescriptor() { +std::array TextureDescriptor(const Volume& volume) { const auto address = static_cast(reinterpret_cast(Texels.data())); return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), - ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), - 0xfacu | (Type3D << 28u), - Depth - 1u, - 0u, + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((volume.width - 1u) & 3u) << 30u), + ((volume.width - 1u) >> 2u) | ((volume.height - 1u) << 14u), + volume.swizzle | (volume.baseLevel << 12u) | (volume.lastLevel << 16u) | (Type3D << 28u), + volume.depth - 1u, + volume.lastLevel << 4u, 0u, 0u, }; } -std::array SamplerDescriptor(std::uint32_t clamp) { - return {clamp | (clamp << 3u) | (clamp << 6u), 0u, 0u, 0u}; +std::array SamplerDescriptor(const SamplerState& sampler) { + return {sampler.clamp | (sampler.clamp << 3u) | (sampler.clamp << 6u) | sampler.extra0, sampler.word1, sampler.word2, sampler.border << 30u}; } -std::uint8_t Red(std::uint32_t x, std::uint32_t y, std::uint32_t z) { - return static_cast(x + Width * y + Width * Height * z); +std::uint32_t LevelStart(const Volume& volume, std::uint32_t level) { + std::uint32_t start = 0; + for (std::uint32_t below = 0; below < level; ++below) { + start += std::max(volume.width >> below, 1u) * std::max(volume.height >> below, 1u) * std::max(volume.depth >> below, 1u); + } + return start; } -std::uint8_t Blue(std::uint32_t x, std::uint32_t y, std::uint32_t z) { - return static_cast(255u - 2u * Red(x, y, z)); +std::array Texel(const Volume& volume, std::uint32_t level, std::uint32_t x, std::uint32_t y, std::uint32_t z) { + const auto width = std::max(volume.width >> level, 1u); + const auto height = std::max(volume.height >> level, 1u); + const auto id = LevelStart(volume, level) + x + width * y + width * height * z; + return {static_cast(id + 1u), 0x11, static_cast(254u - id), static_cast(id * 37u + 11u)}; } -void FillTexels() { - const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor())); +void FillTexels(const Volume& volume) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(volume))); Require(geometry.guestBytes <= Texels.size(), "image gather volume: the volume does not fit the texel storage"); - const auto& mip = geometry.mips.at(0); Texels.fill(0); - for (std::uint32_t z = 0; z < Depth; ++z) { - for (std::uint32_t y = 0; y < Height; ++y) { - for (std::uint32_t x = 0; x < Width; ++x) { - auto* texel = &Texels[geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; - texel[0] = Red(x, y, z); - texel[1] = 0x11; - texel[2] = Blue(x, y, z); - texel[3] = 0xff; + for (std::uint32_t level = 0; level <= volume.lastLevel; ++level) { + const auto& mip = geometry.mips.at(level); + for (std::uint32_t z = 0; z < std::max(volume.depth >> level, 1u); ++z) { + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + const auto value = Texel(volume, level, x, y, z); + std::copy(value.begin(), value.end(), &Texels[geometry.GuestLayerOffset(z) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]); + } } } } } -void FillInput() { +float Position(std::uint32_t index, std::uint32_t extent) { + return Positions[index] + static_cast(PositionExtents[index] * static_cast(extent)); +} + +void FillInput(const Volume& volume) { + const auto width = std::max(volume.width >> volume.baseLevel, 1u); + const auto height = std::max(volume.height >> volume.baseLevel, 1u); + const auto depth = std::max(volume.depth >> volume.baseLevel, 1u); for (std::uint32_t tid = 0; tid < Threads; ++tid) { Input[tid * 4u + 0u] = 0u; - Input[tid * 4u + 1u] = std::bit_cast(-0.23f + 0.047f * static_cast(tid)); - Input[tid * 4u + 2u] = std::bit_cast(1.29f - 0.053f * static_cast(tid)); - Input[tid * 4u + 3u] = std::bit_cast(-0.37f + 0.061f * static_cast((tid * 7u) % Threads)); + Input[tid * 4u + 1u] = std::bit_cast(Position(tid % 20u, width) / static_cast(width)); + Input[tid * 4u + 2u] = std::bit_cast(Position((tid * 7u + 3u) % 20u, height) / static_cast(height)); + Input[tid * 4u + 3u] = std::bit_cast((Position((tid * 11u + 5u) % 20u, depth) + 0.5f) / static_cast(depth)); } } -int Address(int coordinate, std::uint32_t size, std::uint32_t clamp) { - const int extent = static_cast(size); - if (clamp == ClampEdge) return std::clamp(coordinate, 0, extent - 1); - if (clamp == ClampWrap) return ((coordinate % extent) + extent) % extent; - const int period = ((coordinate % (2 * extent)) + 2 * extent) % (2 * extent); - return period < extent ? period : 2 * extent - 1 - period; +int Fold(int index) { + return index < 0 ? -1 - index : index; +} + +std::optional Single(int index, int extent, std::uint32_t clamp) { + if (clamp == 0u) return ((index % extent) + extent) % extent; + if (clamp == 1u) { + const int phase = ((index % (2 * extent)) + 2 * extent) % (2 * extent); + return phase < extent ? phase : 2 * extent - 1 - phase; + } + const int source = (clamp & 1u) != 0u ? Fold(index) : index; + if (clamp < 6u) return std::clamp(source, 0, extent - 1); + if (source < 0 || source >= extent) return std::nullopt; + return source; +} + +std::array, 2> Footprint(int first, int extent, std::uint32_t clamp) { + if ((clamp & 6u) != 4u) return {Single(first, extent, clamp), Single(first + 1, extent, clamp)}; + const std::array sources{clamp == 5u ? Fold(first) : first, clamp == 5u ? Fold(first + 1) : first + 1}; + const int low = std::min(sources[0], sources[1]); + const int pinned = std::clamp(low, -1, extent - 1); + std::array, 2> result; + for (std::size_t index = 0; index < 2u; ++index) { + const int effective = pinned + sources[index] - low; + if (effective >= 0 && effective < extent) result[index] = effective; + } + return result; +} + +std::uint32_t Channel(const Variant& variant, const std::array& texel) { + const auto select = (variant.volume.swizzle >> (3u * variant.channel)) & 7u; + return texel.at(select - 4u); +} + +std::uint32_t BorderValue(const Variant& variant) { + if (variant.sampler.border == 2u) return 255u; + return variant.sampler.border == 1u && variant.channel == 3u ? 255u : 0u; } std::array Expected(std::uint32_t tid, const Variant& variant) { + const auto& volume = variant.volume; + const auto level = volume.baseLevel; + const auto width = static_cast(std::max(volume.width >> level, 1u)); + const auto height = static_cast(std::max(volume.height >> level, 1u)); + const auto depth = static_cast(std::max(volume.depth >> level, 1u)); const float u = std::bit_cast(Input[tid * 4u + 1u]); const float v = std::bit_cast(Input[tid * 4u + 2u]); const float r = std::bit_cast(Input[tid * 4u + 3u]); - const int left = static_cast(std::floor(u * static_cast(Width) - 0.5f)); - const int top = static_cast(std::floor(v * static_cast(Height) - 0.5f)); - const auto z = static_cast(Address(static_cast(std::floor(r * static_cast(Depth))), Depth, variant.clamp)); - const auto texel = [&](int x, int y) { - const auto column = static_cast(Address(x, Width, variant.clamp)); - const auto row = static_cast(Address(y, Height, variant.clamp)); - return static_cast(variant.blue ? Blue(column, row, z) : Red(column, row, z)); + const auto columns = Footprint(static_cast(std::floor(u * static_cast(width) - 0.5f)), width, variant.sampler.clamp); + const auto rows = Footprint(static_cast(std::floor(v * static_cast(height) - 0.5f)), height, variant.sampler.clamp); + const auto slice = Single(static_cast(std::floor(r * static_cast(depth))), depth, (variant.sampler.clamp & 6u) == 4u ? (variant.sampler.clamp & 1u) | 2u : variant.sampler.clamp); + const auto value = [&](std::size_t column, std::size_t row) { + if (!columns[column] || !rows[row] || !slice) return BorderValue(variant); + return Channel(variant, Texel(volume, level, static_cast(*columns[column]), static_cast(*rows[row]), static_cast(*slice))); }; - return {texel(left, top + 1), texel(left + 1, top + 1), texel(left + 1, top), texel(left, top)}; + return {value(0, 1), value(1, 1), value(1, 0), value(0, 0)}; } ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const Variant& variant) { std::vector userData(20, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); - const auto texture = TextureDescriptor(); - const auto sampler = SamplerDescriptor(variant.clamp); + const auto texture = TextureDescriptor(variant.volume); + const auto sampler = SamplerDescriptor(variant.sampler); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); std::copy(texture.begin(), texture.end(), userData.begin() + 8); @@ -152,33 +223,60 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device, const Variant& variant) { +void Dispatch(AgcDriver::VulkanDevice& device, const Variant& variant) { + FillTexels(variant.volume); + FillInput(variant.volume); Output.fill(-1.0f); const auto result = Compile(device, variant); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(variant.code.data())); device.WaitIdle(); +} + +void Run(AgcDriver::VulkanDevice& device, const Variant& variant) { + Dispatch(device, variant); for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto expected = Expected(tid, variant); for (std::uint32_t component = 0; component < 4u; ++component) { - const float actual = Output[tid * 4u + component]; - const float scaled = actual * 255.0f; - Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && static_cast(std::lround(scaled)) == expected[component], std::string(variant.name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255"); + const float scaled = Output[tid * 4u + component] * 255.0f; + Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && static_cast(std::lround(scaled)) == expected[component], variant.name + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255"); } } } +void RunThrows(AgcDriver::VulkanDevice& device, const Variant& variant, const std::string& message) { + try { + Dispatch(device, variant); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(message) != std::string::npos, variant.name + ": threw '" + error.what() + "', expected '" + message + "'"); + return; + } + throw std::runtime_error(variant.name + ": did not throw '" + message + "'"); +} + } int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - FillTexels(); - FillInput(); - Run(*device, {GatherRed, false, ClampEdge, "3D gather of red, clamp to edge"}); - Run(*device, {GatherBlue, true, ClampEdge, "3D gather of blue, clamp to edge"}); - Run(*device, {GatherRed, false, ClampWrap, "3D gather of red, wrap"}); - Run(*device, {GatherRed, false, ClampMirror, "3D gather of red, mirror"}); + const Volume cube{8, 4, 4}; + const Volume odd{7, 5, 3}; + const char* clampNames[8] = {"wrap", "mirror", "clamp to edge", "mirror once to edge", "clamp to half border", "mirror once to half border", "clamp to border", "mirror once to border"}; + const char* borderNames[3] = {"transparent black", "opaque black", "opaque white"}; + for (std::uint32_t clamp = 0; clamp < 8u; ++clamp) { + for (std::uint32_t border = 0; border < (clamp >= 4u ? 3u : 1u); ++border) { + const auto suffix = std::string(clampNames[clamp]) + (clamp >= 4u ? std::string(", ") + borderNames[border] : std::string()); + Run(*device, {GatherRed, 0, cube, {clamp, border}, "3D gather of red, " + suffix}); + Run(*device, {GatherAlpha, 3, cube, {clamp, border}, "3D gather of alpha, " + suffix}); + Run(*device, {GatherBlue, 2, odd, {clamp, border}, "3D gather of blue from a 7x5x3 volume, " + suffix}); + } + } + const SamplerState lodState{2, 0, 4u << 9u, 0x100u | (0xfffu << 12u), 0x100u | (1u << 20u) | (1u << 22u) | (2u << 26u)}; + Run(*device, {GatherRed, 0, {8, 4, 4, 0, 1}, lodState, "3D gather of red ignores the sampler's LOD clamp, bias, filters and anisotropy"}); + Run(*device, {GatherRed, 0, {8, 4, 4, 1, 1}, {0}, "3D gather of red reads the base level"}); + Run(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {1}, "3D gather of red through a blue-green-red-alpha swizzle"}); + RunThrows(*device, {GatherRed, 0, cube, {6, 3}, "3D gather with a border color table"}, "a 3D gather with a border color table is not implemented"); + RunThrows(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {6, 2}, "3D gather of a swizzled volume with border addressing"}, "a 3D gather of a swizzled texture with border addressing is not implemented"); std::puts("image 3D gather tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index af39e2bd1a73..571cb0d745ff 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -59,6 +59,16 @@ inline constexpr std::uint32_t ReferenceZero = 3u; [[nodiscard]] inline std::uint32_t Reference(std::uint32_t state) { return (state >> ReferenceShift) & 0x3u; } } +namespace VolumeGather { +inline constexpr std::uint32_t ClampXShift = 0u; +inline constexpr std::uint32_t ClampYShift = 3u; +inline constexpr std::uint32_t ClampZShift = 6u; +inline constexpr std::uint32_t BorderShift = 9u; +inline constexpr std::uint32_t BorderTransparentBlack = 0u; +inline constexpr std::uint32_t BorderOpaqueBlack = 1u; +inline constexpr std::uint32_t BorderOpaqueWhite = 2u; +} + inline constexpr std::uint32_t NoTable = std::numeric_limits::max(); enum class TableOperation : std::uint8_t { Supported, Unsupported }; @@ -106,6 +116,7 @@ struct ImageResource { bool layerRows = false; std::uint32_t maxArrayLayers = 0u; bool optionalDescriptor = false; + bool volumeGather = false; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; IrBufferFormat packedFormat = IrBufferFormat::Invalid; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 3ff81bb44090..8c1cc7eff0df 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -74,6 +74,7 @@ class LayerRowSamplesRequired : public std::runtime_error { class ResourceMaterializer { public: static std::uint32_t EmulatedCompareState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index); + static std::uint32_t VolumeGatherState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index); void ApplyStaticInterface(IrProgram& program, bool nativeSampleOffsets = true) const; static std::vector RuntimeImageModes(const ImageResource& image); static RuntimeImageModeMatch TryRuntimeImageMode(const ImageResource& image, const DescriptorValue& descriptor, std::span modes); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index db5911ef11f5..1e451d7abdb7 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -402,6 +402,12 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l std::vector compareStates(info.images.size()); for (std::uint32_t index = 0; index < info.images.size(); ++index) { const bool convertedSample = !info.images[index].srgbDecodeCompatible && index < info.runtimeImageModes.size() && std::ranges::any_of(info.runtimeImageModes.at(index), [](const ImageResource& mode) { return mode.conversionFormat == IrBufferFormat::Format11_11_10UNorm; }); + if (info.images[index].volumeGather) { + if (info.images[index].depthCompare || convertedSample) fail("a 3D gather of a compared or converted image is not implemented"); + compareStates[index] = ResourceMaterializer::VolumeGatherState(info, snapshot, index); + if (!registrationModules) plan.specialization.push_back({PipelineSpecialization::VolumeGatherBase + index, compareStates[index]}); + continue; + } if (!info.images[index].depthCompare && !convertedSample) continue; const auto compare = compareStates[index] = ResourceMaterializer::EmulatedCompareState(info, snapshot, index); const auto first = PipelineSpecialization::CompareBase + index * PipelineSpecialization::CompareWords; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 533f170f6510..ffa2690385ac 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -2379,6 +2379,34 @@ std::uint32_t ResourceMaterializer::RuntimeImageMode(const ImageResource& image, return *match.mode; } +std::uint32_t ResourceMaterializer::VolumeGatherState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index) { + const auto& image = info.images[index]; + const auto& descriptor = snapshot.images[index]; + if (image.table != NoTable) throw std::runtime_error("a 3D gather of an image table entry is not implemented"); + if (descriptor.dwordCount != 8u || nullImageDescriptor(image, descriptor)) return 0u; + std::optional samplerState; + for (const auto& pair : info.sampledPairs) { + if (pair.image != index) continue; + if (pair.sampler >= info.samplers.size() || info.samplers[pair.sampler].table != NoTable) throw std::runtime_error("a 3D gather through a sampler table is not implemented"); + if (pair.sampler >= snapshot.samplers.size() || snapshot.samplers[pair.sampler].dwordCount != 4u) throw std::runtime_error("a 3D gather has no sampler descriptor"); + const auto& words = snapshot.samplers[pair.sampler].dwords; + if (((words[0] >> 15u) & 0x1u) != 0u) throw std::runtime_error("a 3D gather with unnormalized coordinates is not implemented"); + const auto clampX = words[0] & 0x7u; + const auto clampY = (words[0] >> 3u) & 0x7u; + const auto clampZ = (words[0] >> 6u) & 0x7u; + const auto border = (words[3] >> 30u) & 0x3u; + const bool bordered = std::ranges::any_of(std::array{clampX, clampY, clampZ}, [](std::uint32_t clamp) { return clamp >= 4u; }); + if (bordered && border == 3u) throw std::runtime_error("a 3D gather with a border color table is not implemented"); + if (bordered && (image.numericClass == IrTextureNumericClass::Uint || image.numericClass == IrTextureNumericClass::Sint)) throw std::runtime_error("a 3D gather of an integer texture with border addressing is not implemented"); + if (bordered && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle) throw std::runtime_error("a 3D gather of a swizzled texture with border addressing is not implemented"); + const auto state = (clampX << VolumeGather::ClampXShift) | (clampY << VolumeGather::ClampYShift) | (clampZ << VolumeGather::ClampZShift) | ((bordered ? border : 0u) << VolumeGather::BorderShift); + if (samplerState.has_value() && *samplerState != state) throw std::runtime_error("a 3D gather through samplers that disagree is not implemented"); + samplerState = state; + } + if (!samplerState.has_value()) throw std::runtime_error("a 3D gather has no paired sampler"); + return *samplerState; +} + std::uint32_t ResourceMaterializer::EmulatedCompareState(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint32_t index) { return emulatedCompareState(info, snapshot, index); } diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index a7e26c97d072..c6c8ad90c35d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -2313,6 +2313,7 @@ class Tracker { const bool flatVolumeLoad = op == IrOpcode::ImageRead && memory.imageSampleFlags == 0u; const bool flatVolumeSample = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; const bool flatVolumeQuery = op == IrOpcode::ImageQueryDimensions; + image.volumeGather = image.volumeGather || (op == IrOpcode::ImageGatherRaw && image.dimension == RdnaImageDimension::Dim3D); image.flatVolumeCompatible = image.flatVolumeCompatible && (flatVolumeLoad || flatVolumeSample || flatVolumeQuery) && !memory.imagePacked && memory.imageByElements == 0u; const bool flatLineLoad = op == IrOpcode::ImageRead && !memory.imageHasMip && memory.imageSampleFlags == 0u; const auto lineSampleFlags = memory.imageSampleFlags & ~RdnaImageSampleFlagCompare; diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index efbb4dfc4532..57143c36d535 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -50,6 +50,7 @@ inline constexpr std::uint32_t UnnormalizedBaseLevelBase = 57408u; inline constexpr std::uint32_t FmaskFormatBase = 57472u; inline constexpr std::uint32_t MirrorUnnormalizedBase = 57536u; inline constexpr std::uint32_t MinLodBase = 57600u; +inline constexpr std::uint32_t VolumeGatherBase = 57664u; inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint32_t element) { return DescriptorIndexBase + binding * DescriptorIndexStride + element; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 22b99997519e..1b2b7f39a345 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -819,6 +819,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.layerRows); out.Value(image.maxArrayLayers); out.Value(image.optionalDescriptor); + out.Value(image.volumeGather); out.Value(image.byElements); out.Value(image.byComponents); out.Value(image.packedFormat); @@ -933,6 +934,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.layerRows); in.Value(image.maxArrayLayers); in.Value(image.optionalDescriptor); + in.Value(image.volumeGather); in.Value(image.byElements); in.Value(image.byComponents); in.Value(image.packedFormat); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 74e2168feaa7..0e937c6bee7b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -839,6 +839,15 @@ std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const Image std::uint32_t EmitVolumeGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { auto& state = ctx.state; const auto numericClass = access.image.numericClass; + const auto f32 = TypeF32(state); + const auto i32 = TypeI32(state); + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto gatherState = state.registrationModules + ? EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, u32, RuntimeImageDword(state, access.mem.resource, offsetof(RuntimeAbi::ResourceMetadata, flags) / sizeof(std::uint32_t)), ConstantU32(state, RuntimeAbi::CompareStateShift)), RuntimeAbi::CompareStateMask) + : state.module.SpecializationConstant(u32, PipelineSpecialization::VolumeGatherBase + access.mem.resource, 0u); + const auto field = [&](std::uint32_t shift, std::uint32_t mask) { return EmitAndConstant(state, Binary(state, spv::OpShiftRightLogical, u32, gatherState, ConstantU32(state, shift)), mask); }; + const auto equal = [&](std::uint32_t value, std::uint32_t literal) { return Binary(state, spv::OpIEqual, boolean, value, ConstantU32(state, literal)); }; state.module.EmitCapability(spv::CapabilityImageQuery); const auto image = LoadSampledImageDescriptor(state, access.mem.resource); const auto size = state.module.AllocateId(); @@ -847,36 +856,99 @@ std::uint32_t EmitVolumeGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAcce std::uint32_t starts[3] = {}; for (std::uint32_t axis = 0; axis < 3u; axis++) { const auto extent = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), extent, size, axis); - extents[axis] = Unary(state, spv::OpConvertUToF, TypeF32(state), extent); + state.module.AddFunction(spv::OpCompositeExtract, u32, extent, size, axis); + extents[axis] = Unary(state, spv::OpBitcast, i32, extent); const auto coord = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), coord, setup.coord, axis); - auto scaled = Binary(state, spv::OpFMul, TypeF32(state), coord, extents[axis]); + state.module.AddFunction(spv::OpCompositeExtract, f32, coord, setup.coord, axis); + auto scaled = Binary(state, spv::OpFMul, f32, coord, Unary(state, spv::OpConvertUToF, f32, extent)); + state.module.AddAnnotation(spv::OpDecorate, scaled, spv::DecorationNoContraction); if (axis < 2u) { - scaled = Binary(state, spv::OpFSub, TypeF32(state), scaled, ConstantF32(state, 0x3f000000u)); - } - starts[axis] = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), starts[axis], GlslStd450(state), GLSLstd450Floor, scaled); - } - const auto centre = [&](std::uint32_t axis, std::uint32_t half) { - return Binary(state, spv::OpFDiv, TypeF32(state), Binary(state, spv::OpFAdd, TypeF32(state), starts[axis], ConstantF32(state, half)), extents[axis]); + scaled = Binary(state, spv::OpFSub, f32, scaled, ConstantF32(state, 0x3f000000u)); + state.module.AddAnnotation(spv::OpDecorate, scaled, spv::DecorationNoContraction); + } + const auto floored = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, f32, floored, GlslStd450(state), GLSLstd450Floor, scaled); + starts[axis] = Unary(state, spv::OpConvertFToS, i32, floored); + } + const auto resolve = [&](std::initializer_list indices, std::uint32_t axis, std::uint32_t mode) { + const auto extent = extents[axis]; + const auto last = Binary(state, spv::OpISub, i32, extent, ConstantI32(state, 1)); + const auto modulo = [&](std::uint32_t value, std::uint32_t period) { + const auto remainder = Binary(state, spv::OpSRem, i32, value, period); + return Binary(state, spv::OpIAdd, i32, remainder, Select(state, i32, Binary(state, spv::OpSLessThan, boolean, remainder, ConstantI32(state, 0)), period, ConstantI32(state, 0))); + }; + const auto extInst = [&](std::uint32_t op, std::uint32_t a, std::uint32_t b, std::uint32_t c) { + const auto id = state.module.AllocateId(); + if (c == 0u) { + state.module.AddFunction(spv::OpExtInst, i32, id, GlslStd450(state), op, a, b); + } else { + state.module.AddFunction(spv::OpExtInst, i32, id, GlslStd450(state), op, a, b, c); + } + return id; + }; + const auto once = Binary(state, spv::OpINotEqual, boolean, EmitAndConstant(state, mode, 1u), ConstantU32(state, 0u)); + const auto halfBorder = equal(EmitAndConstant(state, mode, 6u), 4u); + const bool footprint = indices.size() == 2u; + std::vector sources; + for (const auto index : indices) { + const auto folded = Select(state, i32, Binary(state, spv::OpSLessThan, boolean, index, ConstantI32(state, 0)), Binary(state, spv::OpISub, i32, ConstantI32(state, -1), index), index); + sources.push_back(Select(state, i32, once, folded, index)); + } + std::uint32_t low = 0u; + std::uint32_t pinned = 0u; + if (footprint) { + low = extInst(GLSLstd450SMin, sources[0], sources[1], 0u); + pinned = extInst(GLSLstd450SClamp, low, ConstantI32(state, -1), last); + } + const auto period = Binary(state, spv::OpIMul, i32, extent, ConstantI32(state, 2)); + std::vector> resolved; + auto source = sources.begin(); + for (const auto index : indices) { + auto effective = *source++; + if (footprint) { + effective = Select(state, i32, halfBorder, Binary(state, spv::OpIAdd, i32, pinned, Binary(state, spv::OpISub, i32, effective, low)), effective); + } + const auto phase = modulo(index, period); + const auto mirrored = Select(state, i32, Binary(state, spv::OpSLessThan, boolean, phase, extent), phase, Binary(state, spv::OpISub, i32, Binary(state, spv::OpISub, i32, period, ConstantI32(state, 1)), phase)); + const auto clamped = extInst(GLSLstd450SClamp, effective, ConstantI32(state, 0), last); + const auto coordinate = Select(state, i32, equal(mode, 0u), modulo(index, extent), Select(state, i32, equal(mode, 1u), mirrored, clamped)); + const auto outside = Binary(state, spv::OpLogicalOr, boolean, Binary(state, spv::OpSLessThan, boolean, effective, ConstantI32(state, 0)), Binary(state, spv::OpSGreaterThan, boolean, effective, last)); + const auto bordering = Binary(state, spv::OpUGreaterThanEqual, boolean, mode, ConstantU32(state, footprint ? 4u : 6u)); + resolved.emplace_back(coordinate, Binary(state, spv::OpLogicalAnd, boolean, bordering, outside)); + } + return resolved; }; - const std::uint32_t columns[2] = {centre(0u, 0x3f000000u), centre(0u, 0x3fc00000u)}; - const std::uint32_t rows[2] = {centre(1u, 0x3f000000u), centre(1u, 0x3fc00000u)}; - const auto slice = centre(2u, 0x3f000000u); - const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler); + const auto modeX = field(VolumeGather::ClampXShift, 7u); + const auto modeY = field(VolumeGather::ClampYShift, 7u); + const auto modeZ = field(VolumeGather::ClampZShift, 7u); + const auto border = field(VolumeGather::BorderShift, 3u); + const auto columns = resolve({starts[0], Binary(state, spv::OpIAdd, i32, starts[0], ConstantI32(state, 1))}, 0u, modeX); + const auto rows = resolve({starts[1], Binary(state, spv::OpIAdd, i32, starts[1], ConstantI32(state, 1))}, 1u, modeY); + const auto slice = resolve({starts[2]}, 2u, modeZ)[0]; const auto vectorType = ImageVectorType(state, numericClass, 4); const auto scalarType = ImageScalarType(state, numericClass); const auto component = ImageConversionFormat(access.image).format == IrBufferFormat::Invalid ? ImageGatherComponent(EffectiveDmask(access.mem)) : 0u; + std::uint32_t borderValue = 0u; + if (numericClass == IrTextureNumericClass::Float) { + const auto white = equal(border, VolumeGather::BorderOpaqueWhite); + const auto opaque = component == 3u ? Binary(state, spv::OpLogicalOr, boolean, white, equal(border, VolumeGather::BorderOpaqueBlack)) : white; + borderValue = Select(state, f32, opaque, ConstantF32(state, 0x3f800000u), ConstantF32(state, 0u)); + } else { + borderValue = numericClass == IrTextureNumericClass::Sint ? ConstantI32(state, 0) : ConstantU32(state, 0u); + } constexpr std::uint32_t footprint[4][2] = {{0u, 1u}, {1u, 1u}, {1u, 0u}, {0u, 0u}}; std::uint32_t values[4] = {}; for (std::uint32_t index = 0; index < 4u; index++) { - const auto sampleCoord = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 3), sampleCoord, columns[footprint[index][0]], rows[footprint[index][1]], slice); + const auto& column = columns[footprint[index][0]]; + const auto& row = rows[footprint[index][1]]; + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(state, 3), coord, column.first, row.first, slice.first); const auto texel = state.module.AllocateId(); - state.module.AddFunction(spv::OpImageSampleExplicitLod, vectorType, texel, sampled, sampleCoord, spv::ImageOperandsLodMask, ZeroF32(state)); - values[index] = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, scalarType, values[index], texel, component); + state.module.AddFunction(spv::OpImageFetch, vectorType, texel, image, coord, spv::ImageOperandsLodMask, ConstantU32(state, 0u)); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, scalarType, value, texel, component); + const auto outside = Binary(state, spv::OpLogicalOr, boolean, Binary(state, spv::OpLogicalOr, boolean, column.second, row.second), slice.second); + values[index] = Select(state, scalarType, outside, borderValue, value); } const auto result = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vectorType, result, values[0], values[1], values[2], values[3]); From a153f095d57ed274fd499db884d4f59a22cc0fdf Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:42:36 +0200 Subject: [PATCH 03/14] fix(recompiler): snap 3D gather coordinates to 1/256 texel before the footprint --- .../tests/execution/ImageGatherVolume.cpp | 15 ++++++++++----- .../SpirvBackend/src/SpirvImageEmitter.cpp | 16 ++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp index 3a8ad16b3a7b..54e8677c0833 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -26,8 +26,8 @@ constexpr std::uint32_t Format8888UNorm = 56; constexpr std::uint32_t Type3D = 10; constexpr std::uint32_t IdentitySwizzle = 0xfacu; constexpr std::uint32_t BlueGreenRedAlpha = 0xf2eu; -constexpr std::array Positions{-9.3f, -4.8f, -1.3f, -0.7f, -0.2f, 0.3f, 0.9f, 1.6f, 2.2f, 3.7f, - -1.3f, -0.7f, -0.2f, 0.3f, 0.8f, 1.6f, 0.3f, 1.9f, -0.3f, -1.6f}; +constexpr std::array Positions{-9.3f, -4.8f, -1.3f, -0.7f, -0.2f, 0.3f, 1.4985f, 1.6f, 3.499f, 3.7f, + -1.3f, -0.7f, -0.2f, -0.5015f, 0.8f, 1.6f, 0.3f, 1.9f, -0.3f, -1.6f}; constexpr std::array PositionExtents{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, -1, -1}; alignas(256) std::array Input{}; @@ -172,6 +172,11 @@ std::array, 2> Footprint(int first, int extent, std::uint32_t return result; } +int Snapped(float coordinate, int extent, int offset) { + const auto steps = static_cast(std::floor(static_cast(coordinate) * extent * 256.0 + 0.5)) - offset; + return static_cast(steps >= 0 ? steps / 256 : -((-steps + 255) / 256)); +} + std::uint32_t Channel(const Variant& variant, const std::array& texel) { const auto select = (variant.volume.swizzle >> (3u * variant.channel)) & 7u; return texel.at(select - 4u); @@ -191,9 +196,9 @@ std::array Expected(std::uint32_t tid, const Variant& variant) const float u = std::bit_cast(Input[tid * 4u + 1u]); const float v = std::bit_cast(Input[tid * 4u + 2u]); const float r = std::bit_cast(Input[tid * 4u + 3u]); - const auto columns = Footprint(static_cast(std::floor(u * static_cast(width) - 0.5f)), width, variant.sampler.clamp); - const auto rows = Footprint(static_cast(std::floor(v * static_cast(height) - 0.5f)), height, variant.sampler.clamp); - const auto slice = Single(static_cast(std::floor(r * static_cast(depth))), depth, (variant.sampler.clamp & 6u) == 4u ? (variant.sampler.clamp & 1u) | 2u : variant.sampler.clamp); + const auto columns = Footprint(Snapped(u, width, 128), width, variant.sampler.clamp); + const auto rows = Footprint(Snapped(v, height, 128), height, variant.sampler.clamp); + const auto slice = Single(Snapped(r, depth, 0), depth, (variant.sampler.clamp & 6u) == 4u ? (variant.sampler.clamp & 1u) | 2u : variant.sampler.clamp); const auto value = [&](std::size_t column, std::size_t row) { if (!columns[column] || !rows[row] || !slice) return BorderValue(variant); return Channel(variant, Texel(volume, level, static_cast(*columns[column]), static_cast(*rows[row]), static_cast(*slice))); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 0e937c6bee7b..42d198474011 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -860,15 +860,19 @@ std::uint32_t EmitVolumeGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAcce extents[axis] = Unary(state, spv::OpBitcast, i32, extent); const auto coord = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, f32, coord, setup.coord, axis); - auto scaled = Binary(state, spv::OpFMul, f32, coord, Unary(state, spv::OpConvertUToF, f32, extent)); + const auto scaled = Binary(state, spv::OpFMul, f32, coord, Unary(state, spv::OpConvertUToF, f32, extent)); state.module.AddAnnotation(spv::OpDecorate, scaled, spv::DecorationNoContraction); + const auto steps = Binary(state, spv::OpFMul, f32, scaled, ConstantF32(state, 0x43800000u)); + state.module.AddAnnotation(spv::OpDecorate, steps, spv::DecorationNoContraction); + const auto halfStep = Binary(state, spv::OpFAdd, f32, steps, ConstantF32(state, 0x3f000000u)); + state.module.AddAnnotation(spv::OpDecorate, halfStep, spv::DecorationNoContraction); + const auto floored = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, f32, floored, GlslStd450(state), GLSLstd450Floor, halfStep); + auto snapped = Unary(state, spv::OpConvertFToS, i32, floored); if (axis < 2u) { - scaled = Binary(state, spv::OpFSub, f32, scaled, ConstantF32(state, 0x3f000000u)); - state.module.AddAnnotation(spv::OpDecorate, scaled, spv::DecorationNoContraction); + snapped = Binary(state, spv::OpISub, i32, snapped, ConstantI32(state, 128)); } - const auto floored = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, f32, floored, GlslStd450(state), GLSLstd450Floor, scaled); - starts[axis] = Unary(state, spv::OpConvertFToS, i32, floored); + starts[axis] = Binary(state, spv::OpShiftRightArithmetic, i32, snapped, ConstantI32(state, 8)); } const auto resolve = [&](std::initializer_list indices, std::uint32_t axis, std::uint32_t mode) { const auto extent = extents[axis]; From a6def73399a102d90fc239962e8f22ca2243a9f7 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:02:58 +0200 Subject: [PATCH 04/14] fix(recompiler): reduce 3D gather coordinates by addressing mode and snap their exact product --- .../tests/execution/ImageGatherVolume.cpp | 36 +++++++-- .../SpirvBackend/src/SpirvImageEmitter.cpp | 74 ++++++++++++++----- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp index 54e8677c0833..a5ca0d307cef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -28,11 +29,18 @@ constexpr std::uint32_t IdentitySwizzle = 0xfacu; constexpr std::uint32_t BlueGreenRedAlpha = 0xf2eu; constexpr std::array Positions{-9.3f, -4.8f, -1.3f, -0.7f, -0.2f, 0.3f, 1.4985f, 1.6f, 3.499f, 3.7f, -1.3f, -0.7f, -0.2f, -0.5015f, 0.8f, 1.6f, 0.3f, 1.9f, -0.3f, -1.6f}; +constexpr std::array Extremes{3.0e6f, -3.0e6f, 1.0e9f, -std::numeric_limits::infinity(), std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN(), 65536.3f, -1000.3f}; +constexpr std::array WideColumns{ + 0x3f4412b9u, 0x3f78caa4u, 0x3dbebd8cu, 0x3f1fa5e0u, 0x3f6f111au, 0x3f656d67u, 0x3f03ec82u, 0x3f6869bcu, 0x3f6b91beu, 0x3f305e22u, 0x3f75a2a2u, + 0x3f0dbbe2u, 0x3eb2b6a3u, 0x3dc6ee00u, 0x3f4d5f12u, 0x3f69f2d2u, 0x3dc59097u, 0x3f050867u, 0x3e74d30bu, 0x3f436405u, 0x3f5924b9u, 0x3e4a2c04u, + 0x3eac3af2u, 0x3eb0277fu, 0x3ebd4a8eu, 0x3f67d0deu, 0x3ec6acbeu, 0x3f588bdbu, 0x3f4c84b1u, 0x3f62f419u, 0x3f452e9eu, 0x3f7e97a1u, +}; +constexpr std::uint32_t WideWidth = 3001; constexpr std::array PositionExtents{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, -1, -1}; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(4096) std::array Texels{}; +alignas(4096) std::array Texels{}; alignas(256) constexpr std::array GatherRed{ 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0110, 0x00820603, 0xbf8c3f70, 0xe0781000, @@ -140,6 +148,13 @@ void FillInput(const Volume& volume) { Input[tid * 4u + 1u] = std::bit_cast(Position(tid % 20u, width) / static_cast(width)); Input[tid * 4u + 2u] = std::bit_cast(Position((tid * 7u + 3u) % 20u, height) / static_cast(height)); Input[tid * 4u + 3u] = std::bit_cast((Position((tid * 11u + 5u) % 20u, depth) + 0.5f) / static_cast(depth)); + if (volume.width == WideWidth) { + Input[tid * 4u + 1u] = WideColumns[tid]; + Input[tid * 4u + 2u] = std::bit_cast(0.25f); + Input[tid * 4u + 3u] = std::bit_cast(0.25f); + } else if (tid >= Threads - Extremes.size()) { + Input[tid * 4u + 1u + tid % 3u] = std::bit_cast(Extremes[tid - (Threads - Extremes.size())]); + } } } @@ -172,8 +187,16 @@ std::array, 2> Footprint(int first, int extent, std::uint32_t return result; } -int Snapped(float coordinate, int extent, int offset) { - const auto steps = static_cast(std::floor(static_cast(coordinate) * extent * 256.0 + 0.5)) - offset; +float Reduced(float coordinate, std::uint32_t clamp) { + if (std::isnan(coordinate)) return 0.0f; + if (std::isinf(coordinate)) coordinate = coordinate < 0.0f ? -4.0f : 4.0f; + if (clamp == 0u) return coordinate - std::floor(coordinate); + if (clamp == 1u) return coordinate - 2.0f * std::floor(coordinate * 0.5f); + return std::clamp(coordinate, -4.0f, 4.0f); +} + +int Snapped(float coordinate, int extent, int offset, std::uint32_t clamp) { + const auto steps = static_cast(std::floor(static_cast(Reduced(coordinate, clamp)) * extent * 256.0 + 0.5)) - offset; return static_cast(steps >= 0 ? steps / 256 : -((-steps + 255) / 256)); } @@ -196,9 +219,9 @@ std::array Expected(std::uint32_t tid, const Variant& variant) const float u = std::bit_cast(Input[tid * 4u + 1u]); const float v = std::bit_cast(Input[tid * 4u + 2u]); const float r = std::bit_cast(Input[tid * 4u + 3u]); - const auto columns = Footprint(Snapped(u, width, 128), width, variant.sampler.clamp); - const auto rows = Footprint(Snapped(v, height, 128), height, variant.sampler.clamp); - const auto slice = Single(Snapped(r, depth, 0), depth, (variant.sampler.clamp & 6u) == 4u ? (variant.sampler.clamp & 1u) | 2u : variant.sampler.clamp); + const auto columns = Footprint(Snapped(u, width, 128, variant.sampler.clamp), width, variant.sampler.clamp); + const auto rows = Footprint(Snapped(v, height, 128, variant.sampler.clamp), height, variant.sampler.clamp); + const auto slice = Single(Snapped(r, depth, 0, variant.sampler.clamp), depth, (variant.sampler.clamp & 6u) == 4u ? (variant.sampler.clamp & 1u) | 2u : variant.sampler.clamp); const auto value = [&](std::size_t column, std::size_t row) { if (!columns[column] || !rows[row] || !slice) return BorderValue(variant); return Channel(variant, Texel(volume, level, static_cast(*columns[column]), static_cast(*rows[row]), static_cast(*slice))); @@ -279,6 +302,7 @@ int main() { const SamplerState lodState{2, 0, 4u << 9u, 0x100u | (0xfffu << 12u), 0x100u | (1u << 20u) | (1u << 22u) | (2u << 26u)}; Run(*device, {GatherRed, 0, {8, 4, 4, 0, 1}, lodState, "3D gather of red ignores the sampler's LOD clamp, bias, filters and anisotropy"}); Run(*device, {GatherRed, 0, {8, 4, 4, 1, 1}, {0}, "3D gather of red reads the base level"}); + Run(*device, {GatherRed, 0, {WideWidth, 2, 2}, {2}, "3D gather of red from a 3001 texel wide volume at 1/256 texel boundaries"}); Run(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {1}, "3D gather of red through a blue-green-red-alpha swizzle"}); RunThrows(*device, {GatherRed, 0, cube, {6, 3}, "3D gather with a border color table"}, "a 3D gather with a border color table is not implemented"); RunThrows(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {6, 2}, "3D gather of a swizzled volume with border addressing"}, "a 3D gather of a swizzled texture with border addressing is not implemented"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 42d198474011..0cfd2a27645a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -852,23 +852,66 @@ std::uint32_t EmitVolumeGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAcce const auto image = LoadSampledImageDescriptor(state, access.mem.resource); const auto size = state.module.AllocateId(); state.module.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 3), size, image, ConstantU32(state, 0)); + const std::uint32_t modes[3] = {field(VolumeGather::ClampXShift, 7u), field(VolumeGather::ClampYShift, 7u), field(VolumeGather::ClampZShift, 7u)}; + const auto exact = [&](std::uint32_t op, std::uint32_t type, std::uint32_t lhs, std::uint32_t rhs) { + const auto id = Binary(state, op, type, lhs, rhs); + state.module.AddAnnotation(spv::OpDecorate, id, spv::DecorationNoContraction); + return id; + }; + const auto floorOf = [&](std::uint32_t value) { + const auto id = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, f32, id, GlslStd450(state), GLSLstd450Floor, value); + return id; + }; + const auto shifted = [&](std::uint32_t op, std::uint32_t value, std::uint32_t amount) { return Binary(state, op, u32, value, amount); }; + const auto below = [&](std::uint32_t value, std::uint32_t literal) { return Binary(state, spv::OpULessThan, boolean, value, ConstantU32(state, literal)); }; std::uint32_t extents[3] = {}; std::uint32_t starts[3] = {}; for (std::uint32_t axis = 0; axis < 3u; axis++) { const auto extent = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, u32, extent, size, axis); extents[axis] = Unary(state, spv::OpBitcast, i32, extent); - const auto coord = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, f32, coord, setup.coord, axis); - const auto scaled = Binary(state, spv::OpFMul, f32, coord, Unary(state, spv::OpConvertUToF, f32, extent)); - state.module.AddAnnotation(spv::OpDecorate, scaled, spv::DecorationNoContraction); - const auto steps = Binary(state, spv::OpFMul, f32, scaled, ConstantF32(state, 0x43800000u)); - state.module.AddAnnotation(spv::OpDecorate, steps, spv::DecorationNoContraction); - const auto halfStep = Binary(state, spv::OpFAdd, f32, steps, ConstantF32(state, 0x3f000000u)); - state.module.AddAnnotation(spv::OpDecorate, halfStep, spv::DecorationNoContraction); - const auto floored = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, f32, floored, GlslStd450(state), GLSLstd450Floor, halfStep); - auto snapped = Unary(state, spv::OpConvertFToS, i32, floored); + const auto raw = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, f32, raw, setup.coord, axis); + const auto finite = Select(state, f32, Unary(state, spv::OpIsInf, boolean, raw), Select(state, f32, Binary(state, spv::OpFOrdLessThan, boolean, raw, ConstantF32(state, 0u)), ConstantF32(state, 0xc0800000u), ConstantF32(state, 0x40800000u)), raw); + const auto coord = Select(state, f32, Unary(state, spv::OpIsNan, boolean, raw), ConstantF32(state, 0u), finite); + const auto wrapped = exact(spv::OpFSub, f32, coord, floorOf(coord)); + const auto mirrored = exact(spv::OpFSub, f32, coord, exact(spv::OpFMul, f32, floorOf(exact(spv::OpFMul, f32, coord, ConstantF32(state, 0x3f000000u))), ConstantF32(state, 0x40000000u))); + const auto bounded = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, f32, bounded, GlslStd450(state), GLSLstd450FClamp, coord, ConstantF32(state, 0xc0800000u), ConstantF32(state, 0x40800000u)); + const auto reduced = Select(state, f32, equal(modes[axis], 0u), wrapped, Select(state, f32, equal(modes[axis], 1u), mirrored, bounded)); + const auto bits = Unary(state, spv::OpBitcast, u32, reduced); + const auto negative = Binary(state, spv::OpINotEqual, boolean, shifted(spv::OpShiftRightLogical, bits, ConstantU32(state, 31u)), ConstantU32(state, 0u)); + const auto exponent = EmitAndConstant(state, shifted(spv::OpShiftRightLogical, bits, ConstantU32(state, 23u)), 0xffu); + const auto normal = Binary(state, spv::OpINotEqual, boolean, exponent, ConstantU32(state, 0u)); + const auto mantissa = Binary(state, spv::OpBitwiseOr, u32, EmitAndConstant(state, bits, 0x7fffffu), Select(state, u32, normal, ConstantU32(state, 0x800000u), ConstantU32(state, 0u))); + const auto shift = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, u32, shift, GlslStd450(state), GLSLstd450UMin, Binary(state, spv::OpISub, u32, ConstantU32(state, 142u), Select(state, u32, normal, exponent, ConstantU32(state, 1u))), ConstantU32(state, 40u)); + const auto product = state.module.AllocateId(); + state.module.AddFunction(spv::OpUMulExtended, TypeU32Pair(state), product, mantissa, extent); + const auto productLow = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, productLow, product, 0u); + const auto productHigh = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, productHigh, product, 1u); + const auto halfShift = Binary(state, spv::OpISub, u32, shift, ConstantU32(state, 1u)); + const auto halfInLow = below(halfShift, 32u); + const auto halfLow = Select(state, u32, halfInLow, shifted(spv::OpShiftLeftLogical, ConstantU32(state, 1u), halfShift), ConstantU32(state, 0u)); + const auto halfHigh = Select(state, u32, halfInLow, ConstantU32(state, 0u), shifted(spv::OpShiftLeftLogical, ConstantU32(state, 1u), Binary(state, spv::OpISub, u32, halfShift, ConstantU32(state, 32u)))); + const auto borrow = Select(state, u32, negative, ConstantU32(state, 1u), ConstantU32(state, 0u)); + const auto biasLow = Binary(state, spv::OpISub, u32, halfLow, borrow); + const auto biasHigh = Binary(state, spv::OpISub, u32, halfHigh, Select(state, u32, Binary(state, spv::OpLogicalAnd, boolean, negative, Unary(state, spv::OpLogicalNot, boolean, halfInLow)), ConstantU32(state, 1u), ConstantU32(state, 0u))); + const auto sum = state.module.AllocateId(); + state.module.AddFunction(spv::OpIAddCarry, TypeU32Pair(state), sum, productLow, biasLow); + const auto sumLow = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, sumLow, sum, 0u); + const auto carry = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, carry, sum, 1u); + const auto sumHigh = Binary(state, spv::OpIAdd, u32, Binary(state, spv::OpIAdd, u32, productHigh, biasHigh), carry); + const auto inLow = below(shift, 32u); + const auto fromLow = Binary(state, spv::OpBitwiseOr, u32, shifted(spv::OpShiftRightLogical, sumLow, shift), shifted(spv::OpShiftLeftLogical, sumHigh, Binary(state, spv::OpISub, u32, ConstantU32(state, 32u), shift))); + const auto fromHigh = shifted(spv::OpShiftRightLogical, sumHigh, Binary(state, spv::OpISub, u32, shift, ConstantU32(state, 32u))); + const auto magnitude = Unary(state, spv::OpBitcast, i32, Select(state, u32, inLow, fromLow, fromHigh)); + auto snapped = Select(state, i32, negative, Unary(state, spv::OpSNegate, i32, magnitude), magnitude); if (axis < 2u) { snapped = Binary(state, spv::OpISub, i32, snapped, ConstantI32(state, 128)); } @@ -922,13 +965,10 @@ std::uint32_t EmitVolumeGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAcce } return resolved; }; - const auto modeX = field(VolumeGather::ClampXShift, 7u); - const auto modeY = field(VolumeGather::ClampYShift, 7u); - const auto modeZ = field(VolumeGather::ClampZShift, 7u); const auto border = field(VolumeGather::BorderShift, 3u); - const auto columns = resolve({starts[0], Binary(state, spv::OpIAdd, i32, starts[0], ConstantI32(state, 1))}, 0u, modeX); - const auto rows = resolve({starts[1], Binary(state, spv::OpIAdd, i32, starts[1], ConstantI32(state, 1))}, 1u, modeY); - const auto slice = resolve({starts[2]}, 2u, modeZ)[0]; + const auto columns = resolve({starts[0], Binary(state, spv::OpIAdd, i32, starts[0], ConstantI32(state, 1))}, 0u, modes[0]); + const auto rows = resolve({starts[1], Binary(state, spv::OpIAdd, i32, starts[1], ConstantI32(state, 1))}, 1u, modes[1]); + const auto slice = resolve({starts[2]}, 2u, modes[2])[0]; const auto vectorType = ImageVectorType(state, numericClass, 4); const auto scalarType = ImageScalarType(state, numericClass); const auto component = ImageConversionFormat(access.image).format == IrBufferFormat::Invalid ? ImageGatherComponent(EffectiveDmask(access.mem)) : 0u; From a3834314f8f5b532cc93963b10b54a42a2d1d9bc Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:43:43 +0200 Subject: [PATCH 05/14] fix(recompiler): pair a 3D gather's state only with its own sampler and keep z half border out of border handling A 3D sample of the same image through another sampler no longer feeds the gather's state, z modes 4 and 5 no longer count as border addressing, a null image no longer divides by a zero extent, and the test covers per-axis modes, rounding ties, denormals, null images and the vertex stage. --- .../tests/execution/ImageGatherVolume.cpp | 143 +++++++++++++++--- .../IrMetadata/Resources.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 7 +- .../Optimization/src/ResourceTracker.cpp | 7 +- core/shader/recompiler/ShaderDiskCache.cpp | 8 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 4 +- 6 files changed, 140 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp index a5ca0d307cef..e34afaa12fa2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -36,6 +36,7 @@ constexpr std::array WideColumns{ 0x3eac3af2u, 0x3eb0277fu, 0x3ebd4a8eu, 0x3f67d0deu, 0x3ec6acbeu, 0x3f588bdbu, 0x3f4c84b1u, 0x3f62f419u, 0x3f452e9eu, 0x3f7e97a1u, }; constexpr std::uint32_t WideWidth = 3001; +constexpr std::array Ties{127.5f / 2048.0f, -1.0f / 4096.0f, 0x1.0p-20f, -0.0f, -0x1.0p-149f, 255.5f / 1024.0f, 0x1.0p-149f, 383.5f / 1024.0f}; constexpr std::array PositionExtents{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, -1, -1}; alignas(256) std::array Input{}; @@ -54,6 +55,19 @@ alignas(256) constexpr std::array GatherAlpha{ 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0810, 0x00820603, 0xbf8c3f70, 0xe0781000, 0x80010601, 0xbf810000, }; +alignas(256) constexpr std::array GatherRedBesideSample{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0110, 0x00820603, 0xf09c0110, 0x00a20a03, + 0xbf8c3f70, 0x10141480, 0x060c1506, 0xe0781000, 0x80010601, 0xbf810000, +}; +alignas(256) constexpr std::array VertexGatherRed{ + 0x34020a84, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0110, 0x00820603, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xf80008cf, 0x09080706, 0xbf810000, +}; +alignas(256) constexpr std::array PixelCode{0x7e0002f2u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u}; +constexpr std::uint32_t TargetExtent = 64u; +alignas(256) std::array Pixels{}; + +enum class Inputs { Positions, Ties }; struct Volume { std::uint32_t width; @@ -65,19 +79,27 @@ struct Volume { }; struct SamplerState { - std::uint32_t clamp; + std::array clamp; std::uint32_t border = 0; std::uint32_t extra0 = 0; std::uint32_t word1 = 0xfffu << 12u; std::uint32_t word2 = 0; }; +std::array Same(std::uint32_t clamp) { + return {clamp, clamp, clamp}; +} + struct Variant { std::span code; std::uint32_t channel; Volume volume; SamplerState sampler; std::string name; + Inputs inputs = Inputs::Positions; + bool vertex = false; + bool null = false; + SamplerState other{Same(2)}; }; std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -100,7 +122,7 @@ std::array TextureDescriptor(const Volume& volume) { } std::array SamplerDescriptor(const SamplerState& sampler) { - return {sampler.clamp | (sampler.clamp << 3u) | (sampler.clamp << 6u) | sampler.extra0, sampler.word1, sampler.word2, sampler.border << 30u}; + return {sampler.clamp[0] | (sampler.clamp[1] << 3u) | (sampler.clamp[2] << 6u) | sampler.extra0, sampler.word1, sampler.word2, sampler.border << 30u}; } std::uint32_t LevelStart(const Volume& volume, std::uint32_t level) { @@ -139,7 +161,7 @@ float Position(std::uint32_t index, std::uint32_t extent) { return Positions[index] + static_cast(PositionExtents[index] * static_cast(extent)); } -void FillInput(const Volume& volume) { +void FillInput(const Volume& volume, Inputs inputs) { const auto width = std::max(volume.width >> volume.baseLevel, 1u); const auto height = std::max(volume.height >> volume.baseLevel, 1u); const auto depth = std::max(volume.depth >> volume.baseLevel, 1u); @@ -148,7 +170,9 @@ void FillInput(const Volume& volume) { Input[tid * 4u + 1u] = std::bit_cast(Position(tid % 20u, width) / static_cast(width)); Input[tid * 4u + 2u] = std::bit_cast(Position((tid * 7u + 3u) % 20u, height) / static_cast(height)); Input[tid * 4u + 3u] = std::bit_cast((Position((tid * 11u + 5u) % 20u, depth) + 0.5f) / static_cast(depth)); - if (volume.width == WideWidth) { + if (inputs == Inputs::Ties) { + for (std::uint32_t axis = 0; axis < 3u; ++axis) Input[tid * 4u + 1u + axis] = std::bit_cast(Ties[(tid + axis * 3u + tid / Ties.size()) % Ties.size()]); + } else if (volume.width == WideWidth) { Input[tid * 4u + 1u] = WideColumns[tid]; Input[tid * 4u + 2u] = std::bit_cast(0.25f); Input[tid * 4u + 3u] = std::bit_cast(0.25f); @@ -219,9 +243,10 @@ std::array Expected(std::uint32_t tid, const Variant& variant) const float u = std::bit_cast(Input[tid * 4u + 1u]); const float v = std::bit_cast(Input[tid * 4u + 2u]); const float r = std::bit_cast(Input[tid * 4u + 3u]); - const auto columns = Footprint(Snapped(u, width, 128, variant.sampler.clamp), width, variant.sampler.clamp); - const auto rows = Footprint(Snapped(v, height, 128, variant.sampler.clamp), height, variant.sampler.clamp); - const auto slice = Single(Snapped(r, depth, 0, variant.sampler.clamp), depth, (variant.sampler.clamp & 6u) == 4u ? (variant.sampler.clamp & 1u) | 2u : variant.sampler.clamp); + const auto& clamp = variant.sampler.clamp; + const auto columns = Footprint(Snapped(u, width, 128, clamp[0]), width, clamp[0]); + const auto rows = Footprint(Snapped(v, height, 128, clamp[1]), height, clamp[1]); + const auto slice = Single(Snapped(r, depth, 0, clamp[2]), depth, (clamp[2] & 6u) == 4u ? (clamp[2] & 1u) | 2u : clamp[2]); const auto value = [&](std::size_t column, std::size_t row) { if (!columns[column] || !rows[row] || !slice) return BorderValue(variant); return Channel(variant, Texel(volume, level, static_cast(*columns[column]), static_cast(*rows[row]), static_cast(*slice))); @@ -229,16 +254,23 @@ std::array Expected(std::uint32_t tid, const Variant& variant) return {value(0, 1), value(1, 1), value(1, 0), value(0, 0)}; } -ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const Variant& variant) { - std::vector userData(20, 0u); +std::vector UserData(const Variant& variant) { + std::vector userData(24, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); - const auto texture = TextureDescriptor(variant.volume); + const auto texture = variant.null ? std::array{} : TextureDescriptor(variant.volume); const auto sampler = SamplerDescriptor(variant.sampler); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); std::copy(texture.begin(), texture.end(), userData.begin() + 8); std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const auto other = SamplerDescriptor(variant.other); + std::copy(other.begin(), other.end(), userData.begin() + 20); + return userData; +} + +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const Variant& variant) { + const auto userData = UserData(variant); const std::array memory{{{reinterpret_cast(variant.code.data()), std::as_bytes(variant.code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -251,10 +283,58 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const return ShaderRecompiler::Recompile(request); } +void Draw(AgcDriver::VulkanDevice& device, const Variant& variant) { + const auto userData = UserData(variant); + const std::array memory{{{reinterpret_cast(variant.code.data()), std::as_bytes(variant.code)}}}; + auto target = device.Target(); + ShaderRecompiler::RecompileRequest vertexRequest{ + {ShaderStage::Vertex, reinterpret_cast(variant.code.data()), variant.code, 0, {}}, + {32, 0, userData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, memory}, + target, + {0, 0, 0, 128} + }; + vertexRequest.useCache = false; + const auto vertex = ShaderRecompiler::Recompile(vertexRequest); + Require(vertex.registrationModule == target.registrationModules, variant.name + ": the vertex shader was not emitted in the device's module form"); + const auto vertexPush = static_cast(vertex.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixelInfo{}; + pixelInfo.wave32 = true; + pixelInfo.targetOutputMode[0] = 9u; + pixelInfo.targetExportMapping.fill(0xe4u); + ShaderRecompiler::RecompileRequest pixelRequest{ + {ShaderStage::Fragment, reinterpret_cast(PixelCode.data()), PixelCode, 0, {}}, + {32, 0, {}, std::nullopt, pixelInfo, std::nullopt, {}}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto pixel = ShaderRecompiler::Recompile(pixelRequest); + const std::array shaders{{{ShaderStage::Vertex, &vertex, 0u}, {ShaderStage::Fragment, &pixel, PixelPushOffset(vertexPush, target)}}}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 32u, 32u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {TargetExtent, TargetExtent}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {TargetExtent, TargetExtent}; + state.topology = VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + state.viewport = {0.0f, static_cast(TargetExtent), static_cast(TargetExtent), -static_cast(TargetExtent), 0.0f, 1.0f}; + state.scissor = {{0, 0}, {TargetExtent, TargetExtent}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15u; + state.blends = {state.blend}; + const AgcDriver::Pm4::DrawParameters draw{0u, Threads, 0u, 1u, 0u, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + void Dispatch(AgcDriver::VulkanDevice& device, const Variant& variant) { FillTexels(variant.volume); - FillInput(variant.volume); + FillInput(variant.volume, variant.inputs); Output.fill(-1.0f); + if (variant.vertex) { + Draw(device, variant); + return; + } const auto result = Compile(device, variant); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(variant.code.data())); device.WaitIdle(); @@ -271,6 +351,14 @@ void Run(AgcDriver::VulkanDevice& device, const Variant& variant) { } } +void RunNull(AgcDriver::VulkanDevice& device, Variant variant) { + variant.null = true; + Dispatch(device, variant); + for (std::uint32_t index = 0; index < Output.size(); ++index) { + Require(Output[index] == 0.0f, variant.name + ": value " + std::to_string(index) + " is " + std::to_string(Output[index]) + ", expected 0"); + } +} + void RunThrows(AgcDriver::VulkanDevice& device, const Variant& variant, const std::string& message) { try { Dispatch(device, variant); @@ -294,18 +382,33 @@ int main() { for (std::uint32_t clamp = 0; clamp < 8u; ++clamp) { for (std::uint32_t border = 0; border < (clamp >= 4u ? 3u : 1u); ++border) { const auto suffix = std::string(clampNames[clamp]) + (clamp >= 4u ? std::string(", ") + borderNames[border] : std::string()); - Run(*device, {GatherRed, 0, cube, {clamp, border}, "3D gather of red, " + suffix}); - Run(*device, {GatherAlpha, 3, cube, {clamp, border}, "3D gather of alpha, " + suffix}); - Run(*device, {GatherBlue, 2, odd, {clamp, border}, "3D gather of blue from a 7x5x3 volume, " + suffix}); + Run(*device, {GatherRed, 0, cube, {Same(clamp), border}, "3D gather of red, " + suffix}); + Run(*device, {GatherAlpha, 3, cube, {Same(clamp), border}, "3D gather of alpha, " + suffix}); + Run(*device, {GatherBlue, 2, odd, {Same(clamp), border}, "3D gather of blue from a 7x5x3 volume, " + suffix}); + Run(*device, {GatherRed, 0, cube, {Same(clamp), border}, "3D gather of red at rounding ties and denormals, " + suffix, Inputs::Ties}); } } - const SamplerState lodState{2, 0, 4u << 9u, 0x100u | (0xfffu << 12u), 0x100u | (1u << 20u) | (1u << 22u) | (2u << 26u)}; + const std::array, 6> mixed{{{0, 6, 1}, {6, 1, 0}, {1, 0, 7}, {2, 2, 4}, {3, 5, 5}, {4, 7, 2}}}; + for (const auto& clamps : mixed) { + const auto suffix = std::string(clampNames[clamps[0]]) + " / " + clampNames[clamps[1]] + " / " + clampNames[clamps[2]]; + Run(*device, {GatherRed, 0, cube, {clamps, 2}, "3D gather of red with per-axis modes " + suffix}); + Run(*device, {GatherBlue, 2, odd, {clamps, 1}, "3D gather of blue from a 7x5x3 volume with per-axis modes " + suffix}); + Run(*device, {VertexGatherRed, 0, cube, {clamps, 2}, "vertex shader 3D gather of red with per-axis modes " + suffix, Inputs::Positions, true}); + } + Run(*device, {VertexGatherRed, 0, odd, {Same(5), 2}, "vertex shader 3D gather of red from a 7x5x3 volume, mirror once to half border", Inputs::Positions, true}); + Run(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {{2, 2, 4}}, "3D gather of red through a blue-green-red-alpha swizzle with half border only on z"}); + Run(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {{2, 3, 5}}, "3D gather of red through a blue-green-red-alpha swizzle with mirror once to half border only on z"}); + Run(*device, {GatherRedBesideSample, 0, cube, {Same(0)}, "3D gather of red, wrap, beside a 3D sample through a clamp to border sampler", Inputs::Positions, false, false, {Same(6), 2}}); + Run(*device, {GatherRedBesideSample, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {Same(1)}, "3D gather of red through a swizzle, mirror, beside a 3D sample through a clamp to border sampler", Inputs::Positions, false, false, {Same(7), 1}}); + const SamplerState lodState{Same(2), 0, 4u << 9u, 0x100u | (0xfffu << 12u), 0x100u | (1u << 20u) | (1u << 22u) | (2u << 26u)}; Run(*device, {GatherRed, 0, {8, 4, 4, 0, 1}, lodState, "3D gather of red ignores the sampler's LOD clamp, bias, filters and anisotropy"}); - Run(*device, {GatherRed, 0, {8, 4, 4, 1, 1}, {0}, "3D gather of red reads the base level"}); - Run(*device, {GatherRed, 0, {WideWidth, 2, 2}, {2}, "3D gather of red from a 3001 texel wide volume at 1/256 texel boundaries"}); - Run(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {1}, "3D gather of red through a blue-green-red-alpha swizzle"}); - RunThrows(*device, {GatherRed, 0, cube, {6, 3}, "3D gather with a border color table"}, "a 3D gather with a border color table is not implemented"); - RunThrows(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {6, 2}, "3D gather of a swizzled volume with border addressing"}, "a 3D gather of a swizzled texture with border addressing is not implemented"); + Run(*device, {GatherRed, 0, {8, 4, 4, 1, 1}, {Same(0)}, "3D gather of red reads the base level"}); + Run(*device, {GatherRed, 0, {WideWidth, 2, 2}, {Same(2)}, "3D gather of red from a 3001 texel wide volume at 1/256 texel boundaries"}); + Run(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {Same(1)}, "3D gather of red through a blue-green-red-alpha swizzle"}); + RunThrows(*device, {GatherRed, 0, cube, {Same(6), 3}, "3D gather with a border color table"}, "a 3D gather with a border color table is not implemented"); + RunThrows(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {Same(6), 2}, "3D gather of a swizzled volume with border addressing"}, "a 3D gather of a swizzled texture with border addressing is not implemented"); + RunNull(*device, {GatherRed, 0, cube, {Same(0)}, "3D gather of red from a null image descriptor, wrap"}); + RunNull(*device, {VertexGatherRed, 0, cube, {Same(1)}, "vertex shader 3D gather of red from a null image descriptor, mirror", Inputs::Positions, true}); std::puts("image 3D gather tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 571cb0d745ff..cba24ca9586f 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -175,6 +175,7 @@ struct SampledResourcePair { std::uint32_t image = 0; std::uint32_t sampler = 0; std::uint32_t firstUsePc = 0; + bool volumeGather = false; bool operator==(const SampledResourcePair& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ffa2690385ac..a9eecc320b99 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -2383,10 +2383,11 @@ std::uint32_t ResourceMaterializer::VolumeGatherState(const ShaderInfo& info, co const auto& image = info.images[index]; const auto& descriptor = snapshot.images[index]; if (image.table != NoTable) throw std::runtime_error("a 3D gather of an image table entry is not implemented"); - if (descriptor.dwordCount != 8u || nullImageDescriptor(image, descriptor)) return 0u; + if (descriptor.dwordCount != 8u) throw std::runtime_error("a 3D gather of an image without an 8-dword descriptor is not implemented"); + if (nullImageDescriptor(image, descriptor)) return 0u; std::optional samplerState; for (const auto& pair : info.sampledPairs) { - if (pair.image != index) continue; + if (pair.image != index || !pair.volumeGather) continue; if (pair.sampler >= info.samplers.size() || info.samplers[pair.sampler].table != NoTable) throw std::runtime_error("a 3D gather through a sampler table is not implemented"); if (pair.sampler >= snapshot.samplers.size() || snapshot.samplers[pair.sampler].dwordCount != 4u) throw std::runtime_error("a 3D gather has no sampler descriptor"); const auto& words = snapshot.samplers[pair.sampler].dwords; @@ -2395,7 +2396,7 @@ std::uint32_t ResourceMaterializer::VolumeGatherState(const ShaderInfo& info, co const auto clampY = (words[0] >> 3u) & 0x7u; const auto clampZ = (words[0] >> 6u) & 0x7u; const auto border = (words[3] >> 30u) & 0x3u; - const bool bordered = std::ranges::any_of(std::array{clampX, clampY, clampZ}, [](std::uint32_t clamp) { return clamp >= 4u; }); + const bool bordered = clampX >= 4u || clampY >= 4u || clampZ >= 6u; if (bordered && border == 3u) throw std::runtime_error("a 3D gather with a border color table is not implemented"); if (bordered && (image.numericClass == IrTextureNumericClass::Uint || image.numericClass == IrTextureNumericClass::Sint)) throw std::runtime_error("a 3D gather of an integer texture with border addressing is not implemented"); if (bordered && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle) throw std::runtime_error("a 3D gather of a swizzled texture with border addressing is not implemented"); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index c6c8ad90c35d..dd316edd5de0 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -2351,17 +2351,18 @@ class Tracker { return static_cast(m_info.samplers.size() - 1); } - void AddSampledPair(std::uint32_t image, std::uint32_t sampler, std::uint32_t pc) { + void AddSampledPair(std::uint32_t image, std::uint32_t sampler, std::uint32_t pc, bool volumeGather) { for (auto& pair : m_info.sampledPairs) { if (pair.image == image && pair.sampler == sampler) { pair.firstUsePc = std::min(pair.firstUsePc, pc); + pair.volumeGather = pair.volumeGather || volumeGather; return; } } if (m_info.sampledPairs.size() >= ShaderInfo::MaxSampledPairs) { fail("sampled image/sampler pair limit exceeded"); } - m_info.sampledPairs.push_back({image, sampler, pc}); + m_info.sampledPairs.push_back({image, sampler, pc, volumeGather}); } void AddHandlePatch(IrValue* handle, std::uint32_t resource) { @@ -2490,7 +2491,7 @@ class Tracker { fail("sampler resource limit exceeded"); } AddHandlePatch(samplerHandle, sampler); - AddSampledPair(resource, sampler, flags.pc); + AddSampledPair(resource, sampler, flags.pc, op == IrOpcode::ImageGatherRaw && m_info.images[resource].dimension == RdnaImageDimension::Dim3D); } AddMemoryPatch(flags.index, resource, sampler, imageInfo.needsSampler); } diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 1b2b7f39a345..5232de74af7b 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -99,7 +99,7 @@ static_assert(sizeof(ShaderInfo) == 232, "ShaderInfo changed: update the info en static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder"); static_assert(sizeof(ImageResource) == 108, "ImageResource changed: update the info encoder"); static_assert(sizeof(SamplerResource) == 24, "SamplerResource changed: update the info encoder"); -static_assert(sizeof(SampledResourcePair) == 12, "SampledResourcePair changed: update the info encoder"); +static_assert(sizeof(SampledResourcePair) == 16, "SampledResourcePair changed: update the info encoder"); static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info encoder"); static_assert(sizeof(StageOutput) == 48, "StageOutput changed: update the info encoder"); static_assert(sizeof(IrBindingLayout) == 80, "IrBindingLayout changed: update the layout encoder"); @@ -145,7 +145,7 @@ static_assert(fieldCount() == 24, "ShaderInfo changed: update the in static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); static_assert(fieldCount() == 49, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); -static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); +static_assert(fieldCount() == 4, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); static_assert(fieldCount() == 4, "StageOutput changed: update the info encoder"); static_assert(fieldCount() == 25, "IrResourcePlan changed: update EncodePlan and DecodePlan"); @@ -843,6 +843,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(pair.image); out.Value(pair.sampler); out.Value(pair.firstUsePc); + out.Value(pair.volumeGather); }); writer.List(info.inputs, [](Writer& out, const StageInput& input) { out.Value(input.kind); @@ -956,10 +957,11 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(sampler.foldTexelOffsetsOnly); in.Value(sampler.table); }); - reader.List(info.sampledPairs, 12, [](Reader& in, SampledResourcePair& pair) { + reader.List(info.sampledPairs, 13, [](Reader& in, SampledResourcePair& pair) { in.Value(pair.image); in.Value(pair.sampler); in.Value(pair.firstUsePc); + in.Value(pair.volumeGather); }); reader.List(info.inputs, 21, [](Reader& in, StageInput& input) { in.Value(input.kind); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 0cfd2a27645a..23911fdc34de 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -868,8 +868,10 @@ std::uint32_t EmitVolumeGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAcce std::uint32_t extents[3] = {}; std::uint32_t starts[3] = {}; for (std::uint32_t axis = 0; axis < 3u; axis++) { + const auto queried = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, queried, size, axis); const auto extent = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, u32, extent, size, axis); + state.module.AddFunction(spv::OpExtInst, u32, extent, GlslStd450(state), GLSLstd450UMax, queried, ConstantU32(state, 1u)); extents[axis] = Unary(state, spv::OpBitcast, i32, extent); const auto raw = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, f32, raw, setup.coord, axis); From 8d33865e1f50b605b2852f98144f742babb0be98 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:00:21 +0200 Subject: [PATCH 06/14] test(agc): check that a null image's 3D gather divides by no image size query --- .../tests/execution/ImageGatherVolume.cpp | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp index e34afaa12fa2..0aa9e5182440 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -351,8 +351,42 @@ void Run(AgcDriver::VulkanDevice& device, const Variant& variant) { } } +void RequireNoDivisionByImageSize(std::span words, const std::string& name) { + constexpr std::uint32_t OpCompositeExtract = 81, OpImageQuerySizeLod = 103, OpBitcast = 124, OpIAdd = 128, OpIMul = 132, OpUDiv = 134, OpSDiv = 135, OpUMod = 137, OpSRem = 138, OpSMod = 139, OpShiftLeftLogical = 196; + std::vector> definitions; + std::vector> divisions; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16u; + const auto opcode = words[offset] & 0xffffu; + Require(count != 0u && offset + count <= words.size(), name + ": the SPIR-V module is malformed"); + const auto instruction = words.subspan(offset, count); + if (opcode == OpCompositeExtract || opcode == OpImageQuerySizeLod || opcode == OpBitcast || opcode == OpIAdd || opcode == OpIMul || opcode == OpShiftLeftLogical) { + if (definitions.size() <= instruction[2]) definitions.resize(instruction[2] + 1u); + definitions[instruction[2]] = {instruction.begin(), instruction.end()}; + } + if (opcode == OpUDiv || opcode == OpSDiv || opcode == OpUMod || opcode == OpSRem || opcode == OpSMod) divisions.push_back(instruction); + offset += count; + } + const auto zeroable = [&](auto&& self, std::uint32_t id) -> bool { + if (id >= definitions.size() || definitions[id].empty()) return false; + const auto& definition = definitions[id]; + const auto opcode = definition[0] & 0xffffu; + if (opcode == OpImageQuerySizeLod) return true; + if (opcode == OpCompositeExtract || opcode == OpBitcast || opcode == OpShiftLeftLogical) return self(self, definition[3]); + return self(self, definition[3]) || self(self, definition[4]); + }; + for (const auto division : divisions) { + Require(!zeroable(zeroable, division[4]), name + ": a division takes the image size query's extent, which is 0 for a null image, as its divisor"); + } +} + void RunNull(AgcDriver::VulkanDevice& device, Variant variant) { variant.null = true; + if (!variant.vertex) { + FillInput(variant.volume, variant.inputs); + const auto result = Compile(device, variant); + RequireNoDivisionByImageSize(result.spirv.Words(), variant.name); + } Dispatch(device, variant); for (std::uint32_t index = 0; index < Output.size(); ++index) { Require(Output[index] == 0.0f, variant.name + ": value " + std::to_string(index) + " is " + std::to_string(Output[index]) + ", expected 0"); From 696e9fa20287494503251ff565f9ccebf5b2e123 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:12:32 +0200 Subject: [PATCH 07/14] docs(shader): record the measured 3D gather addressing in the shader debt --- docs/dev/debt/shader.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index fc7d1ef656e2..c936a53a2e0c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -81,6 +81,7 @@ - [SRT signed comparison](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - [MIMG `tfe` and `lwe`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws. - [Packed horizontal gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. +- [3D gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4_lz` through a 3D T#) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA2 (Radeon 610M, gfx1036), not the PS5's gfx1013: each coordinate is reduced by its axis's clamp mode (wrap to [0,1), mirror to [0,2), otherwise clamped to [-4,4]; NaN reads as 0 and infinities as -4 and 4), multiplied by the extent and 256 and rounded half up exactly, and the four texels around it are read from one slice, with x and y offset by half a texel and z not, in the order (x0,y1), (x1,y1), (x1,y0), (x0,y0). Half-border modes keep the footprint's two texels together, pinning the lower one to [-1, extent-1]; on z they act as clamp to edge and mirror once to edge. The sampler's LOD clamp, bias, filters and anisotropy are ignored and the view's base level is read. A border color table, a swizzled or integer texture with border addressing, an image or sampler table, unnormalized coordinates and samplers that disagree throw. - [MIMG BY2/BY4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. - [2D stores to 1D storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. An `image_store` with DIM 2D_ARRAY through a 1D T# writes texel x, ignoring y, only when the slice is 0, and is dropped for any other slice: measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) through the AMD Windows driver's comgr and HIP runtime (wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even) on a 64-texel linear 32_UINT T# with DEPTH 0 and 3, for x of 0, 5 and 63, y of 0, 1, 3 and 0xffffffff and slices 0, 1, 2, 5 and 0xffffffff; the PS5's gfx1013 is not measured. Storage loads, atomics and `image_store_mip` through such a pair still throw - [MIMG PCK2/PCK4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_pck2/pck4`, opcodes 0x70, 0x71, 0x73 and 0x74) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. With data mask 0x1, they write one dword: the raw bits of the N texels (PCK2: 2, PCK4: 4) from `x & ~(N - 1)` on row y, concatenated (element k at bit `k * texel bits`) and zero-extended. The dword is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when N texels fit in 32 bits: R8, R16 or RG8 for PCK2, R8 for PCK4. Wider texels (R32, RG16, RGBA8) and masks other than 0x1 write zeros on hardware. Those masks throw at decode, and formats outside UNORM/UINT/SINT R8, R16 and RG8 throw. #1314's earlier measurement saw only zeros because it used masks 0x3 and 0xf. The stores (0x76, 0x77, 0x79 and 0x7a) decode but throw at translation. They are not measured, and opcode 0x76 on 32_32_32_32_FLOAT hung the GPU (#1314). From 27d9dc2e4e7b3c895b7d130e90ca4d360e6c34ae Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:28:35 +0200 Subject: [PATCH 08/14] test(agc): gather the wide 3D volume case from 241 texels so it fits every device's 3D image limit --- .../tests/execution/ImageGatherVolume.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp index 0aa9e5182440..b42c888ddc90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -31,11 +31,11 @@ constexpr std::array Positions{-9.3f, -4.8f, -1.3f, -0.7f, -0.2f, 0.3 -1.3f, -0.7f, -0.2f, -0.5015f, 0.8f, 1.6f, 0.3f, 1.9f, -0.3f, -1.6f}; constexpr std::array Extremes{3.0e6f, -3.0e6f, 1.0e9f, -std::numeric_limits::infinity(), std::numeric_limits::infinity(), std::numeric_limits::quiet_NaN(), 65536.3f, -1000.3f}; constexpr std::array WideColumns{ - 0x3f4412b9u, 0x3f78caa4u, 0x3dbebd8cu, 0x3f1fa5e0u, 0x3f6f111au, 0x3f656d67u, 0x3f03ec82u, 0x3f6869bcu, 0x3f6b91beu, 0x3f305e22u, 0x3f75a2a2u, - 0x3f0dbbe2u, 0x3eb2b6a3u, 0x3dc6ee00u, 0x3f4d5f12u, 0x3f69f2d2u, 0x3dc59097u, 0x3f050867u, 0x3e74d30bu, 0x3f436405u, 0x3f5924b9u, 0x3e4a2c04u, - 0x3eac3af2u, 0x3eb0277fu, 0x3ebd4a8eu, 0x3f67d0deu, 0x3ec6acbeu, 0x3f588bdbu, 0x3f4c84b1u, 0x3f62f419u, 0x3f452e9eu, 0x3f7e97a1u, + 0x3bcbaf45u, 0x3d326c59u, 0x3dbf2fcdu, 0x3de9ad25u, 0x3e678f67u, 0x3e7cce13u, 0x3ef99f56u, 0x3f021f56u, 0x3f065f12u, 0x3f0baebdu, 0x3f10fe68u, + 0x3f164e13u, 0x3f1b9dbeu, 0x3f20ed69u, 0x3f263d14u, 0x3f2b8cbfu, 0x3f30dc6au, 0x3f362c15u, 0x3f3b7bc0u, 0x3f40cb6bu, 0x3f461b16u, 0x3f4b6ac1u, + 0x3f50ba6cu, 0x3f560a17u, 0x3f5a49d3u, 0x3f5f997eu, 0x3f64e929u, 0x3f6a38d4u, 0x3f6f887fu, 0x3f74d82au, 0x3f7a27d5u, 0x3f7f7780u, }; -constexpr std::uint32_t WideWidth = 3001; +constexpr std::uint32_t WideWidth = 241; constexpr std::array Ties{127.5f / 2048.0f, -1.0f / 4096.0f, 0x1.0p-20f, -0.0f, -0x1.0p-149f, 255.5f / 1024.0f, 0x1.0p-149f, 383.5f / 1024.0f}; constexpr std::array PositionExtents{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, -1, -1}; @@ -437,7 +437,7 @@ int main() { const SamplerState lodState{Same(2), 0, 4u << 9u, 0x100u | (0xfffu << 12u), 0x100u | (1u << 20u) | (1u << 22u) | (2u << 26u)}; Run(*device, {GatherRed, 0, {8, 4, 4, 0, 1}, lodState, "3D gather of red ignores the sampler's LOD clamp, bias, filters and anisotropy"}); Run(*device, {GatherRed, 0, {8, 4, 4, 1, 1}, {Same(0)}, "3D gather of red reads the base level"}); - Run(*device, {GatherRed, 0, {WideWidth, 2, 2}, {Same(2)}, "3D gather of red from a 3001 texel wide volume at 1/256 texel boundaries"}); + Run(*device, {GatherRed, 0, {WideWidth, 2, 2}, {Same(2)}, "3D gather of red from a 241 texel wide volume at 1/256 texel boundaries"}); Run(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {Same(1)}, "3D gather of red through a blue-green-red-alpha swizzle"}); RunThrows(*device, {GatherRed, 0, cube, {Same(6), 3}, "3D gather with a border color table"}, "a 3D gather with a border color table is not implemented"); RunThrows(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {Same(6), 2}, "3D gather of a swizzled volume with border addressing"}, "a 3D gather of a swizzled texture with border addressing is not implemented"); From d7018a97af5c71901d36b0fd7a103a1e44f3e8a1 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:49:05 +0200 Subject: [PATCH 09/14] docs(shader): shorten the 3D gather debt entry to the 1,000-character guideline --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c936a53a2e0c..ac91c5ccc94a 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -81,7 +81,7 @@ - [SRT signed comparison](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - [MIMG `tfe` and `lwe`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, the LOD warning (texture-streaming feedback, like the T# `MIN_LOD_WARN` itself) is never reported, and `lwe` on `image_get_lod` throws. - [Packed horizontal gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4h_pck`, MIMG opcode 98): the four window texels are read raw and concatenated into a little-endian bitstream, with DMASK bit i writing stream word i to the next destination register; measured on RDNA2 (gfx1031) at every edge: the anchor is floor(x-0.5) with elements at -1, 0, +1 and +2 clamped horizontally into the width, the row is floor(y), negative rows clamp to row 0, rows at or past the height return zeros, sampler addressing modes do not affect the window, and formats wider than 32 bits per texel write zeros in every register. The emitter fetches each window texel at LOD 0 and packs its bits like `image_load_pck`, so the sampler's filter, addressing and LOD state are not used; formats whose bits are not recoverable from the view and D16 data throw. The anchor and row multiply the normalized coordinate by the image size without contraction. The stream rule was measured for R8_UNORM, RG8_UNORM and RGBA8; only RGBA8 has an execution test. `image_gather8h_pck` (opcode 99) never returns texel data on this hardware - across dmasks 0x1 to 0xFF including the upper nibble, RGBA8, RGBA16 and RGBA32 formats, 1D, 2D and 3D descriptors and normalized and unnormalized sampling it only zero-fills popcount(dmask) registers (sentinel-prefilled destinations) - so it decodes but throws at translation; its eight-texel semantics remain unmeasured. -- [3D gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4_lz` through a 3D T#) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA2 (Radeon 610M, gfx1036), not the PS5's gfx1013: each coordinate is reduced by its axis's clamp mode (wrap to [0,1), mirror to [0,2), otherwise clamped to [-4,4]; NaN reads as 0 and infinities as -4 and 4), multiplied by the extent and 256 and rounded half up exactly, and the four texels around it are read from one slice, with x and y offset by half a texel and z not, in the order (x0,y1), (x1,y1), (x1,y0), (x0,y0). Half-border modes keep the footprint's two texels together, pinning the lower one to [-1, extent-1]; on z they act as clamp to edge and mirror once to edge. The sampler's LOD clamp, bias, filters and anisotropy are ignored and the view's base level is read. A border color table, a swizzled or integer texture with border addressing, an image or sampler table, unnormalized coordinates and samplers that disagree throw. +- [3D gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_gather4_lz` through a 3D T#) follow a [hardware oracle](../HW_ORACLE.md) measurement on RDNA2 (Radeon 610M, gfx1036), not the PS5's gfx1013: each coordinate is reduced by its axis's clamp mode (wrap to [0,1), mirror to [0,2), else clamped to [-4,4]; NaN reads as 0, infinities as -4 and 4), multiplied by the extent and 256 and rounded half up exactly; the four texels around it come from one slice, x and y offset by half a texel and z not, in the order (x0,y1), (x1,y1), (x1,y0), (x0,y0). Half-border modes keep the footprint's two texels together, pinning the lower one to [-1, extent-1]; on z they act as clamp to edge and mirror once to edge. LOD clamp, bias, filters and anisotropy are ignored; the view's base level is read. Border color tables, swizzled or integer textures with border addressing, image or sampler tables, unnormalized coordinates and disagreeing samplers throw. - [MIMG BY2/BY4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_by2/by4`, opcodes 0x42, 0x43, 0x4a and 0x4b) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. They read N texels (BY2: 2, BY4: 4) from `x & ~(N - 1)` on row y, texel by texel and component by component within each texel, with each texel converted like `image_load`. Every register is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when the N texels fit in one dword: R8 and R16 (DMASK 0x3) or RG8 (0xf) for BY2, and R8 (0xf) for BY4. With 32-bit texels (R32, RG32, RG16, RGBA8) the hardware returns zeros. Other formats, masks, dimensions, swizzles, D16/A16 and indirect descriptors throw. The stores (0x52, 0x53, 0x5a and 0x5b) decode but throw at translation. Measured on hardware, they write from the unaligned x up to the end of its dword, which is not implemented. - [2D stores to 1D storage images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp): an `image_store` with DIM 2D through a 1D T# writes texel x and ignores y, as measured on RDNA2 (gfx1035) for y from 0 to 4 and 0xffffffff, single- and four-component formats and a 1x1 8_8_8_8_UNORM T#: the T# type decides the addressing, DIM only how many address VGPRs are read. An `image_store` with DIM 2D_ARRAY through a 1D T# writes texel x, ignoring y, only when the slice is 0, and is dropped for any other slice: measured on RDNA2 (gfx1036) with the [hardware oracle](../HW_ORACLE.md) through the AMD Windows driver's comgr and HIP runtime (wave32, IEEE off, DX10 clamp on, denormals preserved, round to nearest even) on a 64-texel linear 32_UINT T# with DEPTH 0 and 3, for x of 0, 5 and 63, y of 0, 1, 3 and 0xffffffff and slices 0, 1, 2, 5 and 0xffffffff; the PS5's gfx1013 is not measured. Storage loads, atomics and `image_store_mip` through such a pair still throw - [MIMG PCK2/PCK4 loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_load[_mip]_pck2/pck4`, opcodes 0x70, 0x71, 0x73 and 0x74) were measured on RDNA2 (gfx1035) with the [hardware oracle](../HW_ORACLE.md) for 2D images with identity swizzle. With data mask 0x1, they write one dword: the raw bits of the N texels (PCK2: 2, PCK4: 4) from `x & ~(N - 1)` on row y, concatenated (element k at bit `k * texel bits`) and zero-extended. The dword is zero when the mip level is past the last level, `x + N - 1` is outside the level's width (unaligned x, unsigned), or y is outside its height. Data only comes back when N texels fit in 32 bits: R8, R16 or RG8 for PCK2, R8 for PCK4. Wider texels (R32, RG16, RGBA8) and masks other than 0x1 write zeros on hardware. Those masks throw at decode, and formats outside UNORM/UINT/SINT R8, R16 and RG8 throw. #1314's earlier measurement saw only zeros because it used masks 0x3 and 0xf. The stores (0x76, 0x77, 0x79 and 0x7a) decode but throw at translation. They are not measured, and opcode 0x76 on 32_32_32_32_FLOAT hung the GPU (#1314). From 80f07b510fac930c50d5f0269c1751a8aa948de7 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Tue, 27 Oct 2026 05:38:55 +0100 Subject: [PATCH 10/14] fix(recompiler): count the volume gather field next to main's optional descriptor field in the disk cache info encoder --- 3rdparty/freetype | 2 +- core/shader/recompiler/ShaderDiskCache.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/3rdparty/freetype b/3rdparty/freetype index 37515cf56ba3..42608f77f207 160000 --- a/3rdparty/freetype +++ b/3rdparty/freetype @@ -1 +1 @@ -Subproject commit 37515cf56ba3349656049edbed89ffc6f6466f2d +Subproject commit 42608f77f20749dd6ddc9e0536788eaad70ea4b5 diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 5232de74af7b..8f7367a47f35 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -143,7 +143,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 24, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 49, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 50, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 4, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); From 5af9948a3e5b836bcf965c6308c59537a2b281ac Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:43:04 +0100 Subject: [PATCH 11/14] test(shader): drop the 3D gather checks that pin unimplemented border paths as throws --- .../tests/execution/ImageGatherVolume.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp index b42c888ddc90..fbe1e3014e7e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -393,16 +393,6 @@ void RunNull(AgcDriver::VulkanDevice& device, Variant variant) { } } -void RunThrows(AgcDriver::VulkanDevice& device, const Variant& variant, const std::string& message) { - try { - Dispatch(device, variant); - } catch (const std::exception& error) { - Require(std::string(error.what()).find(message) != std::string::npos, variant.name + ": threw '" + error.what() + "', expected '" + message + "'"); - return; - } - throw std::runtime_error(variant.name + ": did not throw '" + message + "'"); -} - } int main() { @@ -439,8 +429,6 @@ int main() { Run(*device, {GatherRed, 0, {8, 4, 4, 1, 1}, {Same(0)}, "3D gather of red reads the base level"}); Run(*device, {GatherRed, 0, {WideWidth, 2, 2}, {Same(2)}, "3D gather of red from a 241 texel wide volume at 1/256 texel boundaries"}); Run(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {Same(1)}, "3D gather of red through a blue-green-red-alpha swizzle"}); - RunThrows(*device, {GatherRed, 0, cube, {Same(6), 3}, "3D gather with a border color table"}, "a 3D gather with a border color table is not implemented"); - RunThrows(*device, {GatherRed, 0, {8, 4, 4, 0, 0, BlueGreenRedAlpha}, {Same(6), 2}, "3D gather of a swizzled volume with border addressing"}, "a 3D gather of a swizzled texture with border addressing is not implemented"); RunNull(*device, {GatherRed, 0, cube, {Same(0)}, "3D gather of red from a null image descriptor, wrap"}); RunNull(*device, {VertexGatherRed, 0, cube, {Same(1)}, "vertex shader 3D gather of red from a null image descriptor, mirror", Inputs::Positions, true}); std::puts("image 3D gather tests passed"); From a5c407ab0b1eca739267dcc10d4c9ca950100676 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:39:08 +0100 Subject: [PATCH 12/14] fix(shader): keep main's FreeType submodule pointer --- 3rdparty/freetype | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/3rdparty/freetype b/3rdparty/freetype index 42608f77f207..37515cf56ba3 160000 --- a/3rdparty/freetype +++ b/3rdparty/freetype @@ -1 +1 @@ -Subproject commit 42608f77f20749dd6ddc9e0536788eaad70ea4b5 +Subproject commit 37515cf56ba3349656049edbed89ffc6f6466f2d From e92ec413c98dbdd28cccb2b86601e7cef6657324 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:40:59 +0100 Subject: [PATCH 13/14] test(agc): check that a 3D gather reduces its texel index without a signed remainder --- .../tests/execution/ImageGatherVolume.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp index fbe1e3014e7e..60bf65a1415b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherVolume.cpp @@ -327,6 +327,17 @@ void Draw(AgcDriver::VulkanDevice& device, const Variant& variant) { device.WaitIdle(); } +void RequireNoSignedRemainder(std::span words, const std::string& name) { + constexpr std::uint32_t OpSRem = 138, OpSMod = 139; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16u; + const auto opcode = words[offset] & 0xffffu; + Require(count != 0u && offset + count <= words.size(), name + ": the SPIR-V module is malformed"); + Require(opcode != OpSRem && opcode != OpSMod, name + ": a signed remainder, whose result Vulkan leaves undefined for a negative operand, reduces the gather's texel index"); + offset += count; + } +} + void Dispatch(AgcDriver::VulkanDevice& device, const Variant& variant) { FillTexels(variant.volume); FillInput(variant.volume, variant.inputs); @@ -336,6 +347,7 @@ void Dispatch(AgcDriver::VulkanDevice& device, const Variant& variant) { return; } const auto result = Compile(device, variant); + RequireNoSignedRemainder(result.spirv.Words(), variant.name); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(variant.code.data())); device.WaitIdle(); } From 3cb6fb3ebb3fa6d30b8a373c3a006504f6247445 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:41:25 +0100 Subject: [PATCH 14/14] fix(recompiler): reduce a 3D gather's wrapped texel index with an unsigned remainder --- core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 23911fdc34de..59d53e4c4339 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -923,8 +923,8 @@ std::uint32_t EmitVolumeGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAcce const auto extent = extents[axis]; const auto last = Binary(state, spv::OpISub, i32, extent, ConstantI32(state, 1)); const auto modulo = [&](std::uint32_t value, std::uint32_t period) { - const auto remainder = Binary(state, spv::OpSRem, i32, value, period); - return Binary(state, spv::OpIAdd, i32, remainder, Select(state, i32, Binary(state, spv::OpSLessThan, boolean, remainder, ConstantI32(state, 0)), period, ConstantI32(state, 0))); + const auto lifted = Unary(state, spv::OpBitcast, u32, Binary(state, spv::OpIAdd, i32, value, period)); + return Unary(state, spv::OpBitcast, i32, Binary(state, spv::OpUMod, u32, lifted, Unary(state, spv::OpBitcast, u32, period))); }; const auto extInst = [&](std::uint32_t op, std::uint32_t a, std::uint32_t b, std::uint32_t c) { const auto id = state.module.AllocateId();