shader: emit float min/max as host instructions on AMD GPUs (#1761)

* shader: emit float min/max as host instructions on AMD GPUs

V_MIN_F32, V_MAX_F32, V_MIN3_F32 and V_MAX3_F32 were emulated with a
classify-and-select sequence of about a dozen instructions each, to give
the guest's NaN and signed-zero results on any host. AMD GPUs execute
GLSL.std.450 NMin/NMax with those same rules, so emit them directly when
the device is AMD and supports VK_KHR_shader_float_controls2. Each
instruction is decorated FPFastMathMode None, since
SignedZeroInfNanPreserve does not cover extended instructions. MED3 keeps
its exact emulation. KYTY_EMULATED_FLOAT_MIN_MAX=1 keeps the emulation.

In SAROS the two heaviest compute shaders get 37% and 59% faster on an
RX 9070 XT (RADV), about 4 ms of GPU time per frame.

Adds VectorMinMaxF32RuntimeEdges (runtime operands, so nothing is folded)
and a --float-min-max-only mode that runs it and
VectorMinMaxF32NanAndSignedZeroEdges with the emulation and, where
supported, with host instructions.

Developed with AI assistance (Claude Code).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* shader: gate host float min/max on RADV and check the emitted instructions

* shader: drop float controls 2 from host float min/max

* shader: drop the float min/max env override and atomic flag

* shader: pass host capabilities to the SPIR-V emitter as a profile built once per device

* shader: enable host float min/max on AMD GPUs

---------

Co-authored-by: arda <ardaeska@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
Arda Sahin
2026-10-21 19:36:08 +00:00
committed by GitHub
parent c07ad621f2
commit 286481889f
11 changed files with 141 additions and 23 deletions
@@ -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<ProgramKey, SourceEntry, ProgramKeyHash> programs;
ProgramKey lookup_key;
GraphicContext& graphics;
const ShaderRecompiler::Spirv::Profile profile;
uint64_t next_shader_id = 0;
};
@@ -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,
@@ -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<const uint32_t> user_data;
std::span<const uint32_t> back_code;
@@ -204,7 +204,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
} // namespace
std::vector<uint32_t> 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<uint32_t> 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",
@@ -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<uint32_t> 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
@@ -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);
}
@@ -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);
@@ -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;