From dc5a6a9d5976877c9f524b97b1c300c7979b993e Mon Sep 17 00:00:00 2001 From: kuba Date: Mon, 28 Sep 2026 12:43:20 +0200 Subject: [PATCH 1/2] [SpirvTranslator] Order emulated wave64 LDS phases in every block A guest wave64 spans two host subgroups. The guest orders one lane's LDS write before another lane's read by executing in lockstep, so the translator puts a workgroup barrier between the halves wherever the wave switches between writing and reading LDS. It only did so for programs of a single block, which left every program with branches -- an NGG geometry stage compute program has seventeen exec-skip branches around its ES to GS LDS exchange -- reading the other half's LDS words with no ordering at all, so the result depended on timing. Every guest branch condition is wave-uniform in this emulation (EXEC and VCC tests go through the wave mask and SCC is scalar), so every block is entered by the whole wave and may hold a barrier, exactly as the wave64 ballot exchanges already do. A block can be reached from either phase, so its first LDS access always starts with a barrier. --- .../Gen5SpirvTranslator.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 4c4a7b9..34e89b6 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -1289,7 +1289,7 @@ public static partial class Gen5SpirvTranslator StoreS(register.Value, value); } - private enum SharedMemoryPhase { None, Read, Write } + private enum SharedMemoryPhase { None, Read, Write, Unknown } private bool TryEmitBlock( IReadOnlyList blocks, @@ -1298,10 +1298,13 @@ public static partial class Gen5SpirvTranslator { error = string.Empty; var block = blocks[blockIndex]; - // One guest wave can span two host subgroups. Keep its shared-memory phases ordered. - // Restrict added barriers to a single block, where all invocations follow the same path. - var synchronizeSharedMemory = _emulateWave64 && blocks.Count == 1; - var sharedMemoryPhase = SharedMemoryPhase.None; + // One guest wave can span two host subgroups; the guest orders its lanes' LDS accesses + // by executing in lockstep, so a change between reading and writing needs a barrier + // between the halves. Every guest branch condition is wave-uniform (EXEC and VCC tests + // go through the wave mask, SCC is scalar), so every block is entered by the whole wave + // and may hold a barrier. Another block may have left either phase behind. + var synchronizeSharedMemory = _emulateWave64; + var sharedMemoryPhase = blocks.Count == 1 ? SharedMemoryPhase.None : SharedMemoryPhase.Unknown; for (var index = block.StartIndex; index < block.EndIndex; index++) { var instruction = _request.Program.Instructions[index]; From 355127b4b851a6ef3c944495febf6b23080228a0 Mon Sep 17 00:00:00 2001 From: kuba Date: Tue, 29 Sep 2026 23:07:28 +0200 Subject: [PATCH 2/2] [SpirvTranslator] Take wave32 EXEC/VCC lane state from the low register In wave32 a lane mask is its low register alone: EXEC_HI and VCC_HI are ordinary SGPRs. Writing one of them therefore must not recompute the wave lane state, and the lane state has to be tested against the low dword rather than against a 64-bit mask composed from the low and high registers. The lane-bit test that guards this covered the wave64 constant width only, so it now accepts a mask of either width. --- .../Gen5SpirvTranslator.cs | 25 +++++++++++++++++-- .../Agc/Gen5WaveMaskSpirvTests.cs | 19 +++++++------- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index 34e89b6..97d3907 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -6837,14 +6837,35 @@ public static partial class Gen5SpirvTranslator Store(ScalarPointer(register), value); if (register is 106 or 107) { - Store(_vcc, IsWaveMaskActive(LoadS64(106))); + if (_waveLaneCount != 32 || register == 106) + { + Store(_vcc, IsLaneSetInMaskRegisters(106)); + } } else if (register is 126 or 127) { - Store(_exec, IsWaveMaskActive(LoadS64(126))); + if (_waveLaneCount != 32 || register == 126) + { + Store(_exec, IsLaneSetInMaskRegisters(126)); + } } } + // In wave32 a lane mask is its low register alone (VCC_HI and EXEC_HI are ordinary SGPRs), + // so the lane's bit is tested on one dword instead of a composed 64-bit mask. + private uint IsLaneSetInMaskRegisters(uint lowRegister) + { + if (_waveLaneCount != 32) + { + return IsWaveMaskActive(LoadS64(lowRegister)); + } + + var laneBit = _subgroupInvocationIdInput == 0 + ? UInt(1) + : ShiftLeftLogical(UInt(1), GuestWaveLane()); + return IsNotZero(BitwiseAnd(LoadS(lowRegister), laneBit)); + } + private void StoreV(uint register, uint value, bool guardWithExec = true) { if (guardWithExec) diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs index 45e7e6c..88b8f5f 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs @@ -22,7 +22,7 @@ public sealed class Gen5WaveMaskSpirvTests // the per-lane _vcc predicate from the wave mask via IsWaveMaskActive. var spirv = Compile([0x7C04_0300u]); - // The lane's bit in single-lane emulation is the 64-bit constant 1, so the + // The lane's bit in single-lane emulation is the constant 1, so the // predicate is `(mask & 1) != 0`. The whole-word bug emitted `mask != 0` // with no such mask. Require the lane-bit AND to be present. Assert.True( @@ -47,26 +47,27 @@ public sealed class Gen5WaveMaskSpirvTests Assert.NotEmpty(spirv); } - // True when the module contains an OpBitwiseAnd whose operand is a 64-bit - // constant of value 1 — the current-lane bit that IsCurrentLaneSet masks the - // wave mask with before the non-zero test. + // True when the module contains an OpBitwiseAnd whose operand is an integer + // constant of value 1 — the current-lane bit that the lane test masks the + // wave mask with before the non-zero test. A wave64 mask is a 64-bit value + // and a wave32 mask is a single dword, so either width counts. private static bool ContainsLaneBitMaskedWaveTest(byte[] spirv) { var laneBitConstIds = new HashSet(); - // Pass 1: collect 64-bit OpConstant result-ids whose value is 1. + // Pass 1: collect OpConstant result-ids whose integer value is 1. foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv)) { - // OpConstant = 43; a 64-bit constant occupies 5 words - // (opcode, resultType, resultId, valueLow, valueHigh). - if (op != 43 || wordCount != 5) + // OpConstant = 43; a 32-bit constant occupies 4 words and a 64-bit one + // 5 (opcode, resultType, resultId, valueLow[, valueHigh]). + if (op != 43 || wordCount is not (4 or 5)) { continue; } var resultId = ReadWord(spirv, offset + 8); var low = ReadWord(spirv, offset + 12); - var high = ReadWord(spirv, offset + 16); + var high = wordCount == 5 ? ReadWord(spirv, offset + 16) : 0u; if (low == 1 && high == 0) { laneBitConstIds.Add(resultId);