From a07ad305787e9e174b61bc64550d13863c2fe0aa Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 08:33:06 +1300 Subject: [PATCH 1/2] feat(shader): request DenormPreserve 32 where the device preserves f32 denormals The recompiler asked for DenormPreserve only for 16-bit and 64-bit floats, so a device that flushes 32-bit denormals by default (RADV, MoltenVK) flushed them whatever the guest's float mode said. target.float32DenormPreserve is set from shaderDenormPreserveFloat32 when denormBehaviorIndependence allows a 32-bit mode, is part of the cache key and of the request serialization (version 41), and makes the module declare DenormPreserve 32 unless the 32-bit flush is already taken in hardware. APS5_NO_HW_F32_PRESERVE=1 turns it off. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 +++ .../Execution/src/VulkanDevice.cpp | 5 +- .../tests/F32DenormPreserve.cpp | 95 +++++++++++++++++++ .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 25 ++++- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 +- core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 3 + .../tests/RequestSerializerTests.cpp | 33 ++++--- 14 files changed, 168 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index c468b48af4..e9aa48b2c2 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -805,6 +805,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_f32_denorm_preserve_tests EXCLUDE_FROM_ALL tests/F32DenormPreserve.cpp) +target_link_libraries(agc_f32_denorm_preserve_tests PRIVATE shader_recompiler_agc_driver libc) +set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_f32_denorm_preserve_tests) +if(BUILD_TESTING) + set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_f32_denorm_preserve COMMAND agc_f32_denorm_preserve_tests) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1") + if(WIN32) + set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_spirv_branch_targets_tests EXCLUDE_FROM_ALL tests/SpirvBranchTargets.cpp) target_link_libraries(agc_spirv_branch_targets_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_spirv_branch_targets_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 4dd977afd7..0ce74ecc31 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -321,6 +321,7 @@ struct VulkanDevice::State { bool float16DenormPreserve = false; bool float32FlushToZeroCandidate = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool probingFloatControls = false; bool demoteToHelperInvocation = false; bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; @@ -1329,7 +1330,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : const auto float32Flush = Float32FlushControlFor(driverId, floatControls.shaderDenormFlushToZeroFloat32, floatControls.denormBehaviorIndependence); state->float32FlushToZeroCandidate = float32Flush.probe && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr; if (state->float32FlushToZeroCandidate && float32Flush.declared) state->capabilities.push_back(spv::CapabilityDenormFlushToZero); - if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); + state->float32DenormPreserve = floatControls.shaderDenormPreserveFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_PRESERVE") == nullptr; + if ((state->float16DenormPreserve || state->float32DenormPreserve) && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve); deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME); deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME); state->capabilities.push_back(4448); @@ -3638,6 +3640,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const { require(state->shaderProfile != nullptr, "shader device profile is unavailable"); auto target = state->shaderProfile->Target(); target.float32FlushToZero = state->float32FlushToZero; + target.float32DenormPreserve = state->float32DenormPreserve; return target; } diff --git a/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp new file mode 100644 index 0000000000..0f3fe85da7 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp @@ -0,0 +1,95 @@ +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace ShaderRecompiler; + +void require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +constexpr std::array CvtF16F32Store{0x7e021500u, 0xe0700000u, 0x80000100u, 0xbf810000u}; + +std::vector recompile(bool preserve, bool flush, bool flushCapability = true) { + const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityDenormPreserve, spv::CapabilityDenormFlushToZero}; + const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; + const std::array extensions{"SPV_KHR_storage_buffer_storage_class", "SPV_KHR_float_controls"}; + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x20000u, CvtF16F32Store, 0, {}}; + request.context.waveSize = 64; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00403000u; + request.target.spirvVersion = 0x00010600u; + request.target.subgroupSize = 64; + request.target.bdaAbiVersion = 1; + request.target.supportedCapabilities = std::span(capabilities).first(flushCapability ? capabilities.size() : capabilities.size() - 1); + request.target.supportedExtensions = extensions; + request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; + request.target.maxWorkgroupInvocations = 1024; + request.target.maxWorkgroupSharedMemoryBytes = 49152; + request.target.float32DenormPreserve = preserve; + request.target.float32FlushToZero = flush; + request.layout = {0, 0, 0, 128}; + request.useCache = false; + auto result = Recompile(request); + require(!result.spirv.empty(), "the shader did not recompile"); + return result.spirv.Copy(); +} + +struct Modes { + bool preserve32 = false; + bool flush32 = false; + bool preserveCapability = false; +}; + +Modes scan(std::span words) { + Modes modes; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + const auto op = static_cast(words[cursor] & 0xffffu); + require(count != 0 && cursor + count <= words.size(), "truncated SPIR-V instruction"); + if (op == spv::OpCapability && words[cursor + 1] == spv::CapabilityDenormPreserve) modes.preserveCapability = true; + if (op == spv::OpExecutionMode && count == 4 && words[cursor + 3] == 32u) { + if (words[cursor + 2] == spv::ExecutionModeDenormPreserve) modes.preserve32 = true; + if (words[cursor + 2] == spv::ExecutionModeDenormFlushToZero) modes.flush32 = true; + } + cursor += count; + } + return modes; +} + +void verify() { + const auto preserved = scan(recompile(true, false)); + require(preserved.preserve32 && preserved.preserveCapability && !preserved.flush32, "float32DenormPreserve did not emit the 32-bit DenormPreserve execution mode and capability"); + const auto plain = scan(recompile(false, false)); + require(!plain.preserve32 && !plain.flush32, "a target without float32DenormPreserve or float32FlushToZero set a 32-bit denormal execution mode"); + const auto flushed = scan(recompile(true, true)); + require(flushed.flush32 && !flushed.preserve32, "the hardware 32-bit flush did not take precedence over the preserve flag"); + const auto nativeFlush = scan(recompile(true, true, false)); + require(!nativeFlush.preserve32 && !nativeFlush.flush32, "a hardware flush without the DenormFlushToZero capability declared a 32-bit denormal execution mode"); +} + +} + +int main() { + try { + verify(); + } catch (const std::exception& error) { + std::fprintf(stderr, "F32DenormPreserve: %s\n", error.what()); + return 1; + } + std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 56b20d8633..41cd7882ce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -706,6 +706,7 @@ void verifyKeySensitivity() { changes("the narrow subgroup clock", [](SampleRequest& sample) { sample.request.target.narrowSubgroupClock = true; }); changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; }); changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; }); + changes("f32 denormal preservation", [](SampleRequest& sample) { sample.request.target.float32DenormPreserve = true; }); changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index c8f04297a6..f19683805c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4848,7 +4848,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQScAAAA=", "new requests did not use serialization version 39"); + require(requestPrefix(encoded, 8u) == "NVNQQSkAAAA=", "new requests did not use serialization version 41"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -4966,7 +4966,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSgAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSoAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5059,6 +5059,7 @@ void verifyVertexInfoVersion19() { auto version20Vertex = flagged; version20Vertex.pop_back(); version20Vertex.pop_back(); + version20Vertex.pop_back(); version20Vertex[4] = 20u; const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex)); require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL"); @@ -5069,6 +5070,7 @@ void verifyVertexInfoVersion19() { auto legacy = plain; legacy.pop_back(); legacy.pop_back(); + legacy.pop_back(); legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u); for (const auto version : {16u, 17u, 18u, 19u}) { legacy[4] = static_cast(version); @@ -5120,6 +5122,7 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); + version30.pop_back(); version30.erase(version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); @@ -5127,6 +5130,7 @@ void verifyVertexInfoVersion19() { auto spanned = sampledPixel; spanned.target.spannedWaves = true; auto version32 = decodeBase64(serializer.Serialize(spanned)); + version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -5138,8 +5142,10 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); + version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); auto version36 = sampledBytes; + version36.pop_back(); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -5151,6 +5157,7 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); + version28.pop_back(); version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); @@ -5159,6 +5166,7 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); + version27.pop_back(); version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); @@ -5167,6 +5175,7 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); @@ -5216,9 +5225,10 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(replay.request) == remapped, "a version 35 depth remap request changed on re-serialization"); const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 6, remappedBytes.begin()), "the depth remap flag and mode are not the last 6 bytes before the f32 flush flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 5u && std::equal(plainBytes.begin(), plainBytes.end() - 7, remappedBytes.begin()), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); @@ -5226,6 +5236,7 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); } auto version35 = remappedBytes; + version35.pop_back(); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); @@ -5272,6 +5283,7 @@ void verifyMergedHullConfiguration() { require(replay.request.graphics.has_value() && replay.request.graphics->tessellation.has_value() && replay.request.graphics->tessellation->mergedHull, "a merged hull configuration was lost in serialization"); require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); auto legacy = decodeBase64(merged); + legacy.pop_back(); require(legacy.size() > 52u && legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); for (const auto version : {30u, 31u, 32u, 34u, 35u, 36u, 37u, 38u}) { @@ -7650,6 +7662,13 @@ int main(int argc, char** argv) { std::vector float32FlushKey; RecompileCacheKey::Build(float32Flush, float32FlushKey); require(plainKey != float32FlushKey, "f32 hardware flush did not change the cache key"); + auto float32Preserve = uncached; + float32Preserve.target.float32DenormPreserve = true; + require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(float32Preserve)).request.target.float32DenormPreserve, "f32 denormal preservation was lost in serialization"); + require(!RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(uncached)).request.target.float32DenormPreserve, "f32 denormal preservation appeared from nowhere in serialization"); + std::vector float32PreserveKey; + RecompileCacheKey::Build(float32Preserve, float32PreserveKey); + require(plainKey != float32PreserveKey && float32FlushKey != float32PreserveKey, "f32 denormal preservation did not change the cache key"); auto changedLayout = request; changedLayout.layout.pushConstantSizeBytes = 64; require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant"); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 19711b8ddc..93bec96a00 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -289,6 +289,7 @@ private: append(key, value.spannedWaves); append(key, value.robustBufferAccess2); append(key, value.float32FlushToZero); + append(key, value.float32DenormPreserve); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 76e0e11888..5f096232b3 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -827,7 +827,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(39u); + writer.WriteU32(41u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -858,6 +858,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(request.context.vertex->depthRemapMode); } writer.WriteBool(request.target.float32FlushToZero); + writer.WriteBool(request.target.float32DenormPreserve); return base64Encode(buffer); } @@ -866,7 +867,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 39u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 41u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -911,6 +912,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } } if (version >= 36u) result.request.target.float32FlushToZero = reader.ReadBool(); + if (version >= 41u) result.request.target.float32DenormPreserve = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9246de6bda..dfb9f5d115 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -597,6 +597,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.spannedWaves = request.target.spannedWaves; targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; targetOptions.float32FlushToZero = request.target.float32FlushToZero; + targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { const auto& paired = *request.graphics->meshBarycentrics; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index a0b6c665fb..3d97d20f77 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -290,6 +290,7 @@ struct SpirvTarget { bool robustBufferAccess2 = false; bool depthRangeRemap = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index c563510d54..1bcd3ad560 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -29,6 +29,7 @@ struct SpirvTargetOptions { bool spannedWaves = false; bool robustBufferAccess2 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool meshBarycentricPixel = false; bool meshBarycentricVertex = false; std::uint32_t barycentricSmoothLocation = 0xffffffffu; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 3603bb59b8..351812ca3c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -112,6 +112,7 @@ struct SpirvEmitterState { bool float16DenormPreserve = false; bool robustBufferAccess2 = false; bool float32FlushToZero = false; + bool float32DenormPreserve = false; bool meshBarycentricPixel = false; bool meshBarycentricVertex = false; std::uint32_t barycentricSmoothLocation = 0xffffffffu; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index ca0506ad1a..06cd36f1fc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -277,6 +277,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float16DenormPreserve = target.float16DenormPreserve; state.robustBufferAccess2 = target.robustBufferAccess2; state.float32FlushToZero = target.float32FlushToZero; + state.float32DenormPreserve = target.float32DenormPreserve; state.hostSubgroupSize = target.subgroupSize; if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { state.requirements.ldsLock = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 3e437d8187..5fad9ba460 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -344,6 +344,9 @@ void DefineModule(SpirvEmitterState& state) { if (state.float32FlushToZero && std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityDenormFlushToZero)) != state.supportedCapabilities.end()) { state.module.EmitCapability(spv::CapabilityDenormFlushToZero); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormFlushToZero, 32u); + } else if (state.float32DenormPreserve && !state.float32FlushToZero) { + state.module.EmitCapability(spv::CapabilityDenormPreserve); + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDenormPreserve, 32u); } const bool deviceFloat64 = std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFloat64)) != state.supportedCapabilities.end(); state.integerInterpolation = state.requirements.float64Interpolation && !deviceFloat64; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 138602235a..8d5bae5a39 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -214,12 +214,12 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 39u; + upgraded[4] = 41u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -254,12 +254,12 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 39u; + layered[4] = 41u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); - layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + layered.insert(layered.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -280,11 +280,11 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 39u; + bytes[4] = 41u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); - bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + bytes.insert(bytes.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -410,10 +410,10 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - upgraded[4] = 39u; + upgraded[4] = 41u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); - upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -429,14 +429,14 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 39u; + main25[4] = 41u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); InsertPixelDefaultsAfterFrontFace(main25); } if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); - main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); + main25.insert(main25.end(), sizeof(std::uint64_t) + 1u + 5u + 1u + 1u, 0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -447,13 +447,19 @@ int main() { for (const bool preserve : {false, true}) { request.target.float16DenormPreserve = preserve; request.target.float32FlushToZero = !preserve; + request.target.float32DenormPreserve = preserve; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 39u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 41u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + auto version40 = bytes; + version40.pop_back(); + version40[4] = 40u; + const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); + Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 40u; + future[4] = 42u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -496,6 +502,7 @@ int main() { Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.pop_back(); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); version35.erase(stencilOperationValue); From 3117837b35cab422512163aa4138325ab285acbb Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 10:44:22 +1300 Subject: [PATCH 2/2] docs(dev): list APS5_NO_HW_F32_PRESERVE in ENVIRONMENT.md --- docs/dev/ENVIRONMENT.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 88b972567d..0ca7e9a29e 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -240,6 +240,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_NO_HOOK_SNAPSHOT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | takes the GpuMutex on every access instead of using the pending-write snapshot | | `APS5_NO_HOST_IMPORT` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | disables importing guest memory, so address-based shaders copy instead of using it in place | | `APS5_NO_HW_F32_FLUSH` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | skips the startup f32 denormal flush probe and keeps the emulated flush in every shader | +| `APS5_NO_HW_F32_PRESERVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | stops requesting DenormPreserve for 32-bit floats, so the device keeps its default f32 denormal handling | | `APS5_NO_IMAGE_PAGE_CACHE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp` | | leaves the main guest image's page-protection table uncached | | `APS5_NO_INDIRECT_DRAW` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp` | | rejects indirect draw packets at submission validation | | `APS5_NO_JOIN_WAW_CHECK` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp` | | lets a contiguous store join the pending one without the write-after-write check |