From f6ce77bb7f4e7145f8ecfd7e2d5f44084a2aec35 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 15:49:37 +0000 Subject: [PATCH] feat(shader): accept texel offsets in 1D gathers image_gather4_lz_o on a 1D view threw. The 1D gather emulation picks the two texels around the coordinate itself, so the 6-bit x offset of the address is added to the left texel index before both texel centres are sampled, as the hardware offsets the footprint. Co-Authored-By: Claude Opus 5.5 --- .../execution/ImageGatherOneDimensional.cpp | 170 ++++++++++++++++++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 13 +- 2 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp new file mode 100644 index 000000000..ac769d0b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp @@ -0,0 +1,170 @@ +#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 Format8888UNorm = 56; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t ClampWrap = 0; +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 Gather1D{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0100, 0x00820603, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; +alignas(256) constexpr std::array Gather1DOffset{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf15c0100, 0x00820602, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; + +struct Variant { + std::span code; + bool offset; + 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, 0x01016facu}; +} + +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, + 0xfacu | (Type1D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array SamplerDescriptor(std::uint32_t clamp) { + return {clamp | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +} + +std::uint8_t Red(std::uint32_t x) { + return static_cast(x * 31u + 5u); +} + +void FillTexels() { + Texels.fill(0); + const auto descriptor = TextureDescriptor(); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + const auto& mip = surface.mips.at(0); + for (std::uint32_t x = 0; x < Width; ++x) { + auto* texel = Texels.data() + mip.tiledOffset + x * 4u; + texel[0] = Red(x); + texel[1] = 0x11; + texel[2] = 0x22; + texel[3] = 0xff; + } +} + +void FillInput() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto offset = static_cast(tid % 11u) - 5; + Input[tid * 4u + 0u] = (static_cast(offset) & 0x3fu) | (tid % 3u == 0u ? 0x3f00u : 0u); + Input[tid * 4u + 1u] = std::bit_cast(-0.3f + 0.05f * static_cast(tid)); + Input[tid * 4u + 2u] = 0u; + Input[tid * 4u + 3u] = 0u; + } +} + +std::uint32_t Texel(int x, std::uint32_t clamp) { + const int size = static_cast(Width); + const int address = clamp == ClampEdge ? std::clamp(x, 0, size - 1) : ((x % size) + size) % size; + return Red(static_cast(address)); +} + +std::array Expected(std::uint32_t tid, const Variant& variant) { + const float u = std::bit_cast(Input[tid * 4u + 1u]); + int left = static_cast(std::floor(u * static_cast(Width) - 0.5f)); + if (variant.offset) { + const auto field = Input[tid * 4u] & 0x3fu; + left += static_cast(field ^ 0x20u) - 0x20; + } + const auto first = Texel(left, variant.clamp); + const auto second = Texel(left + 1, variant.clamp); + return {first, second, second, first}; +} + +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, {Gather1D, false, ClampEdge, "1D gather, clamp to edge"}); + Run(*device, {Gather1D, false, ClampWrap, "1D gather, wrap"}); + Run(*device, {Gather1DOffset, true, ClampEdge, "1D gather with offsets, clamp to edge"}); + Run(*device, {Gather1DOffset, true, ClampWrap, "1D gather with offsets, wrap"}); + std::puts("image 1D 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 419bf78ed..a9c8da1e2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -538,7 +538,7 @@ std::uint32_t UnpackImageGather(SpirvValueEmitContext& ctx, const ImageEmitAcces return result; } -std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t coord) { +std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { auto& state = ctx.state; const auto numericClass = access.image.numericClass; state.module.EmitCapability(spv::CapabilityImageQuery); @@ -547,8 +547,11 @@ std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const Image state.module.AddFunction(spv::OpImageQuerySizeLod, TypeU32(state), width, image, ConstantU32(state, 0)); const auto widthF32 = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToF, TypeF32(state), widthF32, width); - const auto left = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), left, GlslStd450(state), GLSLstd450Floor, Binary(state, spv::OpFSub, TypeF32(state), Binary(state, spv::OpFMul, TypeF32(state), coord, widthF32), ConstantF32(state, 0x3f000000u))); + auto left = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), left, GlslStd450(state), GLSLstd450Floor, Binary(state, spv::OpFSub, TypeF32(state), Binary(state, spv::OpFMul, TypeF32(state), setup.coord, widthF32), ConstantF32(state, 0x3f000000u))); + if (setup.layout.offset != NoImageComponent) { + left = Binary(state, spv::OpFAdd, TypeF32(state), left, Unary(state, spv::OpConvertSToF, TypeF32(state), PackedOffset(ctx, access, setup.layout))); + } const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler, access.slot); const auto vectorType = ImageVectorType(state, numericClass, 4); const auto scalarType = ImageScalarType(state, numericClass); @@ -810,10 +813,10 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con ctx.Fail(access.inst, "is a comparison gather of a color texture, which is not implemented"); } if (dimension == RdnaImageDimension::Dim1D) { - if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagOffset) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { + if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); } - const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup.coord); + const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup); ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, UnpackImageGather(ctx, access, sample), setup.numericClass, false, true))); return; }