Merge remote-tracking branch 'origin/main' into perf_computed_shaders

# Conflicts:
#	tests/SharpEmu.Libs.Tests/Agc/Gen5WaveMaskSpirvTests.cs
This commit is contained in:
ParantezTech 2026-10-02 20:07:35 +03:00
commit 90b1cafe6d
3 changed files with 59 additions and 18 deletions

View File

@ -8023,11 +8023,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)
@ -8259,7 +8265,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));

View File

@ -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 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,10 +50,7 @@ public sealed class Gen5WaveMaskSpirvTests
Assert.NotEmpty(spirv);
}
// 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.
// True when OpBitwiseAnd consumes a constant or shifted current-lane bit.
private static bool ContainsLaneBitMaskedWaveTest(byte[] spirv)
{
var laneBitConstIds = new HashSet<uint>();
@ -74,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).
@ -115,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;
}

View File

@ -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]