shader: share format conversion for packed normalized image stores

This commit is contained in:
nmzik 2026-10-12 23:38:45 +02:00
parent aec8c88755
commit c11254a996
8 changed files with 149 additions and 95 deletions

View File

@ -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;
}

View File

@ -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 =

View File

@ -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 <typename Emit>

View File

@ -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,

View File

@ -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<float>((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<GLSLstd450FClamp, IR::Type::F32>(
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(

View File

@ -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<float>((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<GLSLstd450FClamp, IR::Type::F32>(
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:

View File

@ -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) {

View File

@ -36983,8 +36983,7 @@ TestCase ImageStorePackedUintSaturatesChannels() {
test.storage_image_r32ui = std::vector<u32>(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 <bool sparse> 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<float>(0x7fc00000u), std::bit_cast<float>(0x7f800000u),
std::bit_cast<float>(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<u32>(values[i][0]));
if constexpr (sparse) {
AppendVMovLiteral(&code, 1, std::bit_cast<u32>(values[i][2]));
} else {
AppendVMovLiteral(&code, 1, std::bit_cast<u32>(values[i][1]));
AppendVMovLiteral(&code, 2, std::bit_cast<u32>(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<TestCase> MakeCases() {
AddCase(ImageStoreR32UintUsesFormatlessStorageImage);
AddCase(ImageStorePackedUintSaturatesChannels);
AddCase(ImageStorePackedUintHonorsSparseDmask);
AddCase(ImageStorePackedUnorm<false>);
AddCase(ImageStorePackedUnorm<true>);
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<false>());
RunCase(&vulkan, ImageStorePackedUnorm<true>());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--system-dialog-only") == 0) {