From 1c770441f46fe6366ba4d1347571634013029fcb Mon Sep 17 00:00:00 2001 From: alfalfa-star Date: Fri, 16 Oct 2026 22:55:41 +0200 Subject: [PATCH] perf(shader): flush f32 denormals through the DenormFlushToZero execution mode With APS5_NATIVE_F32_FTZ=1 on a device that supports the DenormFlushToZero float-controls mode for 32-bit floats, shaders whose float mode flushes f32 denormals declare DenormFlushToZero instead of emulating the flush after every f32 operation. Shader validation accepts the execution mode as a float-controls mode. APS5_NATIVE_F32_FTZ_PIXEL_ONLY=1 limits it to pixel shaders. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 ++ .../libSceAgcDriver/Graphics/src/ShaderValidation.cpp | 3 ++- core/shader/recompiler/Recompiler.cpp | 10 ++++++++++ .../SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp | 1 + .../include/SpirvBackend/SpirvEmitterState.hpp | 1 + .../recompiler/SpirvBackend/src/SpirvEmitter.cpp | 1 + .../SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp | 4 ++++ .../include/Translation/InstructionTranslator.hpp | 1 + .../include/Translation/TranslationContext.hpp | 2 ++ .../Translation/src/InstructionTranslator.cpp | 1 + .../recompiler/Translation/src/VectorInstructions.cpp | 1 + 11 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 44c1dce86b7..8f648688d29 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1288,6 +1288,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE && floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL; if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); + static const bool nativeFloat32FlushToZero = std::getenv("APS5_NATIVE_F32_FTZ") != nullptr; + if (nativeFloat32FlushToZero && floatControls.shaderDenormFlushToZeroFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME); state->capabilities.push_back(4448); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index f2c19703811..faa12ba098d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -278,6 +278,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi capability == spv::CapabilityFloat16 || capability == spv::CapabilityRoundingModeRTE || capability == spv::CapabilityDenormPreserve || + capability == spv::CapabilityDenormFlushToZero || capability == spv::CapabilityFloat64 || capability == spv::CapabilityStorageBuffer8BitAccess || capability == spv::CapabilityPhysicalStorageBufferAddresses || @@ -371,7 +372,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi for (std::size_t i = 5; i < count; ++i) Require(module.interface.insert(instruction[i]).second, "duplicate SPIR-V interface ID"); break; case spv::OpExecutionMode: - if (count == 4 && (instruction[2] == spv::ExecutionModeSignedZeroInfNanPreserve || instruction[2] == spv::ExecutionModeRoundingModeRTE || instruction[2] == spv::ExecutionModeDenormPreserve)) { + if (count == 4 && (instruction[2] == spv::ExecutionModeSignedZeroInfNanPreserve || instruction[2] == spv::ExecutionModeRoundingModeRTE || instruction[2] == spv::ExecutionModeDenormPreserve || instruction[2] == spv::ExecutionModeDenormFlushToZero)) { Require(instruction[3] == 16u || instruction[3] == 32u || instruction[3] == 64u, "float controls execution modes require Float16, Float32 or Float64"); executionModeTargets.insert(instruction[1]); break; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9bf473cf580..3ff7acfcfb1 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -184,6 +184,14 @@ std::uint32_t ShaderPreparationContext::ResourcePrograms() const { IrProgram PrepareResourceProgram(const RecompileRequest& request) { return PrepareResourceProgram(request, nullptr); } +bool nativeFloat32FlushToZero(const RecompileRequest& request) { + const auto& capabilities = request.target.supportedCapabilities; + if (std::ranges::find(capabilities, static_cast(spv::CapabilityDenormFlushToZero)) == capabilities.end()) return false; + static const bool pixelOnly = std::getenv("APS5_NATIVE_F32_FTZ_PIXEL_ONLY") != nullptr; + if (pixelOnly && request.shader.stage != ShaderStage::Fragment) return false; + return !request.context.floatMode.has_value() || ((request.context.floatMode->floatMode >> 4u) & 3u) == 0u; +} + IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparationContext* preparation) { frontEndBuilds.fetch_add(1, std::memory_order_relaxed); @@ -210,6 +218,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat const auto supports = [&](spv::Capability capability) { return std::ranges::find(request.target.supportedCapabilities, static_cast(capability)) != request.target.supportedCapabilities.end(); }; translateOptions.nativeFloat16 = supports(spv::CapabilityFloat16) && supports(spv::CapabilityRoundingModeRTE) && supports(spv::CapabilityDenormPreserve); translateOptions.floatMode = request.context.floatMode; + translateOptions.nativeFloat32FlushToZero = nativeFloat32FlushToZero(request); translateOptions.inputInfo = inputInfo; constexpr InstructionTranslator translator; @@ -531,6 +540,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.fragmentDescriptorSet = request.target.registrationModules; targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; targetOptions.spannedWaves = request.target.spannedWaves; + targetOptions.float32FlushToZero = nativeFloat32FlushToZero(request); constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index e4a861594fe..1fb43a221a9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -22,6 +22,7 @@ struct SpirvTargetOptions { bool nonConstantImageOffsets = false; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool float32FlushToZero = false; bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 5c89545900b..81eeab3013d 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -104,6 +104,7 @@ struct SpirvEmitterState { std::uint32_t spannedExchangeVariable = 0; bool narrowSubgroupClock = false; bool float64DenormPreserve = false; + bool float32FlushToZero = false; bool registrationModules = false; bool fragmentDescriptorSet = false; bool float16DenormPreserve = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 0d629adaca2..acf3c6c16ee 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -257,6 +257,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.nonConstantImageOffsets = target.nonConstantImageOffsets; state.narrowSubgroupClock = target.narrowSubgroupClock; state.float64DenormPreserve = target.float64DenormPreserve; + state.float32FlushToZero = target.float32FlushToZero; state.registrationModules = target.registrationModules; state.fragmentDescriptorSet = target.fragmentDescriptorSet; state.float16DenormPreserve = target.float16DenormPreserve; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 763f1a1fa80..00e9add44dc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -327,6 +327,10 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitCapability(spv::CapabilitySignedZeroInfNanPreserve); state.module.EmitExtension("SPV_KHR_float_controls"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeSignedZeroInfNanPreserve, 32u); + if (state.float32FlushToZero) { + state.module.EmitCapability(spv::CapabilityDenormFlushToZero); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormFlushToZero, 32u); + } if (state.requirements.float64) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFloat64)) == state.supportedCapabilities.end()) { throw std::runtime_error("64-bit float instructions need the Float64 capability, which the device lacks"); diff --git a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp index c1aa7723ae4..3d683d62f5e 100644 --- a/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp +++ b/core/shader/recompiler/Translation/include/Translation/InstructionTranslator.hpp @@ -34,6 +34,7 @@ struct TranslateOptions { std::uint64_t shaderHash = 0; bool fragmentShaderBarycentricEnabled = false; bool nativeFloat16 = false; + bool nativeFloat32FlushToZero = false; std::optional floatMode; ShaderStageInputInfo inputInfo; const EmbeddedFetchPlan* embeddedFetch = nullptr; diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index c0e248f9193..a366a74ecc1 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -25,6 +25,7 @@ class TranslationContext { } void SetEntryRounding(const RoundingState& state) { setRounding(state); } void SetNativeFloat16(bool enabled) { nativeFloat16 = enabled; } + void SetNativeFloat32FlushToZero(bool enabled) { nativeFloat32FlushToZero = enabled; } void AddBranchCondition(const BasicBlock& source, BlockInfo& info); void TranslateCodeTableLoad(const RdnaInstruction& instruction, const ControlFlowGraph::CodeTableLoad& table); void TranslateUnlinkedCall(const RdnaInstruction& instruction); @@ -369,6 +370,7 @@ class TranslationContext { RoundingState rounding; bool ieeeMode = false; bool nativeFloat16 = false; + bool nativeFloat32FlushToZero = false; IrBuilder ir; IrBlock& block; IrU1 instructionBranchCondition; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index e29958a5580..23a36e0f9c1 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -531,6 +531,7 @@ IrProgram InstructionTranslator::Translate(const RdnaProgram& decoded, const Con context.SetPixelInput(options.inputInfo.pixel, options.fragmentShaderBarycentricEnabled); context.SetFloatMode(options.floatMode); context.SetNativeFloat16(options.nativeFloat16); + context.SetNativeFloat32FlushToZero(options.nativeFloat32FlushToZero); context.SetEntryRounding(entryRounding.at(cfgBlock.id)); for (std::uint32_t index = cfgBlock.instructionBegin; index < cfgBlock.instructionEnd; index++) { const auto& instruction = decoded.instructions[index]; diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index 645b98534e8..833c12dba7f 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -102,6 +102,7 @@ std::uint32_t TranslationContext::f32DenormalFlushFor(const RdnaInstruction& ins if (denormals == 1u || denormals == 2u) { throw std::runtime_error("f32 denormal mode " + std::to_string(denormals) + " at pc " + std::to_string(inst.programCounter) + " is not implemented"); } + if (denormals == 0u && nativeFloat32FlushToZero) return 0u; return denormals == 0u ? flush : 0u; }