From 56738aa4e12d4e62f9493f11ed1f0ed327e294f2 Mon Sep 17 00:00:00 2001 From: nmzik Date: Mon, 12 Oct 2026 23:51:22 +0200 Subject: [PATCH] shader: sample packed normalized images with one bilinear gather --- src/graphics/guest_gpu/gpu_format.cpp | 2 +- .../renderer/pipeline/descriptors.cpp | 3 +- .../backend/spirv/spirvEmitterImage.cpp | 117 ++++++++++++++---- .../recompiler/frontend/decode/ImageOps.cpp | 3 + .../frontend/decode/ShaderDecoder.h | 1 + .../ir/passes/ResourceMaterialization.cpp | 42 ++++++- tests/ShaderRecompilerComputeTests.cpp | 81 ++++++++++++ 7 files changed, 225 insertions(+), 24 deletions(-) diff --git a/src/graphics/guest_gpu/gpu_format.cpp b/src/graphics/guest_gpu/gpu_format.cpp index c92027a1b..f968991ea 100644 --- a/src/graphics/guest_gpu/gpu_format.cpp +++ b/src/graphics/guest_gpu/gpu_format.cpp @@ -41,7 +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_10UNorm, 4, 0, 0, true, 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}, diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index c70607fe5..f37f20b50 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -568,7 +568,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR const bool shader_conversion = surface_format.conversion_format != Prospero::BufferFormat::kInvalid; const bool sampled_numeric_class = - storage || resource.numeric_class == Prospero::SampledTextureNumericClass(format); + storage || resource.numeric_class == + Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format)); if (!storage && resource.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled && !sampled_numeric_class) { EXIT("sampled image numeric class mismatch: numeric=%u format=%u addr=0x%016" PRIx64 "\n", diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index d8ee47b91..a685a67b4 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -385,12 +385,20 @@ Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state, info.component_count > 4u); EXIT_IF(info.type != Format::ComponentType::Uscaled && ((info.type != Format::ComponentType::Uint && - !(info.type == Format::ComponentType::Unorm && - image.resource_class == IR::ImageResourceClass::Storage)) || !info.packed_bitfield || + info.type != Format::ComponentType::Unorm) || !info.packed_bitfield || info.byte_size != sizeof(uint32_t))); return info; } +uint32_t ExtractPackedImageComponent(EmitterState& state, const Format::BufferFormatInfo& info, + uint32_t packed, uint32_t type, uint32_t component) { + const auto result = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpBitFieldUExtract, type, result, packed, + ConstantU32(state, info.component_bit_offset[component]), + ConstantU32(state, info.component_bits[component])); + return result; +} + uint32_t ImageGatherSource(const EmitterState& state, const IR::MemoryInfo& mem) { const auto component = ImageGatherComponent(mem.dmask); const auto info = ImageConversionFormat(state, mem); @@ -414,13 +422,11 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint } uint32_t components[4] {}; for (uint32_t component = 0; component < info.component_count; component++) { - components[component] = ctx.state.builder.AllocateId(); if (info.packed_bitfield) { - ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, scalar_type, - components[component], packed, - ConstantU32(ctx.state, info.component_bit_offset[component]), - ConstantU32(ctx.state, info.component_bits[component])); + components[component] = + ExtractPackedImageComponent(ctx.state, info, packed, scalar_type, component); } else { + components[component] = ctx.state.builder.AllocateId(); ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type, components[component], texel, component); // UNorm backing preserves the guest's filtering; scaling precedes swizzle constants. @@ -480,21 +486,74 @@ uint32_t UnpackImageGather(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uin return Binary(ctx.state, spv::OpFMul, vector_type, gathered, scales); } - uint32_t values[4] {}; - for (uint32_t lane = 0; lane < 4u; lane++) { - const auto packed = ctx.state.builder.AllocateId(); - ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), packed, gathered, - lane); - values[lane] = ctx.state.builder.AllocateId(); - const auto physical = (selector - 4u) % info.component_count; - ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(ctx.state), values[lane], - packed, - ConstantU32(ctx.state, info.component_bit_offset[physical]), - ConstantU32(ctx.state, info.component_bits[physical])); + return ExtractPackedImageComponent(ctx.state, info, gathered, TypeU32Vector(ctx.state, 4), + (selector - 4u) % info.component_count); +} + +uint32_t SamplePackedUnormLz(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t coord) { + auto& state = ctx.state; + const auto& image = state.program.info.images[mem.resource]; + const auto info = ImageConversionFormat(state, mem); + const auto sampled = MakeSampledImage(state, mem.resource, + LoadSamplerDescriptor(state, mem.sampler)); + const auto gathered = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpImageGather, TypeU32Vector(state, 4), gathered, sampled, + coord, ConstantU32(state, 0u)); + state.builder.RequireCapability(spv::CapabilityImageQuery); + const auto size = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 2), size, + LoadImageDescriptor(state, mem.resource), ConstantU32(state, 0u)); + const auto size_f32 = Unary(state, spv::OpConvertUToF, TypeF32Vector(state, 2), size); + const auto half = ConstantF32Value(state, 0.5f); + const auto halves = state.builder.Constant(spv::OpConstantComposite, + TypeF32Vector(state, 2), half, half); + const auto fractions = state.builder.AllocateId(); + state.builder.AddFunction( + spv::OpExtInst, TypeF32Vector(state, 2), fractions, GlslStd450(state), GLSLstd450Fract, + Binary(state, spv::OpFSub, TypeF32Vector(state, 2), + Binary(state, spv::OpFMul, TypeF32Vector(state, 2), coord, size_f32), halves)); + uint32_t fraction[2]; + uint32_t complement[2]; + for (uint32_t axis = 0; axis < 2u; axis++) { + fraction[axis] = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), fraction[axis], + fractions, axis); + complement[axis] = Binary(state, spv::OpFSub, TypeF32(state), + ConstantF32Value(state, 1.0f), fraction[axis]); } - const auto result = ctx.state.builder.AllocateId(); - ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result, - values[0], values[1], values[2], values[3]); + // Gather order is (x0, y1), (x1, y1), (x1, y0), (x0, y0). + const auto weights = state.builder.AllocateId(); + state.builder.AddFunction( + spv::OpCompositeConstruct, TypeF32Vector(state, 4), weights, + Binary(state, spv::OpFMul, TypeF32(state), complement[0], fraction[1]), + Binary(state, spv::OpFMul, TypeF32(state), fraction[0], fraction[1]), + Binary(state, spv::OpFMul, TypeF32(state), fraction[0], complement[1]), + Binary(state, spv::OpFMul, TypeF32(state), complement[0], complement[1])); + uint32_t decoded[4] {}; + uint32_t selected[4] {ZeroF32(state), ZeroF32(state), ZeroF32(state), ZeroF32(state)}; + for (uint32_t component = 0; component < 4u; component++) { + if ((mem.dmask & (1u << component)) == 0u) continue; + const auto selector = (image.shader_swizzle >> (component * 3u)) & 7u; + if (selector == 1u) { + selected[component] = ConstantF32Value(state, 1.0f); + } else if (selector >= 4u) { + const auto physical = (selector - 4u) % info.component_count; + if (decoded[physical] == 0u) { + const auto texels = Unary( + state, spv::OpConvertUToF, TypeF32Vector(state, 4), + ExtractPackedImageComponent(state, info, gathered, + TypeU32Vector(state, 4), physical)); + decoded[physical] = Binary( + state, spv::OpFMul, TypeF32(state), + Binary(state, spv::OpDot, TypeF32(state), texels, weights), + ConstantF32Value(state, 1.0f / ((1u << info.component_bits[physical]) - 1u))); + } + selected[component] = decoded[physical]; + } + } + const auto result = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), result, + selected[0], selected[1], selected[2], selected[3]); return result; } @@ -700,6 +759,22 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { ctx.Define(inst, QueryDimensions(ctx, mem, *address)); return; } + if (image.resource_class == IR::ImageResourceClass::Sampled && + image.conversion_format == Prospero::BufferFormat::k11_11_10UNorm) { + if (op != IR::ValueOpcode::ImageSampleRaw || + image.dimension != ImageDimension::Dim2D || + mem.image_dimension != ImageDimension::Dim2D || mem.data_bits != 32u || + mem.image_sample_flags != Decoder::ImageSampleFlagLevelZero || + image.indirect_root != IR::ImageResource::NoIndirectImage || + !state.program.info.samplers.at(mem.sampler).indirect_resources.empty()) { + ctx.Fail(inst, "packed UNorm sampling requires a direct 2D sample_lz"); + return; + } + const auto coord = CoordF32(ctx, mem, *address, Layout(mem).coord, 2u); + ctx.Define(inst, ResultVector(ctx, SamplePackedUnormLz(ctx, mem, coord), + Prospero::TextureNumericClass::Float, false, mem)); + return; + } if (op == IR::ValueOpcode::ImageQueryLod) { state.builder.RequireCapability(spv::CapabilityImageQuery); const auto dimension = image.dimension; diff --git a/src/graphics/shader/recompiler/frontend/decode/ImageOps.cpp b/src/graphics/shader/recompiler/frontend/decode/ImageOps.cpp index 04313b2c3..53d8b4f70 100644 --- a/src/graphics/shader/recompiler/frontend/decode/ImageOps.cpp +++ b/src/graphics/shader/recompiler/frontend/decode/ImageOps.cpp @@ -351,6 +351,9 @@ void DecodeMimg(uint32_t pc, std::span code, uint32_t word_index if (a16) { inst.image_sample_flags |= ImageSampleFlagA16; } + if ((sample != nullptr || gather != nullptr) && (word0 & (1u << 12u)) != 0u) { + inst.image_sample_flags |= ImageSampleFlagUnnormalized; + } inst.image_dimension = dimension; inst.image_r128 = r128; inst.image_nsa_dwords = nsa_dwords; diff --git a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h index bceb52a10..43667d7c6 100644 --- a/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h +++ b/src/graphics/shader/recompiler/frontend/decode/ShaderDecoder.h @@ -762,6 +762,7 @@ enum ImageSampleFlag : uint32_t { ImageSampleFlagCd = 1u << 8u, ImageSampleFlagGatherHorizontal = 1u << 9u, ImageSampleFlagAdjust = 1u << 10u, + ImageSampleFlagUnnormalized = 1u << 11u, }; enum class ImageDimension : uint32_t { diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index f37f5ef4e..494ff8eed 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -105,6 +105,40 @@ Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) { : Prospero::BufferFormat::kInvalid; } +bool ValidatePackedUnormSampling(const ResourcePlan& program, const ResourceSnapshot& snapshot, + uint32_t image_index) { + ShaderTextureResource texture; + std::copy_n(snapshot.images[image_index].dwords.begin(), std::size(texture.fields), + texture.fields); + if (texture.Type() != Prospero::ImageType::kColor2D || + texture.BaseLevel() != texture.LastLevel() || texture.MinLod() != 0u || + texture.CornerSample()) { + return SpecializationFail("packed UNorm sampling requires a centered, single-mip 2D image"); + } + for (const auto& pair: program.info.sampled_pairs) { + if (pair.image != image_index) continue; + if (pair.sampler >= snapshot.samplers.size() || + !program.info.samplers[pair.sampler].indirect_resources.empty()) { + return SpecializationFail("packed UNorm sampling requires a direct sampler"); + } + ShaderSamplerResource sampler; + std::copy_n(snapshot.samplers[pair.sampler].dwords.begin(), std::size(sampler.fields), + sampler.fields); + const auto linear = static_cast(Prospero::SamplerFilter::kBilinear); + const auto clamp = static_cast(Prospero::SamplerClampMode::kClampLastTexel); + if (sampler.XyMagFilter() != linear || sampler.XyMinFilter() != linear || + sampler.ClampX() != clamp || sampler.ClampY() != clamp || + sampler.ForceUnormCoords() || sampler.MaxAnisoRatio() != 0u || + sampler.FilterMode() != 0u || sampler.MinLod() != 0u || sampler.ForceSrgb() || + sampler.TruncCoord() || (sampler.fields[0] & (1u << 19u)) != 0u) { // MC_COORD_TRUNC + return SpecializationFail(fmt::format( + "packed UNorm image {} requires normalized bilinear clamp sampling (sampler {})", + image_index, pair.sampler)); + } + } + return true; +} + enum class SamplerClass : uint8_t { Float, Integer }; SamplerClass ClassifySampler(const ImageResource& image) { @@ -653,7 +687,13 @@ 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( - storage ? Prospero::RemapTextureFormat(format) : format); + Prospero::RemapTextureFormat(format)); + if (!storage && format == Prospero::BufferFormat::k11_11_10UNorm) { + if (image.indirect_root != ImageResource::NoIndirectImage) { + return SpecializationFail("packed UNorm sampling requires a direct image"); + } + if (!ValidatePackedUnormSampling(program, snapshot, i)) return false; + } 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 4436723cd..706be88a9 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -34625,6 +34625,83 @@ TestCase ImageSamplePackedUintConvertsSampleAndGather() { return test; } +template TestCase ImageSamplePackedUnormBilinear() { + using O = ShaderOpcode; + namespace D = ShaderRecompiler::Decoder; + const std::vector unnormalized = {EncodeMimg0(0x27, 0x7) | (1u << 12u), + EncodeMimg1(0, 20, 0, 2)}; + D::Instruction decoded; + D::DecodeInstruction(unnormalized, 0, decoded); + Require("ImageSamplePackedUnormBilinear", "coordinate mode", + decoded.image_sample_flags == + (D::ImageSampleFlagLevelZero | D::ImageSampleFlagUnnormalized), + "unnormalized instruction coordinates lost their sampling restriction"); + constexpr u32 channels[4][3] = {{0, 0, 0}, {2047, 0, 512}, + {0, 2047, 1023}, {2047, 2047, 256}}; + constexpr float coordinates[][2] = {{0.5f, 0.5f}, {0.375f, 0.625f}, + {-0.25f, 1.25f}, {0.25f, 0.25f}, + {0.75f, 0.25f}, {0.0f, 0.5f}}; + TestCase test; + test.name = sparse ? "ImageSamplePackedUnormBilinearSparse" : "ImageSamplePackedUnormBilinear"; + test.image_width = test.image_height = 2; + test.sampled_image_format = vk::Format::eR32Uint; + test.sampled_image_dwords_per_pixel = 1; + for (const auto &pixel : channels) { + test.sampled_image_rgba.push_back(pixel[0] | (pixel[1] << 11u) | (pixel[2] << 22u)); + } + test.user_data = MakeSampledTextureData(Prospero::BufferFormat::k11_11_10UNorm); + test.user_data[1] |= 1u << 30u; + test.user_data[2] = 1u << 14u; + test.user_data[8] = 0x92u; + test.user_data[9] = 0x00fff000u; + test.user_data[10] = 0x0a500000u; // Captured linear guest sampler; gather uses integer texels. + test.has_user_data = true; + test.image_descriptor_swizzle = DstSel(6, 4, 5, 1); + test.expected_force_point_sampler = true; + test.expected_float_tolerance = 0.000005f; + auto &code = test.code; + code.push_back(EncodeSop1(0x04, 12, 126)); + for (const auto &uv : coordinates) { + AppendVMovLiteral(&code, 20, std::bit_cast(uv[0])); + AppendVMovLiteral(&code, 21, std::bit_cast(uv[1])); + code.push_back(EncodeMimg0(0x27, sparse ? 0x5 : 0xf)); + code.push_back(EncodeMimg1(0, 20, 0, 2)); + const float x = uv[0] * 2.0f - 0.5f, y = uv[1] * 2.0f - 0.5f; + const int ix = static_cast(std::floor(x)), iy = static_cast(std::floor(y)); + const float fx = x - std::floor(x), fy = y - std::floor(y); + float rgba[4] = {0, 0, 0, 1}; + for (u32 channel = 0; channel < 3; ++channel) { + for (int dy = 0; dy < 2; ++dy) for (int dx = 0; dx < 2; ++dx) { + const auto pixel = std::clamp(iy + dy, 0, 1) * 2 + std::clamp(ix + dx, 0, 1); + rgba[channel] += channels[pixel][channel] / (channel == 2 ? 1023.0f : 2047.0f) * + (dx ? fx : 1.0f - fx) * (dy ? fy : 1.0f - fy); + } + } + constexpr u32 swizzle[4] = {2, 0, 1, 3}; + u32 reg = 0; + for (u32 component = 0; component < 4; ++component) { + if (sparse && component != 0 && component != 2) continue; + AppendStoreVgpr(&code, reg++, test.expected.size()); + test.expected.push_back(std::bit_cast(rgba[swizzle[component]])); + } + } + AppendVMovLiteral(&code, 0, 0x3f400000u); + code.push_back(EncodeSop1(0x04, 126, InlineU32(0))); + code.push_back(EncodeMimg0(0x27, 0x1)); + code.push_back(EncodeMimg1(0, 20, 0, 2)); + code.push_back(EncodeSop1(0x04, 126, 12)); + AppendStoreVgpr(&code, 0, test.expected.size()); + test.expected.push_back(0x3f400000u); + AppendEnd(&code); + test.user_data[50] = test.expected.size() * sizeof(u32); + test.user_data[51] = 3u << 28u; + test.opcodes = {O::V_MOV_B32, O::S_MOV_B64, O::IMAGE_SAMPLE, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + test.required_spirv = {"OpImageGather", "OpBitFieldUExtract", "OpConvertUToF"}; + test.forbidden_spirv = {"OpImageFetch", "OpImageSample", "OpImageRead", "OpImageWrite"}; + return test; +} + template TestCase ImageSamplePacked8() { using O = ShaderOpcode; TestCase test; @@ -38570,6 +38647,8 @@ std::vector MakeCases() { AddCase(ImageLoadR32SintUsesSignedSampledImage); AddCase(ImageLoadPackedUintUnpacksAndSwizzles); AddCase(ImageSamplePackedUintConvertsSampleAndGather); + AddCase(ImageSamplePackedUnormBilinear); + AddCase(ImageSamplePackedUnormBilinear); AddCase(ImageSamplePacked8); AddCase(ImageSamplePacked8); AddCase(ImageSamplePacked8); @@ -44197,6 +44276,8 @@ int main(int argc, char **argv) { vulkan.CheckBgra16Readback(); RunCase(&vulkan, ImageLoadPackedUintUnpacksAndSwizzles()); RunCase(&vulkan, ImageSamplePackedUintConvertsSampleAndGather()); + RunCase(&vulkan, ImageSamplePackedUnormBilinear()); + RunCase(&vulkan, ImageSamplePackedUnormBilinear()); RunCase(&vulkan, ImageStorePackedUintSaturatesChannels()); RunCase(&vulkan, ImageStorePackedUintHonorsSparseDmask()); RunCase(&vulkan, ImageStorePackedUnorm());