diff --git a/CMakeLists.txt b/CMakeLists.txt index 84c8ec836..88320c6c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -657,6 +657,8 @@ if(BUILD_TESTING) add_test(NAME shader_recompiler_compute COMMAND $) add_test(NAME shader_recompiler_tessellation COMMAND $ --tessellation-only) + add_test(NAME shader_recompiler_float_min_max + COMMAND $ --float-min-max-only) add_test(NAME shader_recompiler_alignbyte COMMAND $ --alignbyte-only) add_test(NAME graphics_pipeline_rasterization diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index f819686b1..c1f520642 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -308,7 +308,7 @@ struct PipelineCache::ProgramCache { auto result = ShaderRecompiler::CompileProgram(std::move(translated), options, specialization, push_data_start_dword); if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv, - options.maintenance8_enabled)) { + options.profile->maintenance8)) { DumpShaderSpirv(stage_name, options.shader_hash, result.spirv); EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label, options.shader_hash); @@ -398,7 +398,7 @@ struct PipelineCache::ProgramCache { } ShaderRecompiler::CompileOptions options; options.stage = stage; - options.maintenance8_enabled = graphics.maintenance8_enabled; + options.profile = &profile; options.shader_hash = params.hash; options.user_data = user_data; options.back_code = params.back_code; @@ -480,7 +480,13 @@ struct PipelineCache::ProgramCache { return permutation.handle; } - explicit ProgramCache(GraphicContext& graphics): graphics(graphics) { + explicit ProgramCache(GraphicContext& graphics) + : graphics(graphics), + // AMD GPUs (vendor 0x1002) run NMin/NMax as v_min/v_max, which give the guest's NaN and + // signed-zero results; GLSL.std.450 alone does not order -0 below +0. + profile {.maintenance8 = graphics.maintenance8_enabled, + .native_float_min_max = + graphics.physical_device_properties.vendorID == 0x1002u} { lookup_key.static_state.reserve(MaxStaticKeyWords); } ~ProgramCache() { @@ -495,6 +501,7 @@ struct PipelineCache::ProgramCache { std::unordered_map programs; ProgramKey lookup_key; GraphicContext& graphics; + const ShaderRecompiler::Spirv::Profile profile; uint64_t next_shader_id = 0; }; diff --git a/src/graphics/shader/recompiler/ShaderRecompiler.cpp b/src/graphics/shader/recompiler/ShaderRecompiler.cpp index e565a0da5..3d54df0f5 100644 --- a/src/graphics/shader/recompiler/ShaderRecompiler.cpp +++ b/src/graphics/shader/recompiler/ShaderRecompiler.cpp @@ -930,8 +930,8 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n", GetDumpLabel(options), StageName(ir.stage), ir.shader_hash); - auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword, - options.maintenance8_enabled, options.emit_debug_info); + auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword, *options.profile, + options.emit_debug_info); LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64 " elapsed_ms=%" PRIu64 "\n", GetDumpLabel(options), StageName(ir.stage), ir.shader_hash, diff --git a/src/graphics/shader/recompiler/ShaderRecompiler.h b/src/graphics/shader/recompiler/ShaderRecompiler.h index 041ea3ca8..a0dad18bf 100644 --- a/src/graphics/shader/recompiler/ShaderRecompiler.h +++ b/src/graphics/shader/recompiler/ShaderRecompiler.h @@ -3,6 +3,7 @@ #include "common/common.h" #include "common/stringUtils.h" +#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h" #include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h" #include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h" #include "graphics/shader/shader.h" @@ -23,7 +24,7 @@ struct CompileOptions { bool dump_ir = true; bool early_dump = false; bool emit_debug_info = false; - bool maintenance8_enabled = false; + const Spirv::Profile* profile = &Spirv::DefaultProfile; const char* dump_label = nullptr; std::span user_data; std::span back_code; diff --git a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp index 1a201e46c..5e4fc63d8 100644 --- a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp @@ -204,7 +204,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) { } // namespace std::vector EmitProgram(IR::Program& program, ShaderStageInputInfo input_info, - uint32_t push_data_start_dword, bool maintenance8_enabled, + uint32_t push_data_start_dword, const Profile& profile, bool emit_debug_info) { using namespace Emitter; @@ -220,7 +220,7 @@ std::vector EmitProgram(IR::Program& program, ShaderStageInputInfo inp } IR::ValidateProgram(program, true); program.bindings = {.push_data_start_dword = push_data_start_dword}; - EmitterState state(program, input_info, maintenance8_enabled); + EmitterState state(program, input_info, profile); if (emit_debug_info) { char name[64]; std::snprintf(name, sizeof(name), "rdna2-%016" PRIx64 "-block-byte-pc-plus-one", diff --git a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h index 7c3393840..80f98045d 100644 --- a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h +++ b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h @@ -9,11 +9,19 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv { +// Host device capabilities that change the emitted SPIR-V. Built once per device. +struct Profile { + bool maintenance8 = false; // Dynamic texel offsets on image samples. + bool native_float_min_max = false; // NMin/NMax give the guest's NaN and signed-zero results. +}; + +inline constexpr Profile DefaultProfile {}; + // Resets and writes backend IDs and descriptor indices; do not emit the same program concurrently. std::vector EmitProgram(IR::Program& program, ShaderStageInputInfo input_info, - uint32_t push_data_start_dword = 0, - bool maintenance8_enabled = false, - bool emit_debug_info = false); + uint32_t push_data_start_dword = 0, + const Profile& profile = DefaultProfile, + bool emit_debug_info = false); } // namespace Libs::Graphics::ShaderRecompiler::Spirv diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp index fee74b1fd..ce9635fba 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAluHelpers.cpp @@ -113,6 +113,12 @@ static uint32_t EmitMinMaxF32(EmitterState& state, uint32_t lhs, uint32_t rhs, u } uint32_t EmitMinMaxF32Value(EmitterState& state, uint32_t lhs, uint32_t rhs, bool max_value) { + if (state.profile.native_float_min_max) { + const auto result = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpExtInst, TypeF32(state), result, GlslStd450(state), + max_value ? GLSLstd450NMax : GLSLstd450NMin, lhs, rhs); + return result; + } return EmitMinMaxF32(state, lhs, rhs, EmitBitcastF32ToU32(state, lhs), EmitBitcastF32ToU32(state, rhs), max_value); } diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp index 366b7893f..967feb938 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterImage.cpp @@ -1057,8 +1057,8 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) { operands[1] = AddressF32(ctx, mem, *address, layout.bias); operand_count = 2; } - if (layout.offset != NoImageComponent && - !offset_value.IsImmediate() && !state.maintenance8_enabled) { + if (layout.offset != NoImageComponent && !offset_value.IsImmediate() && + !state.profile.maintenance8) { ctx.Fail(inst, "requires maintenance8 for a dynamic sample texel offset"); } const auto samplers = LoadImageSamplers(ctx, inst); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index fa6820e20..67444ef9c 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -5,6 +5,7 @@ #include "common/stringUtils.h" #include "graphics/shader/recompiler/BufferFormat.h" #include "graphics/shader/recompiler/backend/spirv/SpirvBuilder.h" +#include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h" @@ -73,9 +74,9 @@ struct ImageDefinition { }; struct EmitterState { - EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_, bool maintenance8_enabled_) + EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_, const Profile& profile_) : builder(program_.stage == ShaderType::Mesh ? 0x00010400u : 0x00010300u), - program(program_), input_info(input_info_), maintenance8_enabled(maintenance8_enabled_) { + program(program_), input_info(input_info_), profile(profile_) { if (ShaderWorkgroupInput(program.stage, input_info) != nullptr) { lds_storage_class = spv::StorageClassWorkgroup; } @@ -88,7 +89,7 @@ struct EmitterState { Builder builder; IR::Program& program; ShaderStageInputInfo input_info; - bool maintenance8_enabled; + const Profile& profile; uint32_t debug_source = 0; uint32_t void_type = 0; uint32_t bool_type = 0; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index e8a83524d..55d1351fb 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -1313,6 +1313,9 @@ struct BdaMapping { u32 backing_offset = 0; }; +using ShaderProfile = ShaderRecompiler::Spirv::Profile; +constexpr ShaderProfile Maintenance8Profile{.maintenance8 = true}; + struct TestCase { const char *name = ""; std::vector code; @@ -1340,6 +1343,7 @@ struct TestCase { std::vector expected_storage_image_r32ui; std::vector required_spirv; std::vector forbidden_spirv; + const ShaderProfile *profile = &Maintenance8Profile; ShaderComputeInputInfo compute_info = [] { ShaderComputeInputInfo info{}; info.lds_size_dwords = 1024; @@ -1672,7 +1676,7 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) { } ShaderRecompiler::CompileOptions options; options.stage = ShaderType::Compute; - options.maintenance8_enabled = true; + options.profile = test.profile; options.dump_ir = true; auto compute_info = test.compute_info; compute_info.host_subgroup_size = host_subgroup_size; @@ -1745,7 +1749,7 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) { result.program.shader_info_complete = false; ShaderRecompiler::IR::CollectShaderInfo(result.program, options.input_info); result.spirv = ShaderRecompiler::Spirv::EmitProgram( - result.program, options.input_info, 0u, options.maintenance8_enabled); + result.program, options.input_info, 0u, *options.profile); const auto shader_data = result.program.bindings.descriptor_counts[ static_cast(ShaderRecompiler::IR::DescriptorBindingKind::ShaderData)]; Require(test.name, "binding layout", @@ -1862,7 +1866,7 @@ CompiledShader CompileFragmentCase(const GraphicsCase &test) { ShaderRecompiler::CompileOptions options; options.stage = ShaderType::Pixel; - options.maintenance8_enabled = true; + options.profile = &Maintenance8Profile; options.dump_ir = false; options.input_info.pixel = &pixel_info; options.user_data = user_data; @@ -2001,6 +2005,12 @@ class VulkanHarness { public: VulkanHarness() { Init(); } ~VulkanHarness() { Destroy(); } + // Host float min/max is used on AMD GPUs, as in Kyty. + [[nodiscard]] bool SupportsNativeFloatMinMax() const { + vk::PhysicalDeviceProperties properties; + m_physical_device.getProperties(&properties); + return properties.vendorID == 0x1002u; + } VulkanHarness(const VulkanHarness &) = delete; VulkanHarness &operator=(const VulkanHarness &) = delete; @@ -10799,7 +10809,7 @@ public: ShaderRecompiler::CompileOptions options; options.wave_size = wave_size; options.dump_ir = false; - options.maintenance8_enabled = true; + options.profile = &Maintenance8Profile; input.host_subgroup_size = SubgroupSize(); options.input_info.compute = &input; options.user_data = user_data; @@ -27863,6 +27873,66 @@ TestCase VectorMinMaxF32NanAndSignedZeroEdges() { O::V_MAX3_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}}; } +// MIN/MAX/MIN3/MAX3 on runtime inputs, so no compiler can fold them. The expected bits follow +// the emulated rules: a NaN operand yields the other operand and signed zeros are ordered. +// Denormal operands are left out; their expected results are not pinned down here. +TestCase VectorMinMaxF32RuntimeEdges(u32 wave) { + using O = ShaderOpcode; + const auto nan = [](u32 v) { return (v & 0x7fffffffu) > 0x7f800000u; }; + const auto zero = [](u32 v) { return (v & 0x7fffffffu) == 0u; }; + const auto value = [](u32 v) { return std::bit_cast(v); }; + const auto min2 = [&](u32 a, u32 b) { + if (nan(a)) return b; + if (nan(b)) return a; + if (zero(a) && zero(b)) return a | b; + return value(a) < value(b) ? a : b; + }; + const auto max2 = [&](u32 a, u32 b) { + if (nan(a)) return b; + if (nan(b)) return a; + if (zero(a) && zero(b)) return a & b; + return value(a) >= value(b) ? a : b; + }; + constexpr u32 cases[][3] = { + {0x40000000u, 0x40400000u, 0x3f800000u}, {0x7fc00000u, 0x40000000u, 0xbf800000u}, + {0x40000000u, 0x7fc00000u, 0x3f800000u}, {0x7fa00001u, 0x40000000u, 0x40400000u}, + {0x40000000u, 0x7fa00001u, 0xc0000000u}, {0x80000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x80000000u, 0x00000000u}, {0x80000000u, 0x80000000u, 0x00000000u}, + {0xff800000u, 0x7f800000u, 0x00000000u}, {0x7fc00000u, 0x00000000u, 0x80000000u}, + {0xffc05678u, 0x3f800000u, 0x7fa00001u}, {0xbf800000u, 0xc0000000u, 0x7fc01234u}}; + TestCase test; + test.name = "VectorMinMaxF32RuntimeEdges"; + test.has_compute_info = true; + test.compute_info.wave_size = wave; + test.compute_info.float_mode = 0xf0; // Preserve FP32 input/output denormals. + test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = + test.compute_info.threads_num[2] = 1u; + for (const auto &values : cases) + test.initial.insert(test.initial.end(), values, values + 3); + test.expected = test.initial; + auto &code = test.code; + for (u32 index = 0; index < std::size(cases); ++index) { + for (u32 operand = 0; operand < 3; ++operand) { + AppendVMovU32(&code, 30, (index * 3u + operand) * 4u); + AppendBufferLoadDword(&code, operand, 30); + } + const auto [a, b, c] = cases[index]; + code.push_back(EncodeVop2(0x0f, 10, Vgpr(0), 1)); + code.push_back(EncodeVop2(0x10, 11, Vgpr(0), 1)); + AppendVop3(&code, 0x151, 12, Vgpr(0), Vgpr(1), Vgpr(2)); + AppendVop3(&code, 0x154, 13, Vgpr(0), Vgpr(1), Vgpr(2)); + for (u32 result = 0; result < 4; ++result) + AppendStoreVgpr(&code, 10 + result, static_cast(test.expected.size() + result)); + test.expected.insert(test.expected.end(), + {min2(a, b), max2(a, b), min2(min2(a, b), c), max2(max2(a, b), c)}); + } + AppendEnd(&code); + test.initial.resize(test.expected.size()); + test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_MIN_F32, O::V_MAX_F32, + O::V_MIN3_F32, O::V_MAX3_F32, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + return test; +} + TestCase VectorMed3F32NanUsesMin3Path(u32 wave) { using O = ShaderOpcode; // Runtime inputs keep every MED3 in the emitted module; compare raw result bits. @@ -38144,7 +38214,8 @@ void CheckDynamicSampleOffsets(VulkanHarness &vulkan, u32 wave_size) { "dynamic offsets expanded samples or lost signed six-bit components"); #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS if (wave_size == 32u) { - Require(test.name, "default feature gate", !ShaderRecompiler::CompileOptions{}.maintenance8_enabled, + Require(test.name, "default feature gate", + !ShaderRecompiler::CompileOptions{}.profile->maintenance8, "maintenance8 must reflect the enabled device feature"); ExpectFatal("DynamicSampleOffsetsRequireMaintenance8", [&] { ShaderRecompiler::Spirv::EmitProgram(compiled.program, {.compute = &test.compute_info}); @@ -42983,6 +43054,8 @@ std::vector MakeCases() { AddCase(VectorMinMaxF32NanAndSignedZeroEdges); cases.push_back(VectorMed3F32NanUsesMin3Path(32)); cases.push_back(VectorMed3F32NanUsesMin3Path(64)); + cases.push_back(VectorMinMaxF32RuntimeEdges(32)); + cases.push_back(VectorMinMaxF32RuntimeEdges(64)); AddCase(VectorFloatConversionOps); AddCase(VectorFrexpF32Edges); AddCase(CvtF32ToIntSaturatesNaNAndOutOfRange); @@ -48940,6 +49013,26 @@ int main(int argc, char **argv) { RunCase(&vulkan, CvtPkrtzF16F32SdwaAndOutputModifiers()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--float-min-max-only") == 0) { + // Host min/max must keep the emulated results on AMD GPUs; other GPUs only + // compile it. + static constexpr ShaderProfile native_profile{.maintenance8 = true, + .native_float_min_max = true}; + VulkanHarness vulkan; + const bool supported = vulkan.SupportsNativeFloatMinMax(); + for (bool native : {false, true}) { + for (auto test : + {VectorMinMaxF32NanAndSignedZeroEdges(), + VectorMinMaxF32RuntimeEdges(32), VectorMinMaxF32RuntimeEdges(64)}) { + auto &spirv = native ? test.required_spirv : test.forbidden_spirv; + spirv = {" NMin ", " NMax "}; + test.profile = native ? &native_profile : &Maintenance8Profile; + test.compile_only = native && !supported; + RunCase(&vulkan, test); + } + } + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--thread-dimensions-only") == 0) { VulkanHarness vulkan; CheckComputeThreadDimensions(vulkan); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index a3f2cf0f9..cedced11f 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -16350,8 +16350,8 @@ void CheckSpirvBlockSourceMapping(TestCompileResult &result) { Check(SpirvInstructionOpcodeCount(result.spirv, spv::OpString) == 0 && SpirvInstructionOpcodeCount(result.spirv, spv::OpLine) == 0, "default emission unexpectedly contains source mapping"); - const auto mapped = Spirv::EmitProgram(result.program, options.input_info, 0, - false, true); + const auto mapped = + Spirv::EmitProgram(result.program, options.input_info, 0, {}, true); CheckSpirvBinaryValidates(mapped); uint32_t source = 0; uint32_t label = 0;