From 6cebf7ec560289c4565a298d8fdc85e8477959e0 Mon Sep 17 00:00:00 2001 From: Mathias <06mathias6@gmail.com> Date: Fri, 2 Oct 2026 13:10:39 +0100 Subject: [PATCH] Fix/vulkan wave64 ballot masks (#1011) * fix(shader): preserve wave64 ballot masks on Vulkan * test(shader): cover subgroup lane bits in wave mask predicates --- .../Gen5SpirvTranslator.cs | 18 ++++++--- .../Agc/Gen5WaveMaskSpirvTests.cs | 39 +++++++++++++------ .../Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs | 19 +++++++++ 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs index b57576e..7076240 100644 --- a/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs +++ b/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs @@ -7610,11 +7610,17 @@ public static partial class Gen5SpirvTranslator _uvec4Type, UInt(3), condition); - return _module.AddInstruction( - SpirvOp.VectorExtractDynamic, - _uintType, - ballot, - ShiftRightLogical(Load(_uintType, _subgroupInvocationIdInput), UInt(5))); + var component = ShiftRightLogical(Load(_uintType, _subgroupInvocationIdInput), UInt(5)); + // Avoid dynamic extraction of subgroup ballot words: on the NVIDIA + // device tests it produced zero masks. Select the same word explicitly. + var result = _module.AddInstruction(SpirvOp.CompositeExtract, _uintType, ballot, 0); + for (uint index = 1; index < 4; index++) + { + var word = _module.AddInstruction(SpirvOp.CompositeExtract, _uintType, ballot, index); + result = _module.AddInstruction(SpirvOp.Select, _uintType, + _module.AddInstruction(SpirvOp.IEqual, _boolType, component, UInt(index)), word, result); + } + return result; } private uint BooleanToHalfWaveMask(uint condition) @@ -7841,7 +7847,7 @@ public static partial class Gen5SpirvTranslator instruction.Opcode.Contains("Saveexec", StringComparison.Ordinal) || instruction.Opcode.StartsWith("SCbranchExec", StringComparison.Ordinal) || instruction.Opcode.StartsWith("SCbranchVcc", StringComparison.Ordinal) || - instruction.Opcode.StartsWith("VCmpx", StringComparison.Ordinal) || + instruction.Opcode.StartsWith("VCmp", StringComparison.Ordinal) || instruction.Sources.Any(IsWaveMaskOperand) || instruction.Destinations.Any(IsWaveMaskOperand)); diff --git a/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs b/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs index 45e7e6c..a8ad82e 100644 --- a/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs +++ b/tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs @@ -4,6 +4,7 @@ using System.Buffers.Binary; using SharpEmu.HLE; using SharpEmu.ShaderCompiler; +using SharpEmu.ShaderCompiler.Resources; using SharpEmu.ShaderCompiler.Tests.Resources; using SharpEmu.ShaderCompiler.Vulkan; using Xunit; @@ -15,15 +16,17 @@ public sealed class Gen5WaveMaskSpirvTests { private const ulong ShaderAddress = 0x1_0000_0000; - [Fact] - public void WaveMaskPredicate_IsTestedAtCurrentLaneBit() + [Theory] + [InlineData(ShaderStage.Compute)] + [InlineData(ShaderStage.Pixel)] + public void WaveMaskPredicate_IsTestedAtCurrentLaneBit(ShaderStage stage) { // V_CMP_EQ_F32 vcc, v0, v1 writes VCC at run time, which re-materialises // the per-lane _vcc predicate from the wave mask via IsWaveMaskActive. - var spirv = Compile([0x7C04_0300u]); + var spirv = Compile([0x7C04_0300u], stage); - // The lane's bit in single-lane emulation is the 64-bit constant 1, so the - // predicate is `(mask & 1) != 0`. The whole-word bug emitted `mask != 0` + // Graphics single-lane emulation uses 1; compute shifts it by the lane ID. The + // predicate is `(mask & lane_bit) != 0`. The whole-word bug emitted `mask != 0` // with no such mask. Require the lane-bit AND to be present. Assert.True( ContainsLaneBitMaskedWaveTest(spirv), @@ -47,9 +50,7 @@ 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 OpBitwiseAnd consumes a constant or shifted current-lane bit. private static bool ContainsLaneBitMaskedWaveTest(byte[] spirv) { var laneBitConstIds = new HashSet(); @@ -73,7 +74,23 @@ public sealed class Gen5WaveMaskSpirvTests } } - // Pass 2: look for an OpBitwiseAnd that consumes one of those constants. + // Compute forms the lane bit with OpShiftLeftLogical, then may select + // zero for lanes outside the guest wave. Follow those result IDs too. + foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv)) + { + if (op == 196 && wordCount == 5 && + laneBitConstIds.Contains(ReadWord(spirv, offset + 12))) + { + laneBitConstIds.Add(ReadWord(spirv, offset + 8)); + } + else if (op == 169 && wordCount == 6 && + laneBitConstIds.Contains(ReadWord(spirv, offset + 16))) + { + laneBitConstIds.Add(ReadWord(spirv, offset + 8)); + } + } + + // Look for an OpBitwiseAnd that consumes a current-lane bit. foreach (var (op, wordCount, offset) in EnumerateInstructions(spirv)) { // OpBitwiseAnd = 199 (opcode, resultType, resultId, operand0, operand1). @@ -114,13 +131,13 @@ public sealed class Gen5WaveMaskSpirvTests private static uint ReadWord(byte[] spirv, int offset) => BinaryPrimitives.ReadUInt32LittleEndian(spirv.AsSpan(offset, sizeof(uint))); - private static byte[] Compile(uint[] programWords) + private static byte[] Compile(uint[] programWords, ShaderStage stage = ShaderStage.Compute) { var memory = new FakeCpuMemory(ShaderAddress, 0x2000); var ctx = new CpuContext(memory, Generation.Gen5); Gen5ShaderAtomicDecodeTests.WriteProgram(memory, ShaderAddress, programWords); Assert.True(Gen5ShaderTranslator.TryDecodeProgram(ctx, ShaderAddress, out var program, out var error), error); - var request = ResourceTestProgram.Request(program, userDataCount: 16); + var request = ResourceTestProgram.Request(program, stage: stage, userDataCount: 16); Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out error), error); return shader.Spirv; } diff --git a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs index 5bda8e7..1374644 100644 --- a/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs +++ b/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/Wave64HalfMaskDeviceTests.cs @@ -77,6 +77,25 @@ public sealed class Wave64HalfMaskDeviceTests(HeadlessVulkanFixture fixture) : I return lanes; } + [Fact] + public void ExplicitCompareMask_IsCollectedAcrossTheWave() + { + if (!Ready()) return; + Lane(); + CompareTo("VCmpGeU32", 10, Gen5Operand.Vector(2), Operand(3)); + Add(MoveVectorFromScalar(0, 4, 10)); + Assert.All(Run(), x => Assert.Equal(0xFFFF_FFF8u, x)); + } + + [Fact] + public void InitialExecContainsEveryLane() + { + if (!Ready()) return; + Lane(); + Add(MoveVectorFromScalar(0, 4, 126)); + Assert.All(Run(), x => Assert.Equal(uint.MaxValue, x)); + } + private bool Ready() => GatePrerequisites.Ready(fixture.Vulkan, shaderInt64: true); [Fact]