From eb06284c7871c2ec7df438448a5cb8dad77a2f5f Mon Sep 17 00:00:00 2001 From: Nahuel Genchi <65604505+NahuelGenchi@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:26:51 -0300 Subject: [PATCH] fix(shader): read 2D image accesses through 1D T#s as measured A compute shader in Astro Bot samples an image as 2D while the bound T# is 1D, and the recompiler refused it. Offer a 1D runtime mode for sampled images used only by image_sample_lz and mip-less image_load, which reads the texel the first coordinate selects, as measured on RDNA2 in #2237. --- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../execution/ImagePlaneAddressOnLine.cpp | 198 ++++++++++++++++++ .../IrMetadata/Resources.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 5 + .../Optimization/src/ResourceTracker.cpp | 3 + core/shader/recompiler/ShaderDiskCache.cpp | 2 + docs/dev/TechnicalDebt.md | 1 + 7 files changed, 211 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 459bde29c..b57cc3bd7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -198,6 +198,7 @@ CompiledVariant sampleVariant() { image.fmaskCompatible = false; image.depthBitsCompatible = false; image.flatVolumeCompatible = false; + image.flatLineCompatible = false; image.byElements = 4; image.byComponents = 1; image.indirectRoot = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp new file mode 100644 index 000000000..4614998ce --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePlaneAddressOnLine.cpp @@ -0,0 +1,198 @@ +#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 Size = 16; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type1D = 8; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t SampleResult = 8; +constexpr std::uint32_t LoadResult = 12; +alignas(256) std::array Buffer{}; +alignas(256) std::array Line{}; +alignas(256) std::array Plane{}; + +alignas(256) constexpr std::array Code{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf09c0f08, 0x00610c02, + 0xf0001f08, 0x00011005, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, + 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xbf810000, +}; + +alignas(256) constexpr std::array ExplicitLodCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xbf8c3f70, 0xf0900f08, 0x00610c02, 0xbf8c3f70, 0xe0701020, 0x80000c01, 0xbf810000, +}; + +constexpr std::array LineV{0.0f, 0.5f, 1.0f, 1.5f, -0.25f, 3.0f, 0.25f, 0.75f}; +constexpr std::array LineY{0u, 1u, 2u, 3u, 0xffffffffu, 7u, 0u, 5u}; + +std::uint32_t TexelOf(std::uint32_t tid) { + return (tid * 7u + 3u) % Size; +} + +std::uint32_t Bits(float value) { + return std::bit_cast(value); +} + +void FillInput(bool line) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto x = TexelOf(tid); + const auto u = Bits((static_cast(x) + 0.5f) / static_cast(Size)); + const auto v = line ? Bits(LineV[tid % LineV.size()]) : Bits((static_cast((tid * 5u + 1u) % Size) + 0.5f) / static_cast(Size)); + const auto y = line ? LineY[tid % LineY.size()] : (tid * 5u + 1u) % Size; + const std::array words{u, v, 0u, x, y, 0u}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } +} + +std::array LineDescriptor() { + const auto address = static_cast(reinterpret_cast(Line.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + (Size - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array PlaneDescriptor() { + const auto address = static_cast(reinterpret_cast(Plane.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Size - 1u) & 3u) << 30u), + ((Size - 1u) >> 2u) | ((Size - 1u) << 14u), + 0xfacu | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void FillTexture(std::span storage, const std::array& descriptor, std::uint32_t rows) { + const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(geometry.guestBytes <= storage.size(), "image plane address on a line: the texture does not fit the texel storage"); + const auto& mip = geometry.mips.at(0); + std::fill(storage.begin(), storage.end(), 0xeeu); + for (std::uint32_t y = 0; y < rows; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &storage[geometry.GuestLayerOffset(0) + mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + texel[0] = 77u; + 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, 0x31016facu}; +} + +std::array SamplerDescriptor() { + return {0u, 0xfffu << 12u, 1u << 26u, 0u}; +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture) { + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + 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(); +} + +void Expect(std::uint32_t tid, std::uint32_t result, const std::array& expected, const std::string& name) { + for (std::uint32_t component = 0; component < 4u; ++component) { + const float value = std::bit_cast(Buffer[tid * Words + result + component]) * 255.0f; + Require(std::lround(value) == static_cast(expected[component]), name + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component])); + } +} + +void CheckLine() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array texel{77u, TexelOf(tid), 0u, 255u}; + Expect(tid, SampleResult, texel, "image_sample_lz 2d on a 1D texture with v " + std::to_string(LineV[tid % LineV.size()])); + Expect(tid, LoadResult, texel, "image_load 2d on a 1D texture with y " + std::to_string(static_cast(LineY[tid % LineY.size()]))); + } +} + +void CheckPlane() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array texel{77u, TexelOf(tid), (tid * 5u + 1u) % Size, 255u}; + Expect(tid, SampleResult, texel, "image_sample_lz 2d on a 2D texture"); + Expect(tid, LoadResult, texel, "image_load 2d on a 2D texture"); + } +} + +void RequireRefused(AgcDriver::VulkanDevice& device) { + std::string refusal; + try { + Run(device, ExplicitLodCode, LineDescriptor()); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("incompatible with the static runtime image interface") != std::string::npos, "image_sample_l 2d on a 1D texture was not refused: " + refusal); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + FillTexture(Line, LineDescriptor(), 1u); + FillInput(true); + Run(*device, Code, LineDescriptor()); + CheckLine(); + FillTexture(Plane, PlaneDescriptor(), Size); + FillInput(false); + Run(*device, Code, PlaneDescriptor()); + CheckPlane(); + RequireRefused(*device); + std::puts("image plane address on line tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 3692b718b..882457f94 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -86,6 +86,7 @@ struct ImageResource { bool constantSwizzle = false; bool constantSwizzleCompatible = true; bool flatVolumeCompatible = true; + bool flatLineCompatible = true; std::uint32_t byElements = 0; std::uint32_t byComponents = 0; IrBufferFormat packedFormat = IrBufferFormat::Invalid; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 8f4edf617..c13c2a02d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -633,6 +633,11 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe plane.dimension = RdnaImageDimension::Dim2D; modes.push_back(plane); } + if (image.dimension == RdnaImageDimension::Dim2D && image.flatLineCompatible && !storage && !depth && conversion == IrBufferFormat::Invalid && packed == IrBufferFormat::Invalid) { + auto line = mode; + line.dimension = RdnaImageDimension::Dim1D; + modes.push_back(line); + } if (image.dimension == RdnaImageDimension::Dim1DArray || image.dimension == RdnaImageDimension::Dim2DArray || image.dimension == RdnaImageDimension::Dim2DMsaaArray) { auto plain = mode; plain.dimension = image.dimension == RdnaImageDimension::Dim1DArray ? RdnaImageDimension::Dim1D : image.dimension == RdnaImageDimension::Dim2DArray ? RdnaImageDimension::Dim2D : RdnaImageDimension::Dim2DMsaa; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index d195c5550..3f20951dd 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1065,6 +1065,9 @@ private: const bool flatVolumeLoad = op == IrOpcode::ImageRead && !memory.imageHasMip && memory.imageSampleFlags == 0u; const bool flatVolumeSample = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; image.flatVolumeCompatible = image.flatVolumeCompatible && (flatVolumeLoad || flatVolumeSample) && !memory.imagePacked && memory.imageByElements == 0u; + const bool flatLineLoad = op == IrOpcode::ImageRead && !memory.imageHasMip && memory.imageSampleFlags == 0u; + const bool flatLineSample = op == IrOpcode::ImageSampleRaw && memory.imageSampleFlags == RdnaImageSampleFlagLevelZero; + image.flatLineCompatible = image.flatLineCompatible && (flatLineLoad || flatLineSample) && !memory.imagePacked && memory.imageByElements == 0u; if ((memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0u) { constexpr auto unsupported = RdnaImageSampleFlagLod | RdnaImageSampleFlagDerivative; if (op == IrOpcode::ImageGatherRaw || (memory.imageSampleFlags & unsupported) != 0u) image.emulatedCompare |= EmulatedCompare::Unsupported; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index b32c7c4e4..f0fd8e247 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -483,6 +483,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(image.constantSwizzle); out.Value(image.constantSwizzleCompatible); out.Value(image.flatVolumeCompatible); + out.Value(image.flatLineCompatible); out.Value(image.byElements); out.Value(image.byComponents); out.Value(image.packedFormat); @@ -587,6 +588,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.constantSwizzle); in.Value(image.constantSwizzleCompatible); in.Value(image.flatVolumeCompatible); + in.Value(image.flatLineCompatible); in.Value(image.byElements); in.Value(image.byComponents); in.Value(image.packedFormat); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 7cfb30945..18c6e183c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -317,6 +317,7 @@ Throughout the project, every function at every stage either **does exactly what - Repeated `s_load`s of one [flat SRT](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. - A value read through a null or unmapped pointer still throws at record time if a [branch condition, uniform fill or table base](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data, even if the console skips the read. - A [V#, T# or S#](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) loaded through a null or unmapped pointer read from memory is zero when the work is recorded, and the capture throws if no flat SRT slot records that read. +- [2D accesses through a 1D T#](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read the texel the 1D addressing selects and ignore the second coordinate, as simonbbby measured on RDNA2 (gfx1036) in #2237 for `image_sample_lz` and mip-less `image_load` with point filtering and wrap, clamp and border addressing; the PS5's gfx1013 is not measured. Other 2D accesses through a 1D T# (storage, depth, packed, BY loads, mips, derivatives, comparison) still throw. - On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. - [EXEC read as a value](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) (`s_mov_b64 sN, exec`, the `s_*_saveexec` copy, ALU sources) holds only lanes with a host invocation; the console's also holds the wave's other lanes, e.g. 32-63 of a wave64 on 32 host lanes. - [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR.