From c11254a99630aa743e53ec205340a450f48b81cc Mon Sep 17 00:00:00 2001 From: nmzik Date: Mon, 12 Oct 2026 23:38:45 +0200 Subject: [PATCH] shader: share format conversion for packed normalized image stores --- src/graphics/guest_gpu/gpu_format.cpp | 2 + .../renderer/pipeline/descriptors.cpp | 3 +- .../backend/spirv/spirvEmitterImage.cpp | 80 +++++++++---------- .../backend/spirv/spirvEmitterInternal.h | 3 + .../backend/spirv/spirvEmitterMemory.cpp | 49 +----------- .../spirv/spirvEmitterMemoryHelpers.cpp | 47 ++++++++++- .../ir/passes/ResourceMaterialization.cpp | 3 +- tests/ShaderRecompilerComputeTests.cpp | 57 ++++++++++++- 8 files changed, 149 insertions(+), 95 deletions(-) diff --git a/src/graphics/guest_gpu/gpu_format.cpp b/src/graphics/guest_gpu/gpu_format.cpp index 4413480e..c92027a1 100644 --- a/src/graphics/guest_gpu/gpu_format.cpp +++ b/src/graphics/guest_gpu/gpu_format.cpp @@ -41,6 +41,7 @@ constexpr FormatInfo kFormatInfo[] = { {BufferFormat::k16_16UInt, 4, 0, 4, true, true}, {BufferFormat::k16_16SInt, 4, 0, 4, true, false, true}, {BufferFormat::k16_16Float, 4, 0, 4, true, false}, + {BufferFormat::k11_11_10UNorm, 4, 0, 0, false, false}, {BufferFormat::k11_11_10UInt, 4, 0, 4, true, true}, {BufferFormat::k11_11_10Float, 4, 0, 4, true, false}, {BufferFormat::k10_11_11Float, 4, 0, 4, true, false}, @@ -237,6 +238,7 @@ BufferFormat RemapTextureFormat(BufferFormat format) { switch (format) { case BufferFormat::k8UScaled: return BufferFormat::k8UNorm; case BufferFormat::k8_8UScaled: return BufferFormat::k8_8UNorm; + case BufferFormat::k11_11_10UNorm: case BufferFormat::k11_11_10UInt: return BufferFormat::k32UInt; default: return format; } diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index 92969761..c70607fe 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -311,7 +311,8 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource, const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource && resource.written && (resource.atomic ? !resource.atomic64 : !resource.read); - const auto numeric_class = Prospero::SampledTextureNumericClass(format); + const auto numeric_class = + Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format)); const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource && resource.atomic && !resource.atomic64; const bool format_ok = diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index 6cc4ef92..d8ee47b9 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -377,13 +377,16 @@ uint32_t InverseSwizzle(uint32_t swizzle, uint32_t component) { Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state, const IR::MemoryInfo& mem) { - const auto format = state.program.info.images[mem.resource].conversion_format; + const auto& image = state.program.info.images[mem.resource]; + const auto format = image.conversion_format; if (format == Prospero::BufferFormat::kInvalid) return {}; const auto info = Format::GetFormatInfo(format); EXIT_IF(Prospero::RemapTextureFormat(format) == format || info.component_count == 0u || info.component_count > 4u); EXIT_IF(info.type != Format::ComponentType::Uscaled && - (info.type != Format::ComponentType::Uint || !info.packed_bitfield || + ((info.type != Format::ComponentType::Uint && + !(info.type == Format::ComponentType::Unorm && + image.resource_class == IR::ImageResourceClass::Storage)) || !info.packed_bitfield || info.byte_size != sizeof(uint32_t))); return info; } @@ -537,47 +540,22 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo& return result; } -uint32_t PackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t texel) { - const auto info = ImageConversionFormat(ctx.state, mem); - if (info.format == Prospero::BufferFormat::kInvalid) return texel; - EXIT_IF(info.type != Format::ComponentType::Uint || !info.packed_bitfield); - - auto packed = ConstantU32(ctx.state, 0u); - for (uint32_t component = 0; component < info.component_count; component++) { - const auto value = ctx.state.builder.AllocateId(); - ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), value, texel, - component); - const auto maximum = - ConstantU32(ctx.state, info.component_bits[component] == 32u - ? UINT32_MAX - : (1u << info.component_bits[component]) - 1u); - const auto within = - Binary(ctx.state, spv::OpULessThan, TypeBool(ctx.state), value, maximum); - const auto clamped = ctx.state.builder.AllocateId(); - ctx.state.builder.AddFunction(spv::OpSelect, TypeU32(ctx.state), clamped, within, value, - maximum); - const auto shifted = - info.component_bit_offset[component] == 0u - ? clamped - : Binary(ctx.state, spv::OpShiftLeftLogical, TypeU32(ctx.state), clamped, - ConstantU32(ctx.state, info.component_bit_offset[component])); - packed = Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), packed, shifted); - } - const auto zero = ConstantU32(ctx.state, 0u); - const auto result = ctx.state.builder.AllocateId(); - ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result, - packed, zero, zero, zero); - return result; -} - uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t data, bool integer) { const auto swizzle = ctx.state.program.info.images[mem.resource].shader_swizzle; - uint32_t values[4] {}; + const auto info = ImageConversionFormat(ctx.state, mem); + const bool packed = info.format != Prospero::BufferFormat::kInvalid; + EXIT_IF(packed && (!info.packed_bitfield || + (info.type != Format::ComponentType::Uint && + info.type != Format::ComponentType::Unorm))); + const auto zero = ConstantU32(ctx.state, 0); + uint32_t values[4] {zero, zero, zero, zero}; + auto packed_value = zero; const auto dmask = mem.dmask != 0u ? mem.dmask : 1u; - for (uint32_t component = 0; component < 4u; component++) { + for (uint32_t component = 0; component < (packed ? info.component_count : 4u); component++) { const auto source = InverseSwizzle(swizzle, component); - uint32_t raw = ConstantU32(ctx.state, 0); - if (source < 4u && ((dmask >> source) & 1u) != 0u) { + const bool selected = source < 4u && ((dmask >> source) & 1u) != 0u; + uint32_t raw = zero; + if (selected) { const auto packed_index = DmaskComponentIndex(dmask, source); raw = ctx.state.builder.AllocateId(); ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), raw, data, @@ -587,21 +565,39 @@ uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t d raw = Binary(ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), raw, ConstantU32(ctx.state, 16u)); } - raw = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), raw, - ConstantU32(ctx.state, 0xffffu)); + if (!packed) { + raw = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), raw, + ConstantU32(ctx.state, 0xffffu)); + } } } + if (packed) { + if (!selected) continue; + auto value = EncodeFormattedStoreComponent(ctx.state, info, component, raw, mem.data_bits); + if (info.type == Format::ComponentType::Uint) { + value = EmitUMin32(ctx.state, value, + ConstantU32(ctx.state, (1u << info.component_bits[component]) - 1u)); + } + if (info.component_bit_offset[component] != 0u) { + value = Binary(ctx.state, spv::OpShiftLeftLogical, TypeU32(ctx.state), value, + ConstantU32(ctx.state, info.component_bit_offset[component])); + } + packed_value = packed_value == zero ? value + : Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), packed_value, value); + continue; + } values[component] = integer ? raw : mem.data_bits == 16u ? EmitF16BitsToF32(ctx.state, raw) : Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), raw); } + if (packed) values[0] = packed_value; const auto texel = ctx.state.builder.AllocateId(); ctx.state.builder.AddFunction(spv::OpCompositeConstruct, integer ? TypeU32Vector(ctx.state, 4) : TypeF32Vector(ctx.state, 4), texel, values[0], values[1], values[2], values[3]); - return PackImageTexel(ctx, mem, texel); + return texel; } template diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index e3611f0d..3b77345d 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -446,6 +446,9 @@ uint32_t EmitUFloatToF32Bits(EmitterState& state, uint32_t raw, uint32_t bits); uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferFormatInfo& info, uint32_t component, uint32_t raw); +uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info, + uint32_t component, uint32_t data, uint32_t data_bits); + spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode); uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t pointer, diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp index f1839d91..3b504312 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp @@ -767,53 +767,6 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me }); } -uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx, - const Format::BufferFormatInfo& info, - uint32_t component, uint32_t data, uint32_t data_bits) { - auto& state = ctx.state; - const auto bits = info.component_bits[component]; - if (data_bits == 16u) { - if (info.type == Format::ComponentType::Uint) { - data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16)); - } else if (info.type == Format::ComponentType::Sint) { - data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16)); - } else { - data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data)); - } - } - const bool normalized = info.type == Format::ComponentType::Unorm || - info.type == Format::ComponentType::Snorm; - const bool scaled = info.type == Format::ComponentType::Uscaled || - info.type == Format::ComponentType::Sscaled; - if (normalized || scaled) { - // Two-bit packed channels are unsigned even in signed formats. - const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm || - info.type == Format::ComponentType::Sscaled); - const auto max_value = static_cast((1u << (bits - (is_signed ? 1u : 0u))) - 1u); - const auto lower = normalized ? (is_signed ? -1.0f : 0.0f) - : (is_signed ? -max_value - 1.0f : 0.0f); - const auto upper = normalized ? 1.0f : max_value; - auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan, - ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data)); - value = EmitGlsl( - state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper)); - if (normalized) { - value = EmitFPRoundEven32( - state, EmitFPMul32(state, value, ConstantF32Value(state, max_value))); - } - // Float-to-integer conversion truncates scaled values toward zero. - value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU, - is_signed ? TypeI32(state) : TypeU32(state), value); - return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value; - } - if (bits == 16u && info.type == Format::ComponentType::Float) { - const auto pair = EmitCompositeConstructF32x2( - state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f)); - return EmitPackHalf2x16(state, pair); - } - return data; -} - void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem, const MemoryResourceAccess& resource, const Format::BufferFormatInfo& info, uint32_t data, @@ -832,7 +785,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst, value, data, component); } } - value = EncodeFormattedStoreComponent(ctx, info, component, value, mem.data_bits); + value = EncodeFormattedStoreComponent(ctx.state, info, component, value, mem.data_bits); const auto bits = info.component_bits[component]; if (info.packed_bitfield) { packed = EmitBitFieldInsert( diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp index 3e142eea..a073bd24 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp @@ -1,4 +1,4 @@ -#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h" +#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h" namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter { @@ -417,6 +417,51 @@ uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferForma } } +uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info, + uint32_t component, uint32_t data, uint32_t data_bits) { + const auto bits = info.component_bits[component]; + if (data_bits == 16u) { + if (info.type == Format::ComponentType::Uint) { + data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16)); + } else if (info.type == Format::ComponentType::Sint) { + data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16)); + } else { + data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data)); + } + } + const bool normalized = info.type == Format::ComponentType::Unorm || + info.type == Format::ComponentType::Snorm; + const bool scaled = info.type == Format::ComponentType::Uscaled || + info.type == Format::ComponentType::Sscaled; + if (normalized || scaled) { + // Two-bit packed channels are unsigned even in signed formats. + const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm || + info.type == Format::ComponentType::Sscaled); + const auto max_value = static_cast((1u << (bits - (is_signed ? 1u : 0u))) - 1u); + const auto lower = normalized ? (is_signed ? -1.0f : 0.0f) + : (is_signed ? -max_value - 1.0f : 0.0f); + const auto upper = normalized ? 1.0f : max_value; + auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan, + ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data)); + value = EmitGlsl( + state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper)); + if (normalized) { + value = EmitFPRoundEven32( + state, EmitFPMul32(state, value, ConstantF32Value(state, max_value))); + } + // Float-to-integer conversion truncates scaled values toward zero. + value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU, + is_signed ? TypeI32(state) : TypeU32(state), value); + return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value; + } + if (bits == 16u && info.type == Format::ComponentType::Float) { + const auto pair = EmitCompositeConstructF32x2( + state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f)); + return EmitPackHalf2x16(state, pair); + } + return data; +} + spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) { switch (opcode) { case IR::ValueOpcode::ImageAtomicCompareSwap32: diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index c05d9cf4..f37f5ef4 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -652,7 +652,8 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna } const bool raw_sint_storage = storage && format == Prospero::BufferFormat::k32SInt && base.written && (base.atomic ? !base.atomic64 : !base.read); - image.numeric_class = Prospero::SampledTextureNumericClass(format); + image.numeric_class = Prospero::SampledTextureNumericClass( + storage ? Prospero::RemapTextureFormat(format) : format); if (storage) { if ((!raw_sint_storage && image.numeric_class == Prospero::TextureNumericClass::Sint) || image.numeric_class == Prospero::TextureNumericClass::Unsupported) { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 25206b80..4436723c 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -36983,8 +36983,7 @@ TestCase ImageStorePackedUintSaturatesChannels() { test.storage_image_r32ui = std::vector(16, 0); test.expected_storage_image_r32ui = std::move(expected_image); test.required_spirv = {"OpCapability StorageImageWriteWithoutFormat", - StorageUint2DImageBindingName(false), "OpULessThan", - "OpSelect"}; + StorageUint2DImageBindingName(false), "UMin"}; test.forbidden_spirv = {"R32ui"}; return test; } @@ -37021,6 +37020,54 @@ TestCase ImageStorePackedUintHonorsSparseDmask() { return test; } +template TestCase ImageStorePackedUnorm() { + using O = ShaderOpcode; + // Float inputs and exact packed UNorm components, including ties to even. + constexpr float values[][3] = { + {-1.0f, 0.5f, 2.0f}, {0.0f, 1.0f, 0.5f}, + {std::bit_cast(0x7fc00000u), std::bit_cast(0x7f800000u), + std::bit_cast(0xff800000u)}, + {0.25f, 0.75f, 0.25f}, {2.5f / 2047.0f, 3.5f / 2047.0f, 2.5f / 1023.0f}}; + constexpr u32 converted[][3] = { + {0, 1024, 1023}, {0, 2047, 512}, {0, 2047, 0}, {512, 1535, 256}, {2, 4, 2}}; + TestCase test; + test.name = sparse ? "ImageStorePackedUnormSparseDmask" : "ImageStorePackedUnormBoundaries"; + test.user_data = MakeStorageTextureData(Prospero::BufferFormat::k11_11_10UNorm); + test.has_user_data = true; + test.image_descriptor_swizzle = DstSel(4, 5, 6, 0); + test.storage_image_r32ui.assign(16, 0xabcdef01u); + test.expected_storage_image_r32ui = test.storage_image_r32ui; + auto &code = test.code; + code.push_back(EncodeSop1(0x04, 12, 126)); + for (u32 i = 0; i < std::size(values); ++i) { + AppendVMovU32(&code, 20, i & 3u); + AppendVMovU32(&code, 21, i >> 2u); + AppendVMovLiteral(&code, 0, std::bit_cast(values[i][0])); + if constexpr (sparse) { + AppendVMovLiteral(&code, 1, std::bit_cast(values[i][2])); + } else { + AppendVMovLiteral(&code, 1, std::bit_cast(values[i][1])); + AppendVMovLiteral(&code, 2, std::bit_cast(values[i][2])); + } + code.push_back(EncodeMimg0(0x08, sparse ? 0x5 : 0x7)); + code.push_back(EncodeMimg1(0, 20)); + test.expected_storage_image_r32ui[i] = converted[i][0] | + (sparse ? 0u : converted[i][1] << 11u) | (converted[i][2] << 22u); + } + AppendVMovU32(&code, 20, 3); + AppendVMovU32(&code, 21, 3); + code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); + code.push_back(EncodeMimg0(0x08, 0x7)); + code.push_back(EncodeMimg1(0, 20)); + code.push_back(EncodeSop1(0x04, 126, 12)); + AppendEnd(&code); + test.opcodes = {O::V_MOV_B32, O::S_MOV_B64, O::IMAGE_STORE, O::S_ENDPGM}; + test.required_spirv = {"OpImageWrite", "RoundEven", "OpConvertFToU", + StorageUint2DImageBindingName(false)}; + test.forbidden_spirv = {"OpImageRead", "OpAtomic", "OpTypeSampler", "R32ui"}; + return test; +} + TestCase ComputeTgSizeSgprUsesWaveMetadata() { using O = ShaderOpcode; @@ -38572,6 +38619,8 @@ std::vector MakeCases() { AddCase(ImageStoreR32UintUsesFormatlessStorageImage); AddCase(ImageStorePackedUintSaturatesChannels); AddCase(ImageStorePackedUintHonorsSparseDmask); + AddCase(ImageStorePackedUnorm); + AddCase(ImageStorePackedUnorm); AddCase(ComputeTgSizeSgprUsesWaveMetadata); AddCase(ImageStoreAndAtomicShareTypedBinding); AddCase(ImageAtomicCompareSwapGlcAndExec); @@ -44148,6 +44197,10 @@ int main(int argc, char **argv) { vulkan.CheckBgra16Readback(); RunCase(&vulkan, ImageLoadPackedUintUnpacksAndSwizzles()); RunCase(&vulkan, ImageSamplePackedUintConvertsSampleAndGather()); + RunCase(&vulkan, ImageStorePackedUintSaturatesChannels()); + RunCase(&vulkan, ImageStorePackedUintHonorsSparseDmask()); + RunCase(&vulkan, ImageStorePackedUnorm()); + RunCase(&vulkan, ImageStorePackedUnorm()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--system-dialog-only") == 0) {