diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 0febf9d6f9..8c3637d70b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -343,6 +343,7 @@ struct VulkanDevice::State { VkPhysicalDeviceProperties properties{}; std::string driverInfo; VkDriverId driverId = VK_DRIVER_ID_MAX_ENUM; + std::uint32_t pixelFunctionStorageDwords = 4096u; bool individuallyFreeDescriptorSets = false; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; @@ -1268,6 +1269,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); } + if (driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_NV_SHADER_SM_BUILTINS_EXTENSION_NAME)) { + VkPhysicalDeviceShaderSMBuiltinsPropertiesNV smBuiltins{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_PROPERTIES_NV}; + VkPhysicalDeviceProperties2 smProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &smBuiltins}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &smProperties); + const auto subgroup = state->subgroup.subgroupSize; + if (smBuiltins.shaderSMCount != 0u && smBuiltins.shaderWarpsPerSM != 0u && subgroup != 0u) { + const auto invocations = static_cast(smBuiltins.shaderSMCount) * smBuiltins.shaderWarpsPerSM * subgroup; + const auto budgetDwords = (4ull << 30) / (invocations * 4ull); + if (budgetDwords < state->pixelFunctionStorageDwords) state->pixelFunctionStorageDwords = static_cast(budgetDwords); + } + } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); require(hasExtension(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME), "VK_KHR_shader_float_controls is unavailable"); @@ -3787,6 +3799,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.robustBufferAccess2 = state->robustBufferAccess2; target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY; + target.pixelFunctionStorageDwords = state->pixelFunctionStorageDwords; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 167de17f59..877cf34683 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5176,6 +5176,7 @@ void verifyVertexInfoVersion19() { require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); auto version20Vertex = flagged; + version20Vertex.resize(version20Vertex.size() - sizeof(std::uint32_t)); version20Vertex.pop_back(); version20Vertex.pop_back(); version20Vertex.pop_back(); @@ -5188,6 +5189,7 @@ void verifyVertexInfoVersion19() { const auto layered = serializer.Deserialize(serializer.Serialize(vertex)); require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index"); auto legacy = plain; + legacy.resize(legacy.size() - sizeof(std::uint32_t)); legacy.pop_back(); legacy.pop_back(); legacy.pop_back(); @@ -5236,6 +5238,7 @@ void verifyVertexInfoVersion19() { const auto orderedBytes = decodeBase64(serializer.Serialize(ordered)); require(orderedBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + static_cast(orderOffset), orderedBytes.begin()) && orderedBytes[orderOffset] == 0x10u && orderedBytes[orderOffset + 11u] == 0x76u && orderedBytes[orderOffset + 12u] == 0x01u && std::equal(plainPixel.begin() + static_cast(orderOffset + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast(orderOffset + sampleOrderBytes)), "the sample order is not the four words the version 42 pixel info adds after the stencil operation value export flag"); auto orderedVersion34 = orderedBytes; + orderedVersion34.resize(orderedVersion34.size() - sizeof(std::uint32_t)); orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); @@ -5245,7 +5248,7 @@ void verifyVertexInfoVersion19() { const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); auto orderedVersion30 = orderedBytes; - orderedVersion30.resize(orderedVersion30.size() - 5u); + orderedVersion30.resize(orderedVersion30.size() - 5u - sizeof(std::uint32_t)); orderedVersion30.erase(orderedVersion30.begin() + static_cast(blendOffset + 4u), orderedVersion30.begin() + static_cast(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); @@ -5253,6 +5256,7 @@ void verifyVertexInfoVersion19() { auto orderedSpanned = ordered; orderedSpanned.target.spannedWaves = true; auto orderedVersion32 = decodeBase64(serializer.Serialize(orderedSpanned)); + orderedVersion32.resize(orderedVersion32.size() - sizeof(std::uint32_t)); orderedVersion32.pop_back(); orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); @@ -5275,6 +5279,7 @@ void verifyVertexInfoVersion19() { const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); auto version30 = sampledBytes; + version30.resize(version30.size() - sizeof(std::uint32_t)); version30.pop_back(); version30.pop_back(); version30.pop_back(); @@ -5287,6 +5292,7 @@ void verifyVertexInfoVersion19() { auto spanned = sampledPixel; spanned.target.spannedWaves = true; auto version32 = decodeBase64(serializer.Serialize(spanned)); + version32.resize(version32.size() - sizeof(std::uint32_t)); version32.pop_back(); version32.pop_back(); version32.pop_back(); @@ -5298,6 +5304,7 @@ void verifyVertexInfoVersion19() { const auto replay32 = serializer.Deserialize(encodeBase64(version32)); require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag"); auto version29 = sampledBytes; + version29.resize(version29.size() - sizeof(std::uint32_t)); version29.pop_back(); version29.pop_back(); version29.pop_back(); @@ -5305,6 +5312,7 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); auto version36 = sampledBytes; + version36.resize(version36.size() - sizeof(std::uint32_t)); version36.pop_back(); version36.pop_back(); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); @@ -5315,6 +5323,7 @@ void verifyVertexInfoVersion19() { const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); auto version28 = sampledBytes; + version28.resize(version28.size() - sizeof(std::uint32_t)); version28.pop_back(); version28.pop_back(); version28.pop_back(); @@ -5325,6 +5334,7 @@ void verifyVertexInfoVersion19() { const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); auto version27 = maskedBytes; + version27.resize(version27.size() - sizeof(std::uint32_t)); version27.pop_back(); version27.pop_back(); version27.pop_back(); @@ -5335,6 +5345,7 @@ void verifyVertexInfoVersion19() { const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); auto legacyPixel = plainPixel; + legacyPixel.resize(legacyPixel.size() - sizeof(std::uint32_t)); legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); @@ -5393,6 +5404,7 @@ void verifyDepthRemapRequests() { require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 9u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 9 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 2 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 2 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag and the pixel function LDS flag of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; + legacyBytes.resize(legacyBytes.size() - sizeof(std::uint32_t)); legacyBytes.pop_back(); legacyBytes.pop_back(); legacyBytes.pop_back(); @@ -5403,6 +5415,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.resize(version35.size() - sizeof(std::uint32_t)); version35.pop_back(); version35.pop_back(); version35.pop_back(); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index cf4bbc0138..10aaf73ff4 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -299,6 +299,7 @@ private: append(key, value.float32DenormPreserve); append(key, value.fusedExactArithmetic); append(key, value.largePixelFunctionLdsBuffer); + append(key, value.pixelFunctionStorageDwords); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 9457081b29..c754c198e6 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -872,6 +872,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const } writer.WriteBool(request.target.fusedExactArithmetic); writer.WriteBool(request.target.largePixelFunctionLdsBuffer); + writer.WriteU32(request.target.pixelFunctionStorageDwords); return base64Encode(buffer); } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 2204a0db94..a310aa792d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -607,6 +607,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.fusedExactArithmetic = request.target.fusedExactArithmetic; targetOptions.largePixelFunctionLdsBuffer = request.target.largePixelFunctionLdsBuffer; + targetOptions.pixelFunctionStorageDwords = request.target.pixelFunctionStorageDwords; 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 978d400225..962ddf0d89 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -300,6 +300,7 @@ struct SpirvTarget { bool float32DenormPreserve = false; bool fusedExactArithmetic = false; bool largePixelFunctionLdsBuffer = false; + std::uint32_t pixelFunctionStorageDwords = 4096u; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 4cb86e2472..9a418524a3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -40,6 +40,7 @@ struct SpirvTargetOptions { std::vector> meshPerVertexLocations; bool waveLdsMemoryBarriers = false; bool largePixelFunctionLdsBuffer = false; + std::uint32_t pixelFunctionStorageDwords = 4096u; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 0cfb1a7be2..8fd7e67442 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -281,7 +281,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; state.hostSubgroupSize = target.subgroupSize; - if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { + if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > target.pixelFunctionStorageDwords) { state.pixelFunctionLdsBuffer = true; state.requirements.subgroupLocalInvocationId = true; state.requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 4ebf8833b1..7e55864b42 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -500,12 +500,14 @@ int main() { request.target.float32FlushToZero = !preserve; request.target.float32DenormPreserve = preserve; request.target.largePixelFunctionLdsBuffer = !preserve; + request.target.pixelFunctionStorageDwords = preserve ? 2048u : 4096u; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve && replay.request.target.pixelFunctionStorageDwords == request.target.pixelFunctionStorageDwords); auto bytes = DecodeBase64(encoded); Require(bytes.size() >= 8u && bytes[4] == 45u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; + version40.resize(version40.size() - sizeof(std::uint32_t)); version40.pop_back(); version40.pop_back(); version40.pop_back(); @@ -571,6 +573,7 @@ int main() { Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.resize(version35.size() - sizeof(std::uint32_t)); version35.pop_back(); version35.pop_back(); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 76b39dd32c..40b1730a16 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,13 +151,10 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a sticky overflow flag and reuses slot 0 for the excess subgroups (sharing that region with the subgroup that owns it); `CheckPixelFunctionLds` throws when the flag is set, from `WaitIdle` and when a present fence retires, then clears it; arrays of 4097 to 6143 dwords take the buffer path though only 4096 (completes), 6144 (timeout), 7680 and 8192 (fault) were measured on that 5090; the pool is the first `HOST_VISIBLE|HOST_COHERENT` type so the host can map the 16-byte header without a separate device-local body (on discrete NVIDIA that is usually system memory for the full allocation, and a device-local body plus host-visible header was not measured); on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The pool fits subgroups in min(2 GiB, storage range); overflow sets a sticky flag checked from `WaitIdle` and present retirement. Header mapped via the first `HOST_VISIBLE|HOST_COHERENT` type. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. -- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. -- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`.