feat(shader): implement MIMG PCK2/PCK4 loads as measured on hardware
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4b6ab6bb73
commit
57d4be33bb
|
|
@ -1039,6 +1039,14 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(image_by PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
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:$<TARGET_FILE_DIR:libc>")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::uint8_t, Bytes> Texels{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Output{};
|
||||
|
||||
struct Format {
|
||||
std::uint32_t id;
|
||||
std::uint32_t bytes;
|
||||
std::uint32_t components;
|
||||
};
|
||||
|
||||
std::vector<std::uint32_t> Code(std::uint32_t opcode, std::uint32_t dmask) {
|
||||
std::vector<std::uint32_t> 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<std::uint32_t, 8> Descriptor(std::uint32_t format, std::uint32_t swizzle) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Texels.data());
|
||||
return {static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(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<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((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<std::uint64_t>(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<std::uint32_t, 16> xs{0u, 1u, 2u, 3u, 5u, 6u, 7u, Width - 4u, Width - 3u, Width - 2u, Width - 1u, Width, 29u, 30u, 0xffffffffu, 0x80000000u};
|
||||
constexpr std::array<std::uint32_t, 5> ys{0u, 1u, 2u, Height, 0xffffffffu};
|
||||
constexpr std::array<std::uint32_t, 3> 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<std::uint32_t> userData(16, 0u);
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output)));
|
||||
const auto descriptor = Descriptor(format, swizzle);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(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<const std::uint32_t> words(code);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(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<std::uintptr_t>(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<std::uintptr_t>(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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
#include "Translation/TranslationContext.hpp"
|
||||
#include "RdnaDecoder/RdnaImageOpDecoder.hpp"
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <stdexcept>
|
||||
#include <source_location>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<std::uint32_t, 8> Encodings{0x70u, 0x71u, 0x73u, 0x74u, 0x76u, 0x77u, 0x79u, 0x7au};
|
||||
constexpr std::array<RdnaOpcode, 8> 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<std::uint32_t, 3> 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::uint32_t>(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<IrValue*> 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<MemoryFlags>().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<MemoryFlags>().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ImageResource> ResourceMaterializer::RuntimeImageModes(const ImageRe
|
|||
const auto format = static_cast<IrBufferFormat>(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) {
|
||||
|
|
|
|||
|
|
@ -970,6 +970,14 @@ enum class RdnaOpcode : std::uint16_t {
|
|||
ImageStoreBy4,
|
||||
ImageStoreMipBy2,
|
||||
ImageStoreMipBy4,
|
||||
ImageLoadPck2,
|
||||
ImageLoadPck4,
|
||||
ImageLoadMipPck2,
|
||||
ImageLoadMipPck4,
|
||||
ImageStorePck2,
|
||||
ImageStorePck4,
|
||||
ImageStoreMipPck2,
|
||||
ImageStoreMipPck4,
|
||||
ImageMsaaLoad,
|
||||
ImageAtomicSwap,
|
||||
ImageAtomicAdd,
|
||||
|
|
|
|||
|
|
@ -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<const std
|
|||
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 pckN = opcode >= 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<const std
|
|||
const bool rayQuery64 = info.opcode == RdnaOpcode::ImageBvh64IntersectRay;
|
||||
const bool rayQuery = rayQuery64 || info.opcode == RdnaOpcode::ImageBvhIntersectRay;
|
||||
std::uint32_t components = rayQuery ? (a16 ? 8u : 11u) + (rayQuery64 ? 1u : 0u) : opcode == 0x0Eu ? 1u : coordinateCount(dimension);
|
||||
if (opcode == 1u || opcode == 4u || opcode == 5u || opcode == 9u || opcode == 0x0bu || (by && (opcode & 8u) != 0u)) {
|
||||
if (opcode == 1u || opcode == 4u || opcode == 5u || opcode == 9u || opcode == 0x0bu || (by && (opcode & 8u) != 0u) || pckNMip) {
|
||||
++components;
|
||||
}
|
||||
if (info.sample || info.gather) {
|
||||
|
|
|
|||
|
|
@ -831,6 +831,14 @@ bool IsImageOpcode(RdnaOpcode opcode) {
|
|||
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:
|
||||
case RdnaOpcode::ImageMsaaLoad:
|
||||
case RdnaOpcode::ImageAtomicSwap:
|
||||
case RdnaOpcode::ImageAtomicAdd:
|
||||
|
|
|
|||
|
|
@ -874,8 +874,9 @@ void EmitQueryLodOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) {
|
|||
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) {
|
||||
const bool packed = access.mem.imagePacked;
|
||||
const auto components = packed ? 1u : access.mem.dataDwords / elements;
|
||||
if (access.image.dimension != RdnaImageDimension::Dim2D || components == 0u || (packed ? access.mem.dataDwords != 1u : 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);
|
||||
|
|
@ -916,6 +917,18 @@ void EmitByReadOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) {
|
|||
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);
|
||||
if (packed) {
|
||||
const auto info = PackedTexelFormat(ctx, access);
|
||||
std::uint32_t texelBits = 0;
|
||||
for (std::uint32_t component = 0; component < info.componentCount; ++component) texelBits += info.componentBits[component];
|
||||
if (texelBits * elements > 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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue