diff --git a/src/graphics/guest_gpu/gpu_format.cpp b/src/graphics/guest_gpu/gpu_format.cpp index 4fdaaac8..8c352797 100644 --- a/src/graphics/guest_gpu/gpu_format.cpp +++ b/src/graphics/guest_gpu/gpu_format.cpp @@ -20,6 +20,7 @@ struct FormatInfo { constexpr FormatInfo kFormatInfo[] = { {BufferFormat::k8UNorm, 1, 0, 1, true, false}, + {BufferFormat::k8UScaled, 1, 0, 0, true, false}, {BufferFormat::k8SNorm, 0, 0, 1, false, false}, {BufferFormat::k8UInt, 1, 0, 1, true, true}, {BufferFormat::k16UNorm, 2, 0, 2, true, false}, @@ -28,6 +29,7 @@ constexpr FormatInfo kFormatInfo[] = { {BufferFormat::k16SInt, 2, 0, 2, true, false, true}, {BufferFormat::k16Float, 2, 0, 2, true, false}, {BufferFormat::k8_8UNorm, 2, 0, 2, true, false}, + {BufferFormat::k8_8UScaled, 2, 0, 0, true, false}, {BufferFormat::k8_8SNorm, 2, 0, 2, true, false}, {BufferFormat::k8_8UInt, 2, 0, 2, true, true}, {BufferFormat::k8_8SInt, 2, 0, 2, true, false, true}, @@ -231,7 +233,12 @@ TextureNumericClass SampledTextureNumericClass(BufferFormat format) { } BufferFormat RemapTextureFormat(BufferFormat format) { - return format == BufferFormat::k11_11_10UInt ? BufferFormat::k32UInt : format; + switch (format) { + case BufferFormat::k8UScaled: return BufferFormat::k8UNorm; + case BufferFormat::k8_8UScaled: return BufferFormat::k8_8UNorm; + case BufferFormat::k11_11_10UInt: return BufferFormat::k32UInt; + default: return format; + } } } // namespace Libs::Graphics::Prospero diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index b0a78f83..7064923f 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -391,28 +391,50 @@ Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state, const auto format = state.program.info.images[mem.resource].conversion_format; if (format == Prospero::BufferFormat::kInvalid) return {}; const auto info = Format::GetFormatInfo(format); - EXIT_IF(Prospero::SampledTextureNumericClass(format) != Prospero::TextureNumericClass::Uint || - Prospero::RemapTextureFormat(format) == format || - info.type != Format::ComponentType::Uint || !info.packed_bitfield || - info.byte_size != sizeof(uint32_t) || info.component_count == 0u || + 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.byte_size != sizeof(uint32_t))); return info; } +uint32_t ImageGatherSource(const EmitterState& state, const IR::MemoryInfo& mem) { + const auto component = ImageGatherComponent(mem.dmask); + const auto info = ImageConversionFormat(state, mem); + if (info.format == Prospero::BufferFormat::kInvalid) return component; + if (info.packed_bitfield) return 0u; + const auto selector = + (state.program.info.images[mem.resource].shader_swizzle >> (component * 3u)) & 7u; + return selector >= 4u ? (selector - 4u) % info.component_count : 0u; +} + uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t texel) { const auto info = ImageConversionFormat(ctx.state, mem); if (info.format == Prospero::BufferFormat::kInvalid) return texel; - const auto packed = ctx.state.builder.AllocateId(); - ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), packed, texel, 0u); - uint32_t components[4] = {ConstantU32(ctx.state, 0), ConstantU32(ctx.state, 0), - ConstantU32(ctx.state, 0), ConstantU32(ctx.state, 0)}; + const auto numeric_class = ctx.state.program.info.images[mem.resource].numeric_class; + const auto scalar_type = ImageScalarType(ctx.state, numeric_class); + uint32_t packed = 0; + if (info.packed_bitfield) { + packed = ctx.state.builder.AllocateId(); + ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type, packed, texel, 0u); + } + uint32_t components[4] {}; for (uint32_t component = 0; component < info.component_count; component++) { components[component] = ctx.state.builder.AllocateId(); - ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(ctx.state), - components[component], packed, - ConstantU32(ctx.state, info.component_bit_offset[component]), - ConstantU32(ctx.state, info.component_bits[component])); + 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])); + } else { + ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type, + components[component], texel, component); + // UNorm backing preserves the guest's filtering; scaling precedes swizzle constants. + components[component] = Binary(ctx.state, spv::OpFMul, scalar_type, components[component], + ConstantF32Value(ctx.state, 255.0f)); + } } for (uint32_t component = info.component_count; component < 4u; component++) { components[component] = components[component % info.component_count]; @@ -423,16 +445,19 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint for (uint32_t component = 0; component < 4u; component++) { const auto selector = (swizzle >> (component * 3u)) & 7u; if (selector == 1u) { - selected[component] = ConstantU32(ctx.state, 1u); + selected[component] = info.packed_bitfield ? ConstantU32(ctx.state, 1u) + : ConstantF32Value(ctx.state, 1.0f); } else if (selector >= 4u) { selected[component] = components[selector - 4u]; } else { - selected[component] = ConstantU32(ctx.state, 0u); + selected[component] = info.packed_bitfield ? ConstantU32(ctx.state, 0u) + : ZeroF32(ctx.state); } } const auto result = ctx.state.builder.AllocateId(); - ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result, - selected[0], selected[1], selected[2], selected[3]); + ctx.state.builder.AddFunction(spv::OpCompositeConstruct, + ImageVectorType(ctx.state, numeric_class, 4), result, selected[0], + selected[1], selected[2], selected[3]); return result; } @@ -443,10 +468,20 @@ uint32_t UnpackImageGather(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uin const auto component = ImageGatherComponent(mem.dmask); const auto selector = (ctx.state.program.info.images[mem.resource].shader_swizzle >> (component * 3u)) & 7u; + const auto numeric_class = ctx.state.program.info.images[mem.resource].numeric_class; + const auto vector_type = ImageVectorType(ctx.state, numeric_class, 4); if (selector < 4u) { - const auto value = ConstantU32(ctx.state, selector == 1u ? 1u : 0u); - return ctx.state.builder.Constant(spv::OpConstantComposite, TypeU32Vector(ctx.state, 4), - value, value, value, value); + const auto value = info.packed_bitfield + ? ConstantU32(ctx.state, selector == 1u ? 1u : 0u) + : ConstantF32Value(ctx.state, selector == 1u ? 1.0f : 0.0f); + return ctx.state.builder.Constant(spv::OpConstantComposite, vector_type, value, value, + value, value); + } + if (!info.packed_bitfield) { + const auto scale = ConstantF32Value(ctx.state, 255.0f); + const auto scales = ctx.state.builder.Constant(spv::OpConstantComposite, vector_type, + scale, scale, scale, scale); + return Binary(ctx.state, spv::OpFMul, vector_type, gathered, scales); } uint32_t values[4] {}; @@ -488,10 +523,7 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo& LoadSamplerDescriptor(state, mem.sampler)); const auto vector_type = ImageVectorType(state, numeric_class, 4); const auto scalar_type = ImageScalarType(state, numeric_class); - const auto component = - ImageConversionFormat(state, mem).format == Prospero::BufferFormat::kInvalid - ? ImageGatherComponent(mem.dmask) - : 0u; + const auto component = ImageGatherSource(state, mem); uint32_t values[2] {}; for (uint32_t index = 0; index < 2u; index++) { const auto sample_coord = @@ -515,6 +547,7 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo& 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++) { @@ -669,7 +702,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare); if (dref && state.program.info.images[mem.resource].conversion_format != Prospero::BufferFormat::kInvalid) { - ctx.Fail(inst, "uses depth comparison with a packed integer image"); + ctx.Fail(inst, "uses depth comparison with a converted image"); return; } if (op == IR::ValueOpcode::ImageGatherRaw) { @@ -700,9 +733,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { ? AddressF32(ctx, mem, *address, layout.dref) : ZeroF32(state); } else { - component_or_dref = ConstantU32( - state, ImageConversionFormat(state, mem).format == Prospero::BufferFormat::kInvalid - ? ImageGatherComponent(mem.dmask) : 0u); + component_or_dref = ConstantU32(state, ImageGatherSource(state, mem)); } uint32_t operand_mask = 0; uint32_t offset = 0; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index e55afcf8..a7b919c1 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -111,7 +111,8 @@ enum class SamplerClass : uint8_t { Float, Integer, PointInteger }; SamplerClass ClassifySampler(const ImageResource& image) { if (image.numeric_class == Prospero::TextureNumericClass::Sint || - image.conversion_format != Prospero::BufferFormat::kInvalid) { + (image.numeric_class == Prospero::TextureNumericClass::Uint && + image.conversion_format != Prospero::BufferFormat::kInvalid)) { return SamplerClass::PointInteger; } return image.numeric_class == Prospero::TextureNumericClass::Uint ? SamplerClass::Integer diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 1db65189..6b288fa5 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -1283,6 +1283,7 @@ struct TestCase { std::vector sampled_image_rgba; std::vector> sampled_image_rgba_mips; vk::Format sampled_image_format = vk::Format::eR32G32B32A32Sfloat; + vk::Filter sampler_filter = vk::Filter::eNearest; u32 sampled_image_dwords_per_pixel = 4; vk::ImageType sampled_image_type = vk::ImageType::e2D; vk::ImageViewType sampled_image_view_type = vk::ImageViewType::e2D; @@ -1316,6 +1317,7 @@ struct TestCase { std::vector bda_mappings; bool expand_shader_data_storage = false; bool expected_force_point_sampler = false; + float expected_float_tolerance = 0.0f; std::vector gds_initial; std::vector expected_gds; std::vector> decoded_counts; @@ -12981,7 +12983,7 @@ public: copied_depth_sample.layout = copy_sample_image.backing.state.layout; scheduler.Finish(); auto copy_sample_output = CreateStorageBuffer(name, {}, 2); - const auto copy_sampler = CreateNearestSampler(name); + const auto copy_sampler = CreateSampler(name); Dispatch(copy_sample_test, copy_sample_program, copy_sample_output, nullptr, &copied_depth_sample, nullptr, nullptr, copy_sampler); Require( @@ -14326,11 +14328,12 @@ public: return ret; } - vk::Sampler CreateNearestSampler(const char *shader_name, u32 mip_levels = 1) { + vk::Sampler CreateSampler(const char *shader_name, u32 mip_levels = 1, + vk::Filter filter = vk::Filter::eNearest) { vk::SamplerCreateInfo sampler_info{}; sampler_info.sType = vk::StructureType::eSamplerCreateInfo; - sampler_info.magFilter = vk::Filter::eNearest; - sampler_info.minFilter = vk::Filter::eNearest; + sampler_info.magFilter = filter; + sampler_info.minFilter = filter; sampler_info.mipmapMode = vk::SamplerMipmapMode::eNearest; sampler_info.addressModeU = vk::SamplerAddressMode::eClampToEdge; sampler_info.addressModeV = vk::SamplerAddressMode::eClampToEdge; @@ -17688,6 +17691,15 @@ void CompareWords(const TestCase &test, const char *stage, if (actual == expected) { return; } + if (test.expected_float_tolerance > 0.0f && + std::equal(expected.begin(), expected.end(), actual.begin(), actual.end(), + [&](u32 expected_word, u32 actual_word) { + return std::abs(std::bit_cast(expected_word) - + std::bit_cast(actual_word)) <= + test.expected_float_tolerance; + })) { + return; + } std::ostringstream out; out << "expected ["; for (size_t i = 0; i < expected.size(); i++) { @@ -17735,6 +17747,12 @@ void RunCase(VulkanHarness *vulkan, const TestCase &test) { compiled.program.info.samplers[0].force_point_filtering, "bit-packed sampled image did not force point filtering"); } + if (test.sampler_filter == vk::Filter::eLinear) { + Require(test.name, "sampler specialization", + std::ranges::none_of(compiled.program.info.samplers, + [](const auto &sampler) { return sampler.force_point_filtering; }), + "filterable sampled image incorrectly forced point filtering"); + } if (test.compile_only) { std::printf("[compute] %-32s ok\n", test.name); return; @@ -17809,8 +17827,9 @@ void RunCase(VulkanHarness *vulkan, const TestCase &test) { atomic64 ? vk::Format::eR64Uint : vk::Format::eR32Uint); } if (needs_sampler) { - sampler = vulkan->CreateNearestSampler( - test.name, sampled_image.mip_levels - test.sampled_image_view_base_mip); + sampler = vulkan->CreateSampler( + test.name, sampled_image.mip_levels - test.sampled_image_view_base_mip, + test.sampler_filter); } vulkan->Dispatch(test, compiled, buffer, needs_gds ? &gds_buffer : nullptr, @@ -30763,6 +30782,49 @@ TestCase ImageSamplePackedUintConvertsSampleAndGather() { return test; } +template TestCase ImageSampleUScaled8() { + using O = ShaderOpcode; + TestCase test; + test.name = rg ? "ImageSampleRG8UScaled" : "ImageSampleR8UScaled"; + AppendVMovLiteral(&test.code, 20, std::bit_cast(0.375f)); + AppendVMovLiteral(&test.code, 21, std::bit_cast(0.375f)); + test.code.push_back(EncodeMimg0(0x20, 0xf)); + test.code.push_back(EncodeMimg1(0, 20)); + test.code.push_back(EncodeMimg0(0x47, 0x1)); + test.code.push_back(EncodeMimg1(4, 20)); + test.code.push_back(EncodeMimg0(0x47, 0x8)); + test.code.push_back(EncodeMimg1(8, 20)); + for (u32 component = 0; component < 12; ++component) { + AppendStoreVgpr(&test.code, component, component); + } + AppendEnd(&test.code); + // R varies horizontally; G varies vertically in the opposite direction. + // Bilinear samples must retain fractions despite host UNorm filter precision. + test.image_width = test.image_height = 2; + test.sampled_image_rgba = rg ? std::vector{0xffffff00u, 0x00ff0000u} + : std::vector{0xff00ff00u}; + test.sampled_image_format = rg ? vk::Format::eR8G8Unorm : vk::Format::eR8Unorm; + test.sampled_image_dwords_per_pixel = 1; + test.sampler_filter = vk::Filter::eLinear; + test.expected_float_tolerance = 0.01f; + test.user_data = MakeSampledTextureData(rg ? Prospero::BufferFormat::k8_8UScaled + : Prospero::BufferFormat::k8UScaled); + test.user_data[1] |= 1u << 30u; + test.user_data[2] = 1u << 14u; + test.user_data[50] = 12u * sizeof(u32); + test.user_data[51] = 3u << 28u; + test.has_user_data = true; + test.image_descriptor_swizzle = DstSel(rg ? 5 : 4, 4, 0, 1); + for (float value : {rg ? 191.25f : 63.75f, 63.75f, 0.0f, 1.0f, + 0.0f, rg ? 0.0f : 255.0f, 255.0f, rg ? 255.0f : 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f}) { + test.expected.push_back(std::bit_cast(value)); + } + test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::IMAGE_GATHER4_LZ, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + return test; +} + TestCase ImageLoadR128IgnoresAdjacentMaskSgprs() { using O = ShaderOpcode; @@ -33685,6 +33747,8 @@ std::vector MakeCases() { AddCase(ImageLoadR32SintUsesSignedSampledImage); AddCase(ImageLoadPackedUintUnpacksAndSwizzles); AddCase(ImageSamplePackedUintConvertsSampleAndGather); + AddCase(ImageSampleUScaled8); + AddCase(ImageSampleUScaled8); AddCase(ImageLoadR128IgnoresAdjacentMaskSgprs); AddCase(ImageLoad1DUsesScalarCoordinate); AddCase(ImageGather2DInstructionWith1DDescriptor); @@ -38825,9 +38889,17 @@ int main(int argc, char **argv) { RunCase(&vulkan, VectorFloatConversionOps()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--scaled-texture-only") == 0) { + VulkanHarness vulkan; + RunCase(&vulkan, ImageSampleUScaled8()); + RunCase(&vulkan, ImageSampleUScaled8()); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--packed-texture-only") == 0) { VulkanHarness vulkan; vulkan.CheckPackedTextureComponents(); + RunCase(&vulkan, ImageLoadPackedUintUnpacksAndSwizzles()); + RunCase(&vulkan, ImageSamplePackedUintConvertsSampleAndGather()); return 0; } if (argc == 2 && std::strcmp(argv[1], "--system-dialog-only") == 0) {