From d5a3a4321cda8a84b25322015e26fc66cbffab7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Thu, 8 Oct 2026 21:09:48 +0200 Subject: [PATCH] fix(shader): follow measured hardware for MIMG BY2/BY4 loads Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 + .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../tests/execution/ImageBy.cpp | 191 +++++++++++------- core/libs/tests/ImageBy.cpp | 61 +++--- .../IrMetadata/Resources.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 11 +- .../Optimization/src/ResourceTracker.cpp | 3 +- .../RdnaDecoder/src/RdnaImageOpDecoder.cpp | 4 +- core/shader/recompiler/ShaderDiskCache.cpp | 4 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 62 ++++++ .../MemoryInstructions/ImageInstructions.cpp | 48 +---- docs/dev/TechnicalDebt.md | 1 + 12 files changed, 240 insertions(+), 151 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 887f3abb6..b5e0d2eda 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1035,6 +1035,10 @@ if(BUILD_TESTING) target_link_libraries(image_by_tests PRIVATE shader_recompiler_agc_driver libc) configure_windows_unwind(image_by_tests) add_test(NAME image_by COMMAND image_by_tests) + if(WIN32) + set_tests_properties(image_by PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(image_msaa_load_tests ${LIBS_INCLUDE_DIR}/tests/ImageMsaaLoad.cpp) target_link_libraries(image_msaa_load_tests PRIVATE shader_recompiler_agc_driver libc) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 000512c33..38ca0eb78 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -193,6 +193,7 @@ CompiledVariant sampleVariant() { image.fmaskCompatible = false; image.depthBitsCompatible = false; image.byElements = 4; + image.byComponents = 1; image.indirectRoot = 0; image.indirectMappingOffset = 12; image.indirectSearchIterations = 3; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageBy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageBy.cpp index f360c2c4a..fccae5c4f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageBy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageBy.cpp @@ -7,6 +7,7 @@ #include "VulkanTestDevice.hpp" #include #include +#include #include #include #include @@ -17,50 +18,101 @@ namespace { using AgcDriver::Graphics::Require; -constexpr std::uint32_t Threads = 32; -constexpr std::uint32_t Width = 256; -constexpr std::size_t Bytes = 16384; -alignas(256) std::array Texels{}; -alignas(256) std::array Output{}; -constexpr std::array Encodings{0x42u, 0x43u, 0x4au, 0x4bu, 0x52u, 0x53u, 0x5au, 0x5bu}; -constexpr std::array Values{101u, 202u, 303u, 404u}; -std::vector Code(std::uint32_t opcode) { - const bool store = (opcode & 0x10u) != 0u; - std::vector code{0x34060084u, 0x343c0081u + (opcode & 1u), 0x7e3e0280u, 0x7e400281u}; - if (store) { - for (std::uint32_t index = 0; index < 4; ++index) { - code.push_back(0x7e0002ffu | ((10u + index) << 17u)); - code.push_back(Values[index]); - } +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Width = 61; +constexpr std::uint32_t Height = 3; +constexpr std::uint32_t Levels = 2; +constexpr std::size_t Bytes = 16384; +constexpr std::uint32_t Sentinel = 0xcafe0000u; +constexpr std::uint32_t R8UInt = 5; +constexpr std::uint32_t R16UInt = 11; +constexpr std::uint32_t Rg8UInt = 18; +constexpr std::uint32_t R32UInt = 20; +alignas(256) std::array Texels{}; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +struct Format { + std::uint32_t id; + std::uint32_t bytes; + std::uint32_t components; +}; + +std::vector Code(std::uint32_t opcode, std::uint32_t dmask) { + std::vector code{0x34060084u, 0xe0381000u, 0x80031e03u, 0xbf8c3f70u}; + for (std::uint32_t index = 0; index < 4u; ++index) { + code.push_back(0x7e0002ffu | ((10u + index) << 17u)); + code.push_back(Sentinel + index); } - code.push_back(0xf0000f08u | (opcode << 18u)); + code.push_back(0xf0000008u | (opcode << 18u) | (dmask << 8u)); code.push_back(0x00010a1eu); code.push_back(0xbf8c3f70u); - if (!store) { - code.push_back(0xe0781000u); - code.push_back(0x80000a03u); - } + code.push_back(0xe0781000u); + code.push_back(0x80000a03u); code.push_back(0xbf810000u); return code; } -std::array Descriptor(std::uint32_t format, std::uint32_t swizzle = 0xfacu) { +std::array Descriptor(std::uint32_t format, std::uint32_t swizzle) { const auto address = reinterpret_cast(Texels.data()); return {static_cast(address >> 8u), static_cast(address >> 40u) | (format << 20u) | (((Width - 1u) & 3u) << 30u), - (Width - 1u) >> 2u, swizzle | (1u << 16u) | (9u << 28u), 0u, 1u << 4u, 0u, 0u}; + ((Width - 1u) >> 2u) | ((Height - 1u) << 14u), swizzle | ((Levels - 1u) << 16u) | (9u << 28u), 0u, 1u << 4u, 0u, 0u}; } -void Run(AgcDriver::VulkanDevice& device, std::uint32_t opcode, std::uint32_t format, std::uint32_t swizzle = 0xfacu) { - const auto code = Code(opcode); - const auto output = reinterpret_cast(Output.data()); +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::uint32_t TexelValue(std::uint32_t level, std::uint32_t x, std::uint32_t y, std::uint32_t component, std::uint32_t bytes) { + const auto value = level * 0x9e37u + y * 0x51u + x * 0x13u + component * 0x2bu + 1u; + return bytes == 1u ? value & 0xffu : value & 0xffffu; +} + +void FillTexels(const Format& format) { + Texels.fill(0xeeu); + const auto descriptor = Descriptor(format.id, 0xfacu); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor)); + Require(surface.mips.size() == Levels, "image BY mip count"); + for (std::uint32_t level = 0; level < Levels; ++level) { + const auto& mip = surface.mips[level]; + const auto width = std::max(Width >> level, 1u); + const auto height = std::max(Height >> level, 1u); + for (std::uint32_t y = 0; y < height; ++y) { + for (std::uint32_t x = 0; x < width; ++x) { + for (std::uint32_t component = 0; component < format.components; ++component) { + const auto value = TexelValue(level, x, y, component, format.bytes); + const auto offset = mip.tiledOffset + static_cast(y) * mip.pitchBytes + (x * format.components + component) * format.bytes; + Require(offset + format.bytes <= Texels.size(), "image BY texture size"); + std::memcpy(Texels.data() + offset, &value, format.bytes); + } + } + } + } +} + +void FillInput() { + constexpr std::array xs{0u, 1u, 2u, 3u, 5u, 6u, 7u, Width - 4u, Width - 3u, Width - 2u, Width - 1u, Width, 29u, 30u, 0xffffffffu, 0x80000000u}; + constexpr std::array ys{0u, 1u, 2u, Height, 0xffffffffu}; + constexpr std::array mips{0u, 1u, Levels}; + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + Input[thread * 4u + 0u] = xs[thread % xs.size()]; + Input[thread * 4u + 1u] = ys[(thread * 3u) % ys.size()]; + Input[thread * 4u + 2u] = mips[(thread / 2u) % mips.size()]; + Input[thread * 4u + 3u] = 0u; + } +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t opcode, std::uint32_t format, std::uint32_t dmask, std::uint32_t swizzle = 0xfacu) { + const auto code = Code(opcode, dmask); std::vector userData(16, 0u); - userData[0] = static_cast(output); - userData[1] = static_cast(output >> 32u); - userData[2] = static_cast(Output.size() * sizeof(std::uint32_t)); - userData[3] = 0x01016facu; + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); const auto descriptor = Descriptor(format, swizzle); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + std::copy(output.begin(), output.end(), userData.begin()); std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 4); + std::copy(input.begin(), input.end(), userData.begin() + 12); const std::span words(code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(words)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; @@ -71,53 +123,41 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t opcode, std::uint32_t fo const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); - if ((opcode & 0x10u) != 0u) { - AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(Texels.data()), Bytes, nullptr, "image BY test"); - device.WaitIdle(); - } } -void Check(AgcDriver::VulkanDevice& device, std::uint32_t opcode) { +void Check(AgcDriver::VulkanDevice& device, std::uint32_t opcode, const Format& format, std::uint32_t dmask) { const auto elements = (opcode & 1u) != 0u ? 4u : 2u; - const auto channels = 4u / elements; - const auto format = elements == 2u ? 62u : 20u; - const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format, Width, 1u, 2u); - Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1u) <= Texels.size(), "image BY texture size"); - Texels.fill(0xeeu); - for (std::uint32_t level = 0; level < mips.size(); ++level) { - for (std::uint32_t x = 0; x < mips[level].width; ++x) { - for (std::uint32_t channel = 0; channel < channels; ++channel) { - const auto value = level * 100000u + x * 100u + channel + 7u; - std::memcpy(Texels.data() + mips[level].tiledOffset + (x * channels + channel) * 4u, &value, 4u); - } - } - } + const bool mip = (opcode & 8u) != 0u; + const auto dwords = static_cast(std::popcount(dmask)); + const auto name = "image BY opcode " + std::to_string(opcode) + " format " + std::to_string(format.id) + " dmask " + std::to_string(dmask); + FillTexels(format); + FillInput(); const auto initial = Texels; Output.fill(0xdeadbeefu); - Run(device, opcode, format); - const auto level = (opcode & 8u) != 0u ? 1u : 0u; - if ((opcode & 0x10u) != 0u) { - auto expected = initial; - for (std::uint32_t thread = 0; thread < Threads; ++thread) { - std::memcpy(expected.data() + mips[level].tiledOffset + thread * 16u, Values.data(), 16u); + Run(device, opcode, format.id, dmask); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + const auto x = Input[thread * 4u + 0u]; + const auto y = Input[thread * 4u + 1u]; + const auto level = mip ? Input[thread * 4u + 2u] : 0u; + const auto width = level < Levels ? std::max(Width >> level, 1u) : 0u; + const auto height = level < Levels ? std::max(Height >> level, 1u) : 0u; + const bool inside = level < Levels && x < width && width - x >= elements && y < height; + const auto first = x & ~(elements - 1u); + for (std::uint32_t index = 0; index < 4u; ++index) { + auto expected = Sentinel + index; + if (index < dwords) expected = inside ? TexelValue(level, first + index / format.components, y, index % format.components, format.bytes) : 0u; + const auto actual = Output[thread * 4u + index]; + Require(actual == expected, name + ": thread " + std::to_string(thread) + " (x " + std::to_string(x) + ", y " + std::to_string(y) + ", mip " + std::to_string(level) + ") v" + std::to_string(10u + index) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected)); } - Require(Texels == expected, "image BY store changes adjacent texels, preserving the other mip and untouched bytes"); - } else { - for (std::uint32_t thread = 0; thread < Threads; ++thread) { - for (std::uint32_t index = 0; index < 4u; ++index) { - const auto expected = level * 100000u + (thread * elements + index / channels) * 100u + index % channels + 7u; - Require(Output[thread * 4u + index] == expected, "image BY load texel/channel order"); - } - } - Require(Texels == initial, "image BY load leaves the source unchanged"); } + Require(Texels == initial, name + ": the load changed the texture"); } -void Refused(AgcDriver::VulkanDevice& device, std::uint32_t opcode, std::uint32_t format, std::uint32_t swizzle = 0xfacu) { +void Refused(AgcDriver::VulkanDevice& device, std::uint32_t opcode, std::uint32_t format, std::uint32_t dmask, std::uint32_t swizzle, const char* reason) { bool refused = false; - try { Run(device, opcode, format, swizzle); } - catch (const std::exception& error) { refused = std::string(error.what()).find("RG32_UINT/R32_UINT") != std::string::npos; } - Require(refused, "image BY unsupported format/swizzle rejection"); + try { Run(device, opcode, format, dmask, swizzle); } + catch (const std::exception& error) { refused = std::string(error.what()).find(reason) != std::string::npos; } + Require(refused, "image BY opcode " + std::to_string(opcode) + " format " + std::to_string(format) + " was not refused"); } } @@ -127,11 +167,22 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; GuestAllocations::Mutation().Add(Texels.data(), Bytes, true, true); - for (auto opcode : Encodings) { - Check(*device, opcode); - Refused(*device, opcode, (opcode & 1u) != 0u ? 62u : 20u); - Refused(*device, opcode, (opcode & 1u) != 0u ? 20u : 62u, 0xf2eu); + constexpr Format r8{R8UInt, 1u, 1u}; + constexpr Format r16{R16UInt, 2u, 1u}; + constexpr Format rg8{Rg8UInt, 1u, 2u}; + for (const auto opcode : {0x42u, 0x4au}) { + Check(*device, opcode, r8, 0x3u); + Check(*device, opcode, r16, 0x3u); + Check(*device, opcode, rg8, 0xfu); } + for (const auto opcode : {0x43u, 0x4bu}) Check(*device, opcode, r8, 0xfu); + constexpr auto layout = "elements fill one dword"; + Refused(*device, 0x42u, R32UInt, 0x3u, 0xfacu, layout); + Refused(*device, 0x42u, R8UInt, 0xfu, 0xfacu, layout); + Refused(*device, 0x43u, R16UInt, 0xfu, 0xfacu, layout); + Refused(*device, 0x43u, Rg8UInt, 0xfu, 0xfacu, layout); + Refused(*device, 0x42u, R16UInt, 0x3u, 0xf2eu, layout); + for (const auto opcode : {0x52u, 0x53u, 0x5au, 0x5bu}) Refused(*device, opcode, R8UInt, 0xfu, 0xfacu, "stores are not implemented"); GuestAllocations::Mutation().Remove(Texels.data()); std::cout << "image BY2/BY4 execution tests passed\n"; return 0; diff --git a/core/libs/tests/ImageBy.cpp b/core/libs/tests/ImageBy.cpp index e02c22450..c636525f0 100644 --- a/core/libs/tests/ImageBy.cpp +++ b/core/libs/tests/ImageBy.cpp @@ -1,6 +1,7 @@ #include "Translation/TranslationContext.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" #include +#include #include #include #include @@ -22,50 +23,41 @@ RdnaInstruction Decode(std::uint32_t encoding, std::uint32_t controls = 0u, std: return DecodeRdnaMimg(0u, code, 0u); } -void Check(std::uint32_t index, bool nsa) { +void Check(std::uint32_t index, bool nsa, std::uint32_t mask) { const auto encoding = Encodings[index]; - const auto inst = Decode(encoding, nsa ? 2u : 0u); + const auto inst = Decode(encoding, nsa ? 2u : 0u, 0x00101e1eu, mask); const auto elements = (encoding & 1u) != 0u ? 4u : 2u; - const auto channels = 4u / elements; + const auto dwords = static_cast(std::popcount(mask)); const bool store = (encoding & 0x10u) != 0u; Require(inst.op == Opcodes[index] && IsImageOpcode(inst.op)); - Require(inst.dataDwordCount == 4u && inst.dataComponents == 4u); + Require(inst.dataDwordCount == dwords && inst.dataComponents == dwords); Require(inst.imageAddressComponents == ((encoding & 8u) != 0u ? 3u : 2u)); Require(inst.wordCount == (nsa ? 3u : 2u)); IrProgram program; auto& block = program.CreateBlock(); program.SetEntryBlock(block); TranslationContext context(program, block, 256); + if (store) { + bool refused = false; + try { context.TranslateInstruction(inst); } + catch (const std::runtime_error& error) { refused = std::string(error.what()).find("stores are not implemented") != std::string::npos; } + Require(refused); + return; + } context.TranslateInstruction(inst); - std::vector operations; - bool wroteRegister = false; + std::vector reads; std::uint32_t writes = 0; for (auto* value : block.Instructions()) { - if (value->Opcode() == IrOpcode::SetVectorRegister) { - wroteRegister = true; - ++writes; - } - if (value->Opcode() == (store ? IrOpcode::ImageWrite : IrOpcode::ImageRead)) { - Require(!wroteRegister); - operations.push_back(value); - } - } - Require(operations.size() == elements && writes == (store ? 0u : 4u)); - IrValue* first = operations[0]->Argument(1)->Resolve(); - for (std::uint32_t texel = 0; texel < elements; ++texel) { - const auto& memory = program.Resources().memoryInfo[operations[texel]->Flags().index]; - Require(memory.imageByElements == elements && memory.dmask == (1u << channels) - 1u); - Require(memory.dataDwords == channels && memory.componentCount == channels); - Require(memory.imageHasMip == ((encoding & 8u) != 0u)); - IrValue* address = operations[texel]->Argument(1)->Resolve(); - Require(address->Opcode() == IrOpcode::MakeImageAddress); - for (std::uint32_t component = 1; component < 13u; ++component) Require(address->Argument(component) == first->Argument(component)); - if (texel != 0u) { - auto* x = address->Argument(0)->Resolve(); - Require(x->Opcode() == IrOpcode::IAdd32 && x->Argument(0) == first->Argument(0)); - Require(x->Argument(1)->ImmediateU32() == texel); + if (value->Opcode() == IrOpcode::SetVectorRegister) ++writes; + if (value->Opcode() == IrOpcode::ImageRead) { + Require(writes == 0u); + reads.push_back(value); } } + Require(reads.size() == 1u && writes == dwords); + const auto& memory = program.Resources().memoryInfo[reads[0]->Flags().index]; + Require(memory.imageByElements == elements && memory.dmask == mask && memory.dataDwords == dwords); + Require(memory.imageHasMip == ((encoding & 8u) != 0u)); } void CheckOrdinary(std::uint32_t encoding) { @@ -99,9 +91,14 @@ void Refused(std::uint32_t encoding, std::uint32_t control, std::uint32_t word1 int main() { for (auto encoding : {0u, 1u, 8u, 9u}) CheckOrdinary(encoding); for (std::uint32_t index = 0; index < Encodings.size(); ++index) { - Check(index, false); - Check(index, true); - for (std::uint32_t mask = 0u; mask < 15u; ++mask) Refused(Encodings[index], 0u, 0x00101e1eu, mask); + const bool by2 = (Encodings[index] & 1u) == 0u; + for (const bool nsa : {false, true}) { + Check(index, nsa, 15u); + if (by2) Check(index, nsa, 3u); + } + for (std::uint32_t mask = 0u; mask < 15u; ++mask) { + if (!(by2 && mask == 3u)) Refused(Encodings[index], 0u, 0x00101e1eu, mask); + } for (auto bits : {0x8000u, 0x10000u, 0x20000u}) Refused(Encodings[index], bits); for (auto bits : {0x40000000u, 0x80000000u}) Refused(Encodings[index], 0u, 0x00101e1eu | bits); Refused(Encodings[index], 0u, 0x0010fd1eu); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 0557785ae..ec2d0c79f 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -84,6 +84,7 @@ struct ImageResource { bool fmaskCompatible = true; bool depthBitsCompatible = true; std::uint32_t byElements = 0; + std::uint32_t byComponents = 0; IrBufferFormat packedFormat = IrBufferFormat::Invalid; std::uint32_t emulatedCompare = 0; std::uint32_t indirectRoot = NoIndirectImage; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index a09f37156..c2134f4cd 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -170,9 +170,12 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.packedFormat = format; } if (base.byElements != 0u) { - const auto required = base.byElements == 2u ? IrBufferFormat::Format32_32UInt : IrBufferFormat::Format32UInt; - if ((base.byElements != 2u && base.byElements != 4u) || format != required || descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle || rawImageType(descriptor) != ImageType::Color2D || base.indirectRoot != ImageResource::NoIndirectImage) { - throw std::runtime_error("MIMG BY2/BY4 requires a direct, identity-swizzled 2D RG32_UINT/R32_UINT image respectively"); + const bool eightBit = format == IrBufferFormat::Format8UNorm || format == IrBufferFormat::Format8SNorm || format == IrBufferFormat::Format8UInt || format == IrBufferFormat::Format8SInt; + const bool sixteenBit = format == IrBufferFormat::Format16UNorm || format == IrBufferFormat::Format16SNorm || format == IrBufferFormat::Format16UInt || format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format16Float; + const bool eightBitPair = format == IrBufferFormat::Format8_8UNorm || format == IrBufferFormat::Format8_8SNorm || format == IrBufferFormat::Format8_8UInt || format == IrBufferFormat::Format8_8SInt; + const bool measured = base.byElements == 4u ? base.byComponents == 1u && eightBit : base.byElements == 2u && (base.byComponents == 1u ? eightBit || sixteenBit : base.byComponents == 2u && eightBitPair); + if (!measured || descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle || rawImageType(descriptor) != ImageType::Color2D || base.indirectRoot != ImageResource::NoIndirectImage) { + throw std::runtime_error("MIMG BY2/BY4 requires a direct, identity-swizzled 2D image whose elements fill one dword: R8, R16 or RG8 for BY2, R8 for BY4"); } } decoded.conversionFormat = RemapTextureFormat(format) != format ? format : IrBufferFormat::Invalid; @@ -603,7 +606,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.shaderSwizzle = ShaderImageIdentitySwizzle; if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; modes.push_back(mode); - if (image.dimension == RdnaImageDimension::Dim2D && image.fmaskCompatible && !depth && packed == IrBufferFormat::Invalid) { + if (image.dimension == RdnaImageDimension::Dim2D && image.fmaskCompatible && !depth && packed == IrBufferFormat::Invalid && image.byElements == 0u) { auto volume = mode; volume.dimension = RdnaImageDimension::Dim3D; modes.push_back(volume); diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 8d0ffc71b..ddeeb2cb0 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -867,7 +867,7 @@ private: const bool depth = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; for (std::uint32_t i = 0; i < m_info.images.size(); i++) { auto& image = m_info.images[i]; - if (image.source == source && image.resourceClass == resourceClass && image.dimension == memory.imageDimension && image.mipMode == mip && image.depthCompare == depth && image.r128 == memory.imageR128 && image.packed == memory.imagePacked && image.byElements == memory.imageByElements) { + if (image.source == source && image.resourceClass == resourceClass && image.dimension == memory.imageDimension && image.mipMode == mip && image.depthCompare == depth && image.r128 == memory.imageR128 && image.packed == memory.imagePacked && image.byElements == memory.imageByElements && image.byComponents == (memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u)) { Merge(image, memory, op, pc); return i; } @@ -885,6 +885,7 @@ private: image.r128 = memory.imageR128; image.packed = memory.imagePacked; image.byElements = memory.imageByElements; + image.byComponents = memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u; Merge(image, memory, op, pc); m_info.images.push_back(image); return static_cast(m_info.images.size() - 1); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index 6d1ca7163..7a93a085f 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -359,8 +359,8 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 8u) & 15u; const bool pckGather = opcode == 0x62u || opcode == 0x63u; const bool by = opcode == 0x42u || opcode == 0x43u || opcode == 0x4au || opcode == 0x4bu || opcode == 0x52u || opcode == 0x53u || opcode == 0x5au || opcode == 0x5bu; - if (by && (dmask != 15u || a16 || d16 || dimension != RdnaImageDimension::Dim2D || (word0 & 0x8000u) != 0u)) { - throw std::runtime_error("MIMG BY2/BY4 requires a full data mask, 32-bit addresses and data, a 2D image and a full descriptor"); + if (by && ((dmask != 15u && (dmask != 3u || (opcode & 1u) != 0u)) || a16 || d16 || dimension != RdnaImageDimension::Dim2D || (word0 & 0x8000u) != 0u)) { + throw std::runtime_error("MIMG BY2/BY4 requires a data mask that covers every element (BY2 0x3 or 0xf, BY4 0xf), 32-bit addresses and data, a 2D image and a full descriptor"); } const bool compareSwap = info.opcode == RdnaOpcode::ImageAtomicCmpswap || info.opcode == RdnaOpcode::ImageAtomicFcmpswap; const bool floatAtomic = info.opcode == RdnaOpcode::ImageAtomicFcmpswap || info.opcode == RdnaOpcode::ImageAtomicFmin || info.opcode == RdnaOpcode::ImageAtomicFmax; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index c69f9d09f..10a60c556 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -74,7 +74,7 @@ static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: updat static_assert(sizeof(CompiledShaderInfo) == 336, "CompiledShaderInfo changed: update the info encoder"); static_assert(sizeof(ShaderInfo) == 224, "ShaderInfo changed: update the info encoder"); static_assert(sizeof(BufferResource) == 32, "BufferResource changed: update the info encoder"); -static_assert(sizeof(ImageResource) == 104, "ImageResource changed: update the info encoder"); +static_assert(sizeof(ImageResource) == 112, "ImageResource changed: update the info encoder"); static_assert(sizeof(SamplerResource) == 16, "SamplerResource changed: update the info encoder"); static_assert(sizeof(SampledResourcePair) == 12, "SampledResourcePair changed: update the info encoder"); static_assert(sizeof(StageInput) == 56, "StageInput changed: update the info encoder"); @@ -473,6 +473,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(image.fmaskCompatible); out.Value(image.depthBitsCompatible); out.Value(image.byElements); + out.Value(image.byComponents); out.Value(image.packedFormat); out.Value(image.emulatedCompare); out.Value(image.indirectRoot); @@ -572,6 +573,7 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.fmaskCompatible); in.Value(image.depthBitsCompatible); in.Value(image.byElements); + in.Value(image.byComponents); in.Value(image.packedFormat); in.Value(image.emulatedCompare); in.Value(image.indirectRoot); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 8807621c2..bd373bdac 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -871,7 +871,69 @@ void EmitQueryLodOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { ctx.Define(access.inst, TableResult(ctx, access, result)); } +void EmitByReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { + auto& state = ctx.state; + const auto elements = access.mem.imageByElements; + const auto components = access.mem.dataDwords / elements; + if (access.image.dimension != RdnaImageDimension::Dim2D || components == 0u || components * elements != access.mem.dataDwords || access.mem.dataBits != 32u) { + ctx.Fail(access.inst, "is a MIMG BY2/BY4 load outside the measured 2D, 32-bit data subset"); + } + state.module.EmitCapability(spv::CapabilityImageQuery); + const auto condition = ctx.Arg(access.inst, 2); + ctx.Define(access.inst, EmitValueOrDefaultIfCondition(state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4), [&]() { + const auto image = LoadSampledImageDescriptor(state, access.mem.resource, access.slot); + const auto x = AddressU32(ctx, access, 0); + const auto y = AddressU32(ctx, access, 1); + const auto lod = LodU32(ctx, access); + const auto levels = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageQueryLevels, TypeU32(state), levels, image); + const auto lodInside = Binary(state, spv::OpULessThan, TypeBool(state), lod, levels); + const auto queryLod = Select(state, TypeU32(state), lodInside, lod, ConstantU32(state, 0)); + const auto size = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageQuerySizeLod, ImageViewSizeType(state, access.image.dimension), size, image, ConstantU32(state, 0)); + const auto levelExtent = [&](std::uint32_t axis) { + const auto base = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), base, size, axis); + const auto shifted = Binary(state, spv::OpShiftRightLogical, TypeU32(state), base, queryLod); + return Select(state, TypeU32(state), Binary(state, spv::OpUGreaterThan, TypeBool(state), shifted, ConstantU32(state, 1)), shifted, ConstantU32(state, 1)); + }; + const auto width = levelExtent(0u); + const auto height = levelExtent(1u); + auto inside = Binary(state, spv::OpLogicalAnd, TypeBool(state), lodInside, Binary(state, spv::OpULessThan, TypeBool(state), x, width)); + inside = Binary(state, spv::OpLogicalAnd, TypeBool(state), inside, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), Binary(state, spv::OpISub, TypeU32(state), width, x), ConstantU32(state, elements))); + inside = Binary(state, spv::OpLogicalAnd, TypeBool(state), inside, Binary(state, spv::OpULessThan, TypeBool(state), y, height)); + const auto first = Select(state, TypeU32(state), inside, Binary(state, spv::OpBitwiseAnd, TypeU32(state), x, ConstantU32(state, ~(elements - 1u))), ConstantU32(state, 0)); + const auto row = Select(state, TypeU32(state), inside, y, ConstantU32(state, 0)); + MemoryInfo texelMemory = access.mem; + texelMemory.dmask = (1u << components) - 1u; + texelMemory.dataDwords = components; + texelMemory.componentCount = components; + const ImageEmitAccess texelAccess{access.inst, texelMemory, access.image, access.address, access.table, access.slot}; + std::uint32_t words[4] = {ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0)}; + for (std::uint32_t element = 0; element < elements; ++element) { + const auto column = Binary(state, spv::OpIAdd, TypeU32(state), first, ConstantU32(state, element)); + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2), coord, column, row); + const auto color = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageFetch, ImageVectorType(state, access.image.numericClass, 4), color, image, coord, spv::ImageOperandsLodMask, queryLod); + const auto texel = ResultVector(ctx, texelAccess, UnpackImageTexel(ctx, texelAccess, color), access.image.numericClass, false, false); + for (std::uint32_t component = 0; component < components; ++component) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, texel, component); + words[element * components + component] = Select(state, TypeU32(state), inside, value, ConstantU32(state, 0)); + } + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), result, words[0], words[1], words[2], words[3]); + return TableResult(ctx, access, result); + })); +} + void EmitReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { + if (access.mem.imageByElements != 0u) { + EmitByReadOp(ctx, access); + return; + } auto& state = ctx.state; const auto& dimensionInfo = RdnaImageDimensionInfoFor(access.image.dimension); const auto numericClass = access.image.numericClass; diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index 282ae5e63..16a13a29c 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -139,51 +139,17 @@ bool TranslationContext::imageGetLod(const RdnaInstruction& inst) { } bool TranslationContext::imageBy(const RdnaInstruction& inst) { - const auto elements = (inst.imageOpcodeId & 1u) != 0u ? 4u : 2u; - const auto channels = 4u / elements; - const bool store = (inst.imageOpcodeId & 0x10u) != 0u; + if ((inst.imageOpcodeId & 0x10u) != 0u) { + throw std::runtime_error("MIMG BY2/BY4 stores are not implemented"); + } MemoryInfo memory = imageMemoryInfoFromInstruction(inst); memory.imageHasMip = (inst.imageOpcodeId & 8u) != 0u; - memory.imageByElements = elements; - memory.dmask = (1u << channels) - 1u; - memory.dataDwords = channels; - memory.componentCount = channels; + memory.imageByElements = (inst.imageOpcodeId & 1u) != 0u ? 4u : 2u; IrValue* resource = getImageResource(memory); - IrValue* base = makeImageAddress(inst, inst.source0); + IrValue* address = makeImageAddress(inst, inst.source0); IrValue& exec = ir.GetExec(); - std::array data{}; - if (store) { - for (std::uint32_t index = 0; index < 4u; ++index) { - data[index] = &readRawU32(offsetOperand(inst.destination, index)).Value(); - } - } - for (std::uint32_t texel = 0; texel < elements; ++texel) { - std::array address{}; - for (std::uint32_t index = 0; index < address.size(); ++index) { - address[index] = base->Argument(index); - } - if (texel != 0u) { - address[0] = &ir.IAdd(*address[0], ir.Constant(texel)); - } - IrValue& location = ir.Emit(IrOpcode::MakeImageAddress, IrOpcodeType(IrOpcode::MakeImageAddress), - {address[0], address[1], address[2], address[3], address[4], address[5], address[6], address[7], address[8], address[9], address[10], address[11], address[12]}); - if (store) { - IrValue& zero = ir.Constant(0u); - IrValue& value = ir.Emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, - {data[texel * channels], channels == 2u ? data[texel * channels + 1u] : &zero, &zero, &zero}); - ir.Emit(IrOpcode::ImageWrite, IrOpcodeType(IrOpcode::ImageWrite), {resource, &location, &value, &exec}, addMemoryInfo(memory, inst.programCounter)); - } else { - IrValue& value = ir.Emit(IrOpcode::ImageRead, IrOpcodeType(IrOpcode::ImageRead), {resource, &location, &exec}, addMemoryInfo(memory, inst.programCounter)); - for (std::uint32_t channel = 0; channel < channels; ++channel) { - data[texel * channels + channel] = &ir.CompositeExtract(value, channel); - } - } - } - if (!store) { - for (std::uint32_t index = 0; index < 4u; ++index) { - writeOperand(offsetOperand(inst.destination, index), data[index]); - } - } + IrValue& result = ir.Emit(IrOpcode::ImageRead, IrOpcodeType(IrOpcode::ImageRead), {resource, address, &exec}, addMemoryInfo(memory, inst.programCounter)); + writeImageComponents(inst.destination, &result, memory, 4u); return true; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 371c4318b..c209de0e9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -256,6 +256,7 @@ Throughout the project, every function at every stage either **does exactly what - [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not modelled: `scePthreadCancel` throws, so no cancel request is ever pending and `scePthreadTestcancel` and `pthread_testcancel` return without acting; the cancel state and type set by `scePthreadSetcancelstate` and `scePthreadSetcanceltype` are stored and not used - [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. +- [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. - [Unnormalized S# coordinates](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. - [Unnormalized S#s](../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without offset, comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. - An [unnormalized S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border.