From 7a4e1a22458dcc94feb087d34d36fc33e24154d8 Mon Sep 17 00:00:00 2001 From: DotDebian Date: Mon, 5 Oct 2026 01:30:17 +0200 Subject: [PATCH] fix(shader): give the a16 bias of image samples its own address dword With a16 the bias is a half that occupies a full address dword and the coordinates start in the next one. The layout packed the first coordinate into the upper half of the bias dword, so image_sample_b, image_sample_b_cl, image_sample_b_o and the biased gathers read their coordinates from the wrong halves. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/ImageSampleBiasA16.cpp | 189 ++++++++++++++++++ .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 6 +- 2 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleBiasA16.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleBiasA16.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleBiasA16.cpp new file mode 100644 index 000000000..ec41e7e3f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleBiasA16.cpp @@ -0,0 +1,189 @@ +#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 Words = 32; +constexpr std::uint32_t ResultWord = 8; +constexpr std::uint32_t Size = 16; +constexpr std::uint32_t Levels = 5; +constexpr std::uint32_t Clamps = 4; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t MinLodCapability = 42; +alignas(256) std::array Buffer{}; +alignas(256) std::array Texels{}; + +alignas(256) constexpr std::array BiasCode{ + 0x34020087, 0xe0301000, 0x80000501, 0xe0301004, 0x80000601, 0xbf8c3f70, 0xf0940f08, 0x40610c05, + 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, + 0x7e0802ff, 0x00000201, 0xf0d40f08, 0x40610c04, 0xe0701030, 0x80000c01, 0xe0701034, 0x80000d01, + 0xe0701038, 0x80000e01, 0xe070103c, 0x80000f01, 0x7e0802ff, 0x00003e3f, 0xf0d40f08, 0x40610c04, + 0xe0701040, 0x80000c01, 0xe0701044, 0x80000d01, 0xe0701048, 0x80000e01, 0xe070104c, 0x80000f01, + 0xbf810000, +}; + +alignas(256) constexpr std::array BiasClampCode{ + 0x34020087, 0xe0301000, 0x80000501, 0xe0301004, 0x80000601, 0xe0301008, 0x80000701, 0xbf8c3f70, + 0xf0980f08, 0x40610c05, 0xe0701050, 0x80000c01, 0xe0701054, 0x80000d01, 0xe0701058, 0x80000e01, + 0xe070105c, 0x80000f01, 0xbf810000, +}; + +struct Sample { + std::uint32_t x; + std::uint32_t y; + std::uint32_t clamp; +}; + +Sample SampleOf(std::uint32_t tid) { + return {(tid * 7u + 3u) % Size, (tid * 5u + 1u) % Size, (tid / 3u) % Clamps}; +} + +std::uint32_t Half(float value) { + if (value == 0.0f) { + return 0u; + } + const auto bits = std::bit_cast(value); + return (((bits >> 23u) & 0xffu) - 112u) << 10u | ((bits >> 13u) & 0x3ffu); +} + +std::uint32_t Coordinate(std::uint32_t texel) { + return Half((static_cast(texel) + 0.5f) / static_cast(Size)); +} + +void FillInput() { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + auto* words = &Buffer[tid * Words]; + words[0] = Half(0.0f) | (Coordinate((sample.x + 5u) % Size) << 16u); + words[1] = Coordinate(sample.x) | (Coordinate(sample.y) << 16u); + words[2] = Half(static_cast(sample.clamp)) | (Half(4.0f) << 16u); + } +} + +void FillTexture() { + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888UNorm, Size, Size, Levels); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sample a16 bias: the mip chain does not fit the texel storage"); + Texels.fill(0xeeu); + for (std::uint32_t level = 0; level < Levels; ++level) { + const auto& mip = mips[level]; + for (std::uint32_t y = 0; y < mip.height; ++y) { + for (std::uint32_t x = 0; x < mip.width; ++x) { + auto* texel = &Texels[mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = static_cast(level); + texel[1] = static_cast(x); + texel[2] = static_cast(y); + texel[3] = 255u; + } + } + } +} + +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 void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | ((Levels - 1u) << 16u) | (Type2D << 28u), + 0u, + (Levels - 1u) << 4u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto texture = TextureDescriptor(Texels.data()); + const auto sampler = SamplerDescriptor(); + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +std::uint32_t Wrap(std::uint32_t coordinate, std::int32_t offset, std::uint32_t size) { + const auto extent = static_cast(size); + return static_cast(((static_cast(coordinate) + offset) % extent + extent) % extent); +} + +void Check(bool clamped) { + constexpr std::array names{"image_sample_b a16", "image_sample_b_o a16 (1, 2)", "image_sample_b_o a16 (-1, -2)", "image_sample_b_cl a16"}; + constexpr std::array, 4> offsets{{{0, 0}, {1, 2}, {-1, -2}, {0, 0}}}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto sample = SampleOf(tid); + const std::array levels{0u, 0u, 0u, sample.clamp}; + for (std::uint32_t index = 0; index < (clamped ? 4u : 3u); ++index) { + const std::uint32_t level = levels[index]; + const std::uint32_t size = Size >> level; + const std::array expected{level, Wrap(sample.x >> level, offsets[index][0], size), Wrap(sample.y >> level, offsets[index][1], size), 255u}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + ResultWord + index * 4u + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), std::string(names[index]) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto capabilities = device->Target().supportedCapabilities; + const bool clamped = std::find(capabilities.begin(), capabilities.end(), MinLodCapability) != capabilities.end(); + FillInput(); + FillTexture(); + Run(*device, BiasCode); + if (clamped) { + Run(*device, BiasClampCode); + } + Check(clamped); + std::puts(clamped ? "image sample a16 bias tests passed" : "image sample a16 bias tests passed, image_sample_b_cl skipped without shaderResourceMinLod"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index e749c8d24..58983dc6f 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -159,6 +159,10 @@ std::uint32_t componentWidth(std::uint32_t flags, std::uint32_t component) { return 16; } +bool isBiasComponent(std::uint32_t flags, std::uint32_t component) { + return (flags & RdnaImageSampleFlagBias) != 0u && component == ((flags & RdnaImageSampleFlagOffset) != 0u ? 1u : 0u); +} + RdnaImageDimension decodeDimension(std::uint32_t dimension) { switch (dimension) { case 0: return RdnaImageDimension::Dim1D; @@ -238,7 +242,7 @@ RdnaImageAddressComponent GetRdnaImageAddressComponentLayout(std::uint32_t flags if (index == component) { return {offset, width}; } - offset += width; + offset += isBiasComponent(flags, index) ? 32u : width; } throw std::runtime_error("invalid image address component"); }