diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b5e0d2eda..7d05a2b54 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1039,6 +1039,14 @@ if(BUILD_TESTING) set_tests_properties(image_by PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(image_pck_n_tests ${LIBS_INCLUDE_DIR}/tests/ImagePckN.cpp) + target_link_libraries(image_pck_n_tests PRIVATE shader_recompiler_agc_driver libc) + configure_windows_unwind(image_pck_n_tests) + add_test(NAME image_pck_n COMMAND image_pck_n_tests) + if(WIN32) + set_tests_properties(image_pck_n 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/execution/ImagePckN.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePckN.cpp new file mode 100644 index 000000000..b16b20847 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImagePckN.cpp @@ -0,0 +1,196 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +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(0xf0000008u | (opcode << 18u) | (dmask << 8u)); + code.push_back(0x00010a1eu); + code.push_back(0xbf8c3f70u); + 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) { + 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) | ((Height - 1u) << 14u), swizzle | ((Levels - 1u) << 16u) | (9u << 28u), 0u, 1u << 4u, 0u, 0u}; +} + +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 PCK 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 PCK 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); + 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}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), words, 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 Check(AgcDriver::VulkanDevice& device, std::uint32_t opcode, const Format& format, std::uint32_t dmask) { + const auto elements = opcode == 0x71u || opcode == 0x74u ? 4u : 2u; + const bool mip = opcode == 0x73u || opcode == 0x74u; + const auto texelBits = format.bytes * format.components * 8u; + const auto name = "image PCK 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.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); + std::uint32_t packed = 0; + for (std::uint32_t element = 0; inside && element < elements; ++element) { + std::uint32_t texel = 0; + for (std::uint32_t component = 0; component < format.components; ++component) texel |= TexelValue(level, first + element, y, component, format.bytes) << (component * format.bytes * 8u); + packed |= texel << (element * texelBits); + } + for (std::uint32_t index = 0; index < 4u; ++index) { + const auto expected = index == 0u ? packed : Sentinel + index; + 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 == initial, name + ": the load changed the texture"); +} + +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, dmask, swizzle); } + catch (const std::exception& error) { refused = std::string(error.what()).find(reason) != std::string::npos; } + Require(refused, "image PCK opcode " + std::to_string(opcode) + " format " + std::to_string(format) + " was not refused"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestAllocations::Mutation().Add(Texels.data(), Bytes, true, true); + constexpr Format r8{R8UInt, 1u, 1u}; + constexpr Format r16{R16UInt, 2u, 1u}; + constexpr Format rg8{Rg8UInt, 1u, 2u}; + for (const auto opcode : {0x70u, 0x73u}) { + Check(*device, opcode, r8, 0x1u); + Check(*device, opcode, r16, 0x1u); + Check(*device, opcode, rg8, 0x1u); + } + for (const auto opcode : {0x71u, 0x74u}) Check(*device, opcode, r8, 0x1u); + constexpr auto layout = "elements fill one dword"; + Refused(*device, 0x70u, R32UInt, 0x1u, 0xfacu, layout); + Refused(*device, 0x71u, R16UInt, 0x1u, 0xfacu, layout); + Refused(*device, 0x71u, Rg8UInt, 0x1u, 0xfacu, layout); + Refused(*device, 0x70u, R16UInt, 0x1u, 0xf2eu, "identity swizzle"); + for (const auto opcode : {0x76u, 0x77u, 0x79u, 0x7au}) Refused(*device, opcode, R8UInt, 0x1u, 0xfacu, "stores are not implemented"); + GuestAllocations::Mutation().Remove(Texels.data()); + std::cout << "image PCK2/PCK4 execution tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/tests/ImagePckN.cpp b/core/libs/tests/ImagePckN.cpp new file mode 100644 index 000000000..11898091d --- /dev/null +++ b/core/libs/tests/ImagePckN.cpp @@ -0,0 +1,106 @@ +#include "Translation/TranslationContext.hpp" +#include "RdnaDecoder/RdnaImageOpDecoder.hpp" +#include +#include +#include +#include +#include +#include + +using namespace ShaderRecompiler; + +namespace { + +void Require(bool value, std::source_location location = std::source_location::current()) { + if (!value) throw std::runtime_error("image PCK2/PCK4 regression at line " + std::to_string(location.line())); +} + +constexpr std::array Encodings{0x70u, 0x71u, 0x73u, 0x74u, 0x76u, 0x77u, 0x79u, 0x7au}; +constexpr std::array Opcodes{RdnaOpcode::ImageLoadPck2, RdnaOpcode::ImageLoadPck4, RdnaOpcode::ImageLoadMipPck2, RdnaOpcode::ImageLoadMipPck4, RdnaOpcode::ImageStorePck2, RdnaOpcode::ImageStorePck4, RdnaOpcode::ImageStoreMipPck2, RdnaOpcode::ImageStoreMipPck4}; + +RdnaInstruction Decode(std::uint32_t encoding, std::uint32_t controls = 0u, std::uint32_t word1 = 0x00101e1eu, std::uint32_t mask = 15u, std::uint32_t dimension = 1u) { + const std::array code{0xf0000000u | (dimension << 3u) | (encoding << 18u) | (mask << 8u) | controls, word1, 0x0000201fu}; + return DecodeRdnaMimg(0u, code, 0u); +} + +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, 0x00101e1eu, mask); + const auto elements = encoding == 0x71u || encoding == 0x74u || encoding == 0x77u || encoding == 0x7au ? 4u : 2u; + const bool mip = encoding == 0x73u || encoding == 0x74u || encoding == 0x79u || encoding == 0x7au; + const auto dwords = static_cast(std::popcount(mask)); + const bool store = encoding >= 0x76u; + Require(inst.op == Opcodes[index] && IsImageOpcode(inst.op)); + Require(inst.dataDwordCount == dwords && inst.dataComponents == dwords); + Require(inst.imageAddressComponents == (mip ? 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 reads; + std::uint32_t writes = 0; + for (auto* value : block.Instructions()) { + 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.imagePacked && memory.dmask == mask && memory.dataDwords == dwords); + Require(memory.imageHasMip == mip); +} + +void CheckOrdinary(std::uint32_t encoding) { + const auto inst = Decode(encoding); + const bool store = (encoding & 8u) != 0u; + IrProgram program; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + TranslationContext context(program, block, 256); + context.TranslateInstruction(inst); + std::uint32_t operations = 0; + for (auto* value : block.Instructions()) { + if (value->Opcode() == (store ? IrOpcode::ImageWrite : IrOpcode::ImageRead)) { + const auto& memory = program.Resources().memoryInfo[value->Flags().index]; + Require(memory.imageByElements == 0u && memory.dmask == 15u && memory.dataDwords == 4u); + ++operations; + } + } + Require(operations == 1u); +} + +void Refused(std::uint32_t encoding, std::uint32_t control, std::uint32_t word1 = 0x00101e1eu, std::uint32_t mask = 15u, std::uint32_t dimension = 1u) { + bool refused = false; + try { (void)Decode(encoding, control, word1, mask, dimension); } + catch (const std::runtime_error&) { refused = true; } + Require(refused); +} + +} + +int main() { + for (auto encoding : {0u, 1u, 8u, 9u}) CheckOrdinary(encoding); + for (std::uint32_t index = 0; index < Encodings.size(); ++index) { + for (const bool nsa : {false, true}) Check(index, nsa, 1u); + for (std::uint32_t mask = 0u; mask < 16u; ++mask) { + if (mask != 1u) Refused(Encodings[index], 0u, 0x00101e1eu, mask); + } + for (auto bits : {0x8000u, 0x10000u, 0x20000u}) Refused(Encodings[index], bits, 0x00101e1eu, 1u); + for (auto bits : {0x40000000u, 0x80000000u}) Refused(Encodings[index], 0u, 0x00101e1eu | bits, 1u); + Refused(Encodings[index], 0u, 0x00101effu, 1u); + for (std::uint32_t dimension = 0; dimension < 8u; ++dimension) { + if (dimension != 1u) Refused(Encodings[index], 0u, 0x00101e1eu, 1u, dimension); + } + } +} diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index c2134f4cd..1623afd77 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -173,9 +173,13 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag 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); + const bool packedEight = format == IrBufferFormat::Format8UNorm || format == IrBufferFormat::Format8UInt || format == IrBufferFormat::Format8SInt; + const bool packedSixteen = format == IrBufferFormat::Format16UNorm || format == IrBufferFormat::Format16UInt || format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format8_8UNorm || format == IrBufferFormat::Format8_8UInt || format == IrBufferFormat::Format8_8SInt; + const bool measured = base.packed ? packedEight || (base.byElements == 2u && packedSixteen) + : 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"); + throw std::runtime_error(base.packed ? "MIMG PCK2/PCK4 requires a direct, identity-swizzled 2D UNORM, UINT or SINT image whose elements fill one dword: R8, R16 or RG8 for PCK2, R8 for PCK4" + : "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; @@ -628,6 +632,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe const auto format = static_cast(value); const auto info = GetFormatInfo(format); if (info.packedBitfield || info.componentCount == 0u || (storage && info.byteSize == 12u)) continue; + if (image.byElements != 0u && info.byteSize * 8u * image.byElements > 32u) continue; const auto numeric = storage && (format == IrBufferFormat::Format32SInt || format == IrBufferFormat::Format32_32SInt || format == IrBufferFormat::Format32_32_32_32SInt) ? IrTextureNumericClass::Uint : SampledTextureNumericClass(format); bool supported = numeric != IrTextureNumericClass::Unsupported && (!storage || numeric != IrTextureNumericClass::Sint); for (std::uint32_t component = 0u; component < info.componentCount; ++component) { diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index c6963dcd8..3be783c0c 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -970,6 +970,14 @@ enum class RdnaOpcode : std::uint16_t { ImageStoreBy4, ImageStoreMipBy2, ImageStoreMipBy4, + ImageLoadPck2, + ImageLoadPck4, + ImageLoadMipPck2, + ImageLoadMipPck4, + ImageStorePck2, + ImageStorePck4, + ImageStoreMipPck2, + ImageStoreMipPck4, ImageMsaaLoad, ImageAtomicSwap, ImageAtomicAdd, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index 7a93a085f..640be3b59 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -142,6 +142,14 @@ constexpr ImageOpcodeInfo imageOpcodes[] = { {0x53u, RdnaOpcode::ImageStoreBy4, nullptr, 0, false, false, false}, {0x5au, RdnaOpcode::ImageStoreMipBy2, nullptr, 0, false, false, false}, {0x5bu, RdnaOpcode::ImageStoreMipBy4, nullptr, 0, false, false, false}, + {0x70u, RdnaOpcode::ImageLoadPck2, nullptr, 0, false, false, false}, + {0x71u, RdnaOpcode::ImageLoadPck4, nullptr, 0, false, false, false}, + {0x73u, RdnaOpcode::ImageLoadMipPck2, nullptr, 0, false, false, false}, + {0x74u, RdnaOpcode::ImageLoadMipPck4, nullptr, 0, false, false, false}, + {0x76u, RdnaOpcode::ImageStorePck2, nullptr, 0, false, false, false}, + {0x77u, RdnaOpcode::ImageStorePck4, nullptr, 0, false, false, false}, + {0x79u, RdnaOpcode::ImageStoreMipPck2, nullptr, 0, false, false, false}, + {0x7au, RdnaOpcode::ImageStoreMipPck4, nullptr, 0, false, false, false}, {0x80u, RdnaOpcode::ImageMsaaLoad, nullptr, 0, false, false, false}, {0x60u, RdnaOpcode::ImageGetLod, nullptr, 0, false, false, false}, {0xe6u, RdnaOpcode::ImageBvhIntersectRay, "image_bvh_intersect_ray", 0, false, false, false}, @@ -362,6 +370,11 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span= 0x70u && opcode <= 0x7au && opcode != 0x72u && opcode != 0x75u && opcode != 0x78u; + const bool pckNMip = opcode == 0x73u || opcode == 0x74u || opcode == 0x79u || opcode == 0x7au; + if (pckN && (dmask != 1u || a16 || d16 || dimension != RdnaImageDimension::Dim2D || (word0 & 0x8000u) != 0u)) { + throw std::runtime_error("MIMG PCK2/PCK4 requires data mask 0x1, 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; const bool atomic64 = info.atomic && !floatAtomic && !d16 && dmask == (compareSwap ? 15u : 3u); @@ -374,7 +387,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span 32u) ctx.Fail(access.inst, "is a MIMG PCK2/PCK4 load whose elements do not fit one dword"); + const auto raw = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), raw, PackedImageTexel(ctx, access, color), 0u); + const auto bits = Binary(state, spv::OpBitwiseAnd, TypeU32(state), raw, ConstantU32(state, texelBits == 32u ? 0xffffffffu : (1u << texelBits) - 1u)); + const auto shifted = element == 0u ? bits : Binary(state, spv::OpShiftLeftLogical, TypeU32(state), bits, ConstantU32(state, element * texelBits)); + words[0] = Binary(state, spv::OpBitwiseOr, TypeU32(state), words[0], Select(state, TypeU32(state), inside, shifted, ConstantU32(state, 0))); + continue; + } 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(); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index 16a13a29c..4b5b9a0fa 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -139,12 +139,15 @@ bool TranslationContext::imageGetLod(const RdnaInstruction& inst) { } bool TranslationContext::imageBy(const RdnaInstruction& inst) { - if ((inst.imageOpcodeId & 0x10u) != 0u) { - throw std::runtime_error("MIMG BY2/BY4 stores are not implemented"); + const auto id = inst.imageOpcodeId; + const bool packed = id >= 0x70u; + if (packed ? id >= 0x76u : (id & 0x10u) != 0u) { + throw std::runtime_error(packed ? "MIMG PCK2/PCK4 stores are not implemented" : "MIMG BY2/BY4 stores are not implemented"); } MemoryInfo memory = imageMemoryInfoFromInstruction(inst); - memory.imageHasMip = (inst.imageOpcodeId & 8u) != 0u; - memory.imageByElements = (inst.imageOpcodeId & 1u) != 0u ? 4u : 2u; + memory.imageHasMip = packed ? id == 0x73u || id == 0x74u : (id & 8u) != 0u; + memory.imagePacked = packed; + memory.imageByElements = packed ? (id == 0x71u || id == 0x74u ? 4u : 2u) : (id & 1u) != 0u ? 4u : 2u; IrValue* resource = getImageResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); IrValue& exec = ir.GetExec(); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp index a204b27f8..456c3cf0f 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp @@ -634,6 +634,14 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) { case RdnaOpcode::ImageStoreBy4: case RdnaOpcode::ImageStoreMipBy2: case RdnaOpcode::ImageStoreMipBy4: + case RdnaOpcode::ImageLoadPck2: + case RdnaOpcode::ImageLoadPck4: + case RdnaOpcode::ImageLoadMipPck2: + case RdnaOpcode::ImageLoadMipPck4: + case RdnaOpcode::ImageStorePck2: + case RdnaOpcode::ImageStorePck4: + case RdnaOpcode::ImageStoreMipPck2: + case RdnaOpcode::ImageStoreMipPck4: return imageBy(inst); case RdnaOpcode::ImageLoad: case RdnaOpcode::ImageLoadMip: diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c209de0e9..3894eec4c 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -257,6 +257,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. +- [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). - [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.