[SpirvTranslator] Cut emulated-wave64 and private-LDS overhead in translated shaders (#1034)
* [SpirvTranslator] Keep pure scalar lane spills in private slots under emulated wave64 A wave64 program run as a 64-thread workgroup on a 32-lane host emulates v_readlane with an LDS exchange and a workgroup barrier. Compilers use constant-lane v_writelane/v_readlane pairs on a VGPR as SGPR spill slots; when that VGPR is written only by constant-lane writelanes and read only by constant-lane readlanes, each (vgpr, lane) pair is a private scalar, so the readlane loads the slot instead of exchanging through LDS. This halves the heaviest compute dispatch in Silent Hill: The Short Message's menu. (cherry picked from commit 9915b88e68b6b69d6b74ea931a22eb0589c56f1a) * [SpirvTranslator] Zero private LDS lazily through a written-dword bitmap Graphics stages model LDS as a per-invocation private array that must read zero where the invocation has not written. The array carried a zero initializer, but Metal keeps a dynamically indexed private array in stack memory, so every invocation cleared all of it: an NGG vertex shader (the PS5 default, with in-shader culling through LDS) cleared 8 KiB per vertex. The array is now uninitialized and a zero-initialized bitmap of written dwords (1/32 of the size) records which slots were addressed; LdsPointer zeroes a slot the first time it is addressed, so loads, stores and atomics keep the zero-read semantics. Compute LDS stays workgroup memory. Silent Hill: The Short Message's menu, dominated by ~45M shadow-pass vertices per frame, goes from about 2.0 to 3.1 FPS. (cherry picked from commit 5738b8574397cf2be08cd4e1518b33057c8867b5) * [SpirvTranslator] Test the lane's mask bit in one register under emulated wave64 VCC and EXEC keep a per-lane boolean that every mask write refreshes, and the refresh composed the 64-bit mask from its two registers, built a 64-bit lane bit and masked them. A host without 64-bit integer arithmetic (Apple GPUs through MoltenVK) emulates each of those with a pair of 32-bit operations. The bit lives in exactly one of the two registers, so selecting that register and shifting inside it is the same test in 32-bit arithmetic. Only the emulated-wave64 path changes; wave32 already tested one register, and a native wave64 host keeps the existing form, which also clears the bit for lanes outside the guest wave. (cherry picked from commit 1b2651047fdf232304142c82168e487244dda4ef) * [SpirvTranslator] Let s_barrier publish only what the guest waited for Every compute s_barrier became an OpControlBarrier with AcquireRelease over uniform, workgroup and image memory, on the assumption that the guest had drained vector memory first. s_barrier only orders execution; what it publishes is whatever the preceding s_waitcnt drained. A barrier that follows an LDS-only wait (lgkmcnt(0)) publishes LDS, and ordering device memory there costs a cache flush the guest never asked for: UE's FFT bloom convolution barriers 48 times per group, all LDS-only. The translator now tracks, per block, whether any wait drained vector memory - SOPP vmcnt(0), or the GFX10 SOPK vscnt(0)/vmcnt(0) forms, since stores are counted by vscnt there - and a barrier after an LDS-only wait carries workgroup memory only. A drained counter stays drained for the rest of the block, and a barrier with no wait in its block keeps the full semantics (the wait may be in a block this one cannot be ordered against). (cherry picked from commit 1f81df59bc07d05f2baa6283da95f4901dada444)
This commit is contained in:
parent
30321142cb
commit
db32c9d882
|
|
@ -5009,7 +5009,14 @@ public static partial class Gen5SpirvTranslator
|
|||
var sourceValue = GetRawSource(instruction, 0);
|
||||
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(LaneSelectMask));
|
||||
|
||||
if (_emulateWave64)
|
||||
if (_emulateWave64 && instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister &&
|
||||
TryGetConstantLane(instruction, out var spillLane) &&
|
||||
_laneSpillSlots.TryGetValue((instruction.Sources[0].Value, spillLane), out var spillSlot))
|
||||
{
|
||||
// A pure scalar spill slot: every invocation already holds the wave-uniform value.
|
||||
StoreS(destination, Load(_uintType, spillSlot));
|
||||
}
|
||||
else if (_emulateWave64)
|
||||
{
|
||||
// The selected guest lane can belong to another host subgroup.
|
||||
// Read it even when the guest execution mask disables that lane.
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ public static partial class Gen5SpirvTranslator
|
|||
private uint _storageBlockPointer;
|
||||
private uint _storageUintPointer;
|
||||
private uint _lds;
|
||||
private uint _ldsWritten;
|
||||
private uint _ldsElementPointer;
|
||||
private uint _lds64ElementPointer;
|
||||
private uint _ldsDwordMask;
|
||||
|
|
@ -772,14 +773,24 @@ public static partial class Gen5SpirvTranslator
|
|||
var ldsPointer = _module.TypePointer(storageClass, ldsArrayType);
|
||||
_ldsElementPointer = _module.TypePointer(storageClass, _uintType);
|
||||
_lds64ElementPointer = _module.TypePointer(storageClass, _ulongType);
|
||||
_lds = storageClass == SpirvStorageClass.Workgroup
|
||||
? _module.AddGlobalVariable(ldsPointer, storageClass)
|
||||
: _module.AddGlobalVariable(
|
||||
ldsPointer,
|
||||
storageClass,
|
||||
_module.ConstantNull(ldsArrayType));
|
||||
// A private array must read zero where this invocation has not written, but
|
||||
// zero-initializing it costs every invocation the whole array: Metal keeps a
|
||||
// dynamically indexed private array in stack memory, so an NGG vertex shader
|
||||
// cleared 8 KiB per vertex. A written-dword bitmap clears 1/32 of that, and
|
||||
// LdsPointer zeroes a slot the first time it is addressed.
|
||||
_lds = _module.AddGlobalVariable(ldsPointer, storageClass);
|
||||
_module.AddName(_lds, "lds");
|
||||
_interfaces.Add(_lds);
|
||||
if (storageClass == SpirvStorageClass.Private)
|
||||
{
|
||||
var bitmapType = _module.TypeArray(_uintType, (arrayDwordCount + 31) / 32);
|
||||
_ldsWritten = _module.AddGlobalVariable(
|
||||
_module.TypePointer(SpirvStorageClass.Private, bitmapType),
|
||||
SpirvStorageClass.Private,
|
||||
_module.ConstantNull(bitmapType));
|
||||
_module.AddName(_ldsWritten, "ldsWritten");
|
||||
_interfaces.Add(_ldsWritten);
|
||||
}
|
||||
}
|
||||
|
||||
internal static SpirvImageFormat DecodeStorageImageFormat(
|
||||
|
|
@ -1457,6 +1468,8 @@ public static partial class Gen5SpirvTranslator
|
|||
// need LDS phase ordering when reads and writes span basic blocks.
|
||||
var synchronizeSharedMemory = _emulateWave64 && halfMaskPlan is null;
|
||||
var sharedMemoryPhase = SharedMemoryPhase.None;
|
||||
// A wait in another block may or may not have run before this one.
|
||||
_vectorMemoryWaited = null;
|
||||
for (var index = block.StartIndex; index < block.EndIndex; index++)
|
||||
{
|
||||
var instruction = _request.Program.Instructions[index];
|
||||
|
|
@ -2134,10 +2147,15 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
return true;
|
||||
}
|
||||
if (instruction.Opcode == "SWaitcnt")
|
||||
{
|
||||
NoteWaitCount(instruction);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (instruction.Opcode is
|
||||
"SNop" or
|
||||
"SSetregB32" or
|
||||
"SWaitcnt" or
|
||||
"SInstPrefetch" or
|
||||
"STtraceData" or
|
||||
// Wave scheduling priority hint; no effect on results.
|
||||
|
|
@ -2154,11 +2172,17 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
if (_stage == Gen5SpirvStage.Compute)
|
||||
{
|
||||
// s_waitcnt vmcnt(0) + s_barrier also publishes buffer and image
|
||||
// stores to the workgroup: AcquireRelease over uniform, workgroup
|
||||
// and image memory.
|
||||
// s_barrier orders execution; what it publishes is whatever the guest
|
||||
// waited for first. s_waitcnt vmcnt(0) before it means buffer and image
|
||||
// stores are being published to the workgroup, so the barrier carries
|
||||
// AcquireRelease over uniform, workgroup and image memory. A barrier that
|
||||
// only follows lgkmcnt(0) publishes LDS, and ordering device memory there
|
||||
// costs a cache flush the guest never asked for: UE's FFT bloom
|
||||
// convolution barriers 48 times per group, every one of them lgkmcnt-only.
|
||||
var workgroup = UInt(2);
|
||||
var semantics = UInt(0x8 | 0x40 | 0x100 | 0x800);
|
||||
var semantics = UInt(_vectorMemoryWaited == false
|
||||
? 0x8u | 0x100u
|
||||
: 0x8u | 0x40u | 0x100u | 0x800u);
|
||||
_module.AddStatement(
|
||||
SpirvOp.ControlBarrier,
|
||||
workgroup,
|
||||
|
|
@ -2683,11 +2707,30 @@ public static partial class Gen5SpirvTranslator
|
|||
var index = BitwiseAnd(
|
||||
ShiftRightLogical(addressWithOffset, UInt(2)),
|
||||
UInt(_ldsDwordMask));
|
||||
return _module.AddInstruction(
|
||||
var pointer = _module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_ldsElementPointer,
|
||||
_lds,
|
||||
index);
|
||||
if (_ldsWritten != 0)
|
||||
{
|
||||
var wordPointer = _module.AddInstruction(
|
||||
SpirvOp.AccessChain,
|
||||
_privateUintPointer,
|
||||
_ldsWritten,
|
||||
ShiftRightLogical(index, UInt(5)));
|
||||
var bit = ShiftLeftLogical(UInt(1), BitwiseAnd(index, UInt(31)));
|
||||
var word = Load(_uintType, wordPointer);
|
||||
var unwritten = _module.AddInstruction(
|
||||
SpirvOp.IEqual, _boolType, BitwiseAnd(word, bit), UInt(0));
|
||||
EmitConditional(unwritten, () =>
|
||||
{
|
||||
Store(pointer, UInt(0));
|
||||
Store(wordPointer, BitwiseOr(word, bit));
|
||||
});
|
||||
}
|
||||
|
||||
return pointer;
|
||||
}
|
||||
|
||||
private void StoreLds(uint pointer, uint value)
|
||||
|
|
@ -7466,7 +7509,7 @@ public static partial class Gen5SpirvTranslator
|
|||
var slots = new List<(uint Register, uint Lane)>();
|
||||
if (_emulateWave64)
|
||||
{
|
||||
return slots;
|
||||
return FindPureLaneSpillSlots();
|
||||
}
|
||||
|
||||
var ownsLaneZero = !UsesSubgroupOperations();
|
||||
|
|
@ -7492,6 +7535,70 @@ public static partial class Gen5SpirvTranslator
|
|||
return slots;
|
||||
}
|
||||
|
||||
// Under wave64 emulation a lane read normally rendezvous with the other host subgroup through
|
||||
// LDS and a workgroup barrier. A VGPR written only by constant-lane V_WRITELANE holds spilled
|
||||
// scalars, which are wave-uniform, so every invocation can keep each written lane itself and a
|
||||
// constant-lane V_READLANE needs no exchange. V_WRITELANE still updates the VGPR, so other
|
||||
// reads of it are unaffected. Relative VGPR addressing could alias any register, so programs
|
||||
// that use it keep the exchange.
|
||||
private List<(uint Register, uint Lane)> FindPureLaneSpillSlots()
|
||||
{
|
||||
var slots = new List<(uint Register, uint Lane)>();
|
||||
var instructions = _request.Program.Instructions;
|
||||
if (instructions.Any(static instruction => instruction.Opcode.StartsWith("VMovrel", StringComparison.Ordinal)))
|
||||
{
|
||||
return slots;
|
||||
}
|
||||
|
||||
var candidates = new HashSet<uint>();
|
||||
var rejected = new HashSet<uint>();
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
var isWritelane = instruction.Opcode == "VWritelaneB32";
|
||||
var isReadlane = instruction.Opcode == "VReadlaneB32";
|
||||
for (var index = 0; index < instruction.Destinations.Count; index++)
|
||||
{
|
||||
var operand = instruction.Destinations[index];
|
||||
if (operand.Kind != Gen5OperandKind.VectorRegister)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isWritelane && index == 0 && TryGetConstantLane(instruction, out _))
|
||||
{
|
||||
candidates.Add(operand.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
rejected.Add(operand.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// A variable lane read cannot use a slot; it keeps reading the VGPR, which V_WRITELANE
|
||||
// still writes, so ordinary vector reads of the register stay exact as well.
|
||||
if (isReadlane && instruction.Sources.Count > 0 &&
|
||||
instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister &&
|
||||
!TryGetConstantLane(instruction, out _))
|
||||
{
|
||||
rejected.Add(instruction.Sources[0].Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var instruction in instructions)
|
||||
{
|
||||
if (instruction.Opcode == "VWritelaneB32" &&
|
||||
TryGetVectorDestination(instruction, out var register) &&
|
||||
candidates.Contains(register) && !rejected.Contains(register) &&
|
||||
TryGetConstantLane(instruction, out var lane) &&
|
||||
!slots.Contains((register, lane)))
|
||||
{
|
||||
slots.Add((register, lane));
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
// The lane select operand (source 1) of V_READLANE/V_WRITELANE when it is a constant.
|
||||
private bool TryGetConstantLane(Gen5ShaderInstruction instruction, out uint lane)
|
||||
{
|
||||
|
|
@ -7716,6 +7823,25 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
if (_waveLaneCount != 32)
|
||||
{
|
||||
// The lane's bit lives in one of the two mask registers, so select that register
|
||||
// and shift inside it. Composing the 64-bit mask and masking it with a 64-bit
|
||||
// lane bit costs several times as much on a host that emulates 64-bit integers,
|
||||
// and VCC and EXEC refresh their lane flag on every mask write.
|
||||
if (_subgroupInvocationIdInput != 0 && _emulateWave64)
|
||||
{
|
||||
var lane = GuestWaveLane();
|
||||
var word = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
_module.AddInstruction(SpirvOp.ULessThan, _boolType, lane, UInt(32)),
|
||||
LoadS(lowRegister),
|
||||
LoadS(lowRegister + 1));
|
||||
return IsNotZero(
|
||||
BitwiseAnd(
|
||||
ShiftRightLogical(word, BitwiseAnd(lane, UInt(31))),
|
||||
UInt(1)));
|
||||
}
|
||||
|
||||
return IsWaveMaskActive(LoadS64(lowRegister));
|
||||
}
|
||||
|
||||
|
|
@ -8218,6 +8344,35 @@ public static partial class Gen5SpirvTranslator
|
|||
return _halfMaskPlan;
|
||||
}
|
||||
|
||||
// Null until a wait is seen in the current block; then true once any wait in the block
|
||||
// drained vector memory (vmcnt(0), or vscnt(0), which is what covers stores on GFX10),
|
||||
// which is how the guest publishes buffer and image stores before a barrier. It stays
|
||||
// true for the rest of the block: a later LDS-only wait does not unpublish them.
|
||||
private bool? _vectorMemoryWaited;
|
||||
|
||||
private void NoteWaitCount(Gen5ShaderInstruction instruction)
|
||||
{
|
||||
if (instruction.Words.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var word = instruction.Words[0];
|
||||
if (instruction.Encoding == Gen5ShaderEncoding.Sopk)
|
||||
{
|
||||
// SOPK waits name one counter: 0x17 vscnt, 0x18 vmcnt, 0x19 expcnt, 0x1A lgkmcnt.
|
||||
var counter = (word >> 23) & 0x1Fu;
|
||||
var drainsVectorMemory = counter is 0x17 or 0x18 && (word & 0xFFFFu) == 0;
|
||||
_vectorMemoryWaited = _vectorMemoryWaited == true || drainsVectorMemory;
|
||||
return;
|
||||
}
|
||||
|
||||
// SOPP S_WAITCNT packs vmcnt across bits [3:0] and [15:14].
|
||||
var immediate = word & 0xFFFFu;
|
||||
var vectorCount = (immediate & 0xFu) | (((immediate >> 14) & 0x3u) << 4);
|
||||
_vectorMemoryWaited = _vectorMemoryWaited == true || vectorCount == 0;
|
||||
}
|
||||
|
||||
private void EmitWave64Barrier()
|
||||
{
|
||||
var workgroup = UInt(2);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.ShaderCompiler.Tests.Resources;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
// What s_barrier publishes is whatever the guest waited for before it.
|
||||
public sealed class Gen5BarrierMemoryScopeTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
private const uint Barrier = 0xBF8A0000;
|
||||
private const uint EndProgram = 0xBF810000;
|
||||
private const uint WaitLgkmOnly = 0xBF8CC07F; // s_waitcnt lgkmcnt(0)
|
||||
private const uint WaitVectorMemory = 0xBF8C0F70; // s_waitcnt vmcnt(0)
|
||||
|
||||
private const uint AcquireRelease = 0x8;
|
||||
private const uint UniformMemory = 0x40;
|
||||
private const uint WorkgroupMemory = 0x100;
|
||||
private const uint ImageMemory = 0x800;
|
||||
|
||||
[Theory]
|
||||
[InlineData(WaitLgkmOnly, AcquireRelease | WorkgroupMemory)]
|
||||
[InlineData(WaitVectorMemory, AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory)]
|
||||
public void BarrierPublishesWhatTheWaitCountWaitedFor(uint wait, uint expected)
|
||||
{
|
||||
var program = Decode([wait, Barrier, EndProgram]);
|
||||
Assert.Equal("SBarrier", program.Instructions[1].Opcode);
|
||||
|
||||
var request = ResourceTestProgram.Request(program, userDataCount: 0);
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
Assert.Equal(expected, ControlBarrierSemantics(shader.Spirv));
|
||||
}
|
||||
|
||||
private const uint WaitStoresZeroSopk = 0xBBFD0000; // s_waitcnt_vscnt null, 0
|
||||
private const uint WaitLgkmZeroSopk = 0xBD7D0000; // s_waitcnt_lgkmcnt null, 0
|
||||
|
||||
// GFX10 tracks stores with vscnt, and a drained counter stays drained for the block.
|
||||
[Theory]
|
||||
[InlineData(new[] { WaitStoresZeroSopk, WaitLgkmOnly }, AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory)]
|
||||
[InlineData(new[] { WaitVectorMemory, WaitLgkmOnly }, AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory)]
|
||||
[InlineData(new[] { WaitLgkmZeroSopk }, AcquireRelease | WorkgroupMemory)]
|
||||
public void StoreWaitsCountAndStayCountedAcrossLaterWaits(uint[] waits, uint expected)
|
||||
{
|
||||
var program = Decode([.. waits, Barrier, EndProgram]);
|
||||
var request = ResourceTestProgram.Request(program, userDataCount: 0);
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
Assert.Equal(expected, ControlBarrierSemantics(shader.Spirv));
|
||||
}
|
||||
|
||||
// A barrier with no wait before it in its block keeps the conservative semantics: the
|
||||
// publish may have happened in a block the translator cannot order against this one.
|
||||
[Fact]
|
||||
public void BarrierWithoutAWaitCountKeepsDeviceMemoryOrdering()
|
||||
{
|
||||
var program = Decode([Barrier, EndProgram]);
|
||||
var request = ResourceTestProgram.Request(program, userDataCount: 0);
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
Assert.Equal(
|
||||
AcquireRelease | UniformMemory | WorkgroupMemory | ImageMemory,
|
||||
ControlBarrierSemantics(shader.Spirv));
|
||||
}
|
||||
|
||||
// The value of the memory-semantics operand of the module's single OpControlBarrier.
|
||||
private static uint ControlBarrierSemantics(byte[] spirv)
|
||||
{
|
||||
var words = new uint[spirv.Length / 4];
|
||||
Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length);
|
||||
var constants = new Dictionary<uint, uint>();
|
||||
uint? semanticsId = null;
|
||||
for (var offset = 5; offset < words.Length;)
|
||||
{
|
||||
var wordCount = (int)(words[offset] >> 16);
|
||||
var opcode = (ushort)(words[offset] & 0xFFFF);
|
||||
if (opcode == (ushort)SpirvOp.Constant && wordCount == 4)
|
||||
{
|
||||
constants[words[offset + 2]] = words[offset + 3];
|
||||
}
|
||||
else if (opcode == (ushort)SpirvOp.ControlBarrier)
|
||||
{
|
||||
Assert.Null(semanticsId);
|
||||
semanticsId = words[offset + 3];
|
||||
}
|
||||
|
||||
offset += wordCount;
|
||||
}
|
||||
|
||||
Assert.NotNull(semanticsId);
|
||||
return constants[semanticsId.Value];
|
||||
}
|
||||
|
||||
private static Gen5ShaderProgram Decode(IReadOnlyList<uint> words)
|
||||
{
|
||||
var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint));
|
||||
var bytes = new byte[words.Count * sizeof(uint)];
|
||||
for (var index = 0; index < words.Count; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]);
|
||||
}
|
||||
|
||||
Assert.True(memory.TryWrite(ShaderAddress, bytes));
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, ShaderAddress, out var program, out var error), error);
|
||||
return program;
|
||||
}
|
||||
|
||||
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
|
||||
{
|
||||
private readonly byte[] _storage = new byte[size];
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, destination.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, source.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.CopyTo(_storage.AsSpan(offset, source.Length));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryResolve(ulong address, int length, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
if (address < baseAddress || address - baseAddress > int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = (int)(address - baseAddress);
|
||||
return offset <= _storage.Length - length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.ShaderCompiler.Resources;
|
||||
using SharpEmu.ShaderCompiler.Tests.Resources;
|
||||
using SharpEmu.ShaderCompiler.Vulkan;
|
||||
using Xunit;
|
||||
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
// Graphics stages model LDS as a per-invocation private array that reads zero where the
|
||||
// invocation has not written. A written-dword bitmap provides that instead of a zero
|
||||
// initializer over the whole array.
|
||||
public sealed class Gen5PrivateLdsTests
|
||||
{
|
||||
private const uint PrivateStorageClass = 6;
|
||||
private const uint WorkgroupStorageClass = 4;
|
||||
|
||||
[Fact]
|
||||
public void VertexStageLdsIsLazilyZeroedThroughAWrittenBitmap()
|
||||
{
|
||||
var spirv = Compile(ShaderStage.Vertex);
|
||||
|
||||
var module = new SpirvModuleInspector(spirv);
|
||||
var lds = module.Names.Single(pair => pair.Value == "lds").Key;
|
||||
var written = module.Names.Single(pair => pair.Value == "ldsWritten").Key;
|
||||
Assert.Equal(PrivateStorageClass, module.VariableStorageClasses[lds]);
|
||||
Assert.Equal(PrivateStorageClass, module.VariableStorageClasses[written]);
|
||||
Assert.False(HasInitializer(spirv, lds));
|
||||
Assert.True(HasInitializer(spirv, written));
|
||||
ValidateWhenAvailable(spirv);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeLdsStaysWorkgroupMemoryWithoutABitmap()
|
||||
{
|
||||
var spirv = Compile(ShaderStage.Compute);
|
||||
|
||||
var module = new SpirvModuleInspector(spirv);
|
||||
var lds = module.Names.Single(pair => pair.Value == "lds").Key;
|
||||
Assert.Equal(WorkgroupStorageClass, module.VariableStorageClasses[lds]);
|
||||
Assert.DoesNotContain("ldsWritten", module.Names.Values);
|
||||
}
|
||||
|
||||
private static byte[] Compile(ShaderStage stage)
|
||||
{
|
||||
var program = Program(
|
||||
Vop2(0, "VLshlrevB32", 3, Operand(2), Gen5Operand.Vector(0)),
|
||||
DataShare(4, "DsWriteB32", false, [Gen5Operand.Vector(3), Gen5Operand.Vector(0)], []),
|
||||
DataShare(12, "DsReadB32", false, [Gen5Operand.Vector(3)], [5], 4),
|
||||
EndProgram(20));
|
||||
var request = Request(program, stage);
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
return shader.Spirv;
|
||||
}
|
||||
|
||||
private static void ValidateWhenAvailable(byte[] code)
|
||||
{
|
||||
var sdk = Environment.GetEnvironmentVariable("VULKAN_SDK");
|
||||
var executable = string.IsNullOrWhiteSpace(sdk) ? null
|
||||
: Path.Combine(sdk, OperatingSystem.IsWindows() ? "Bin/spirv-val.exe" : "bin/spirv-val");
|
||||
if (executable is null || !File.Exists(executable))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var path = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(path, code);
|
||||
var start = new System.Diagnostics.ProcessStartInfo(executable) { UseShellExecute = false, RedirectStandardError = true, CreateNoWindow = true };
|
||||
start.ArgumentList.Add("--target-env");
|
||||
start.ArgumentList.Add("vulkan1.2");
|
||||
start.ArgumentList.Add(path);
|
||||
using var process = System.Diagnostics.Process.Start(start)!;
|
||||
var error = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
Assert.True(process.ExitCode == 0, error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
// OpVariable carries an initializer operand after its storage class.
|
||||
private static bool HasInitializer(byte[] spirv, uint variable)
|
||||
{
|
||||
var words = new uint[spirv.Length / 4];
|
||||
Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length);
|
||||
for (var offset = 5; offset < words.Length;)
|
||||
{
|
||||
var wordCount = (int)(words[offset] >> 16);
|
||||
if ((SpirvOp)(words[offset] & 0xFFFF) == SpirvOp.Variable && words[offset + 2] == variable)
|
||||
{
|
||||
return wordCount > 4;
|
||||
}
|
||||
|
||||
offset += Math.Max(wordCount, 1);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Variable {variable} is not declared.");
|
||||
}
|
||||
}
|
||||
|
|
@ -322,6 +322,56 @@ public sealed class Gen5ScalarLaneTransferTests
|
|||
Assert.Equal(expectSlot, text.Contains("v86_lane3", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, true)]
|
||||
[InlineData(true, false)]
|
||||
public void EmulatedWave64KeepsPureScalarSpillsWithoutAnExchange(bool vectorWrite, bool expectSlot)
|
||||
{
|
||||
// Wave64 on a 32-lane host reads another lane through LDS and a workgroup barrier. A VGPR
|
||||
// that only V_WRITELANE writes holds wave-uniform spilled scalars, so its constant-lane
|
||||
// reads need no exchange; once an ordinary instruction writes it, the exchange returns.
|
||||
const ulong shaderAddress = 0x1000;
|
||||
var memory = new TestCpuMemory(shaderAddress, 0x100);
|
||||
var words = new List<uint>
|
||||
{
|
||||
0xD7610056, 0x0001047E, // v_writelane_b32 v86, s126, 2
|
||||
0xD7610056, 0x0001067F, // v_writelane_b32 v86, s127, 3
|
||||
};
|
||||
if (vectorWrite)
|
||||
{
|
||||
words.Add(0x7EAC0280); // v_mov_b32 v86, 0
|
||||
}
|
||||
|
||||
words.AddRange(
|
||||
[
|
||||
0xD7600000, 0x00010556, // v_readlane_b32 s0, v86, 2
|
||||
0xD7600001, 0x00010756, // v_readlane_b32 s1, v86, 3
|
||||
0xBF810000, // s_endpgm
|
||||
]);
|
||||
var shader = new byte[words.Count * sizeof(uint)];
|
||||
for (var index = 0; index < words.Count; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(shader.AsSpan(index * sizeof(uint)), words[index]);
|
||||
}
|
||||
|
||||
Assert.True(memory.TryWrite(shaderAddress, shader));
|
||||
var ctx = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(ctx, shaderAddress, out var program, out var decodeError), decodeError);
|
||||
var (plan, resources, layout) = ResourceTestProgram.Prepare(program);
|
||||
var request = new ShaderCompileRequest(plan, resources, layout)
|
||||
{
|
||||
LocalSizeX = 64,
|
||||
ThreadCountX = 64,
|
||||
WaveSize = 64,
|
||||
};
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var compiled, out var compileError), compileError);
|
||||
|
||||
var text = System.Text.Encoding.ASCII.GetString(compiled.Spirv);
|
||||
Assert.Equal(expectSlot, text.Contains("v86_lane2", StringComparison.Ordinal));
|
||||
Assert.Equal(expectSlot, text.Contains("v86_lane3", StringComparison.Ordinal));
|
||||
Assert.Equal(!expectSlot, ReadSpirvOpcodes(compiled.Spirv).Contains((ushort)SpirvOp.ControlBarrier));
|
||||
}
|
||||
|
||||
private static Gen5ShaderInstruction ScalarInstruction(
|
||||
uint pc,
|
||||
Gen5ShaderEncoding encoding,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
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;
|
||||
|
||||
namespace SharpEmu.ShaderCompiler.Tests;
|
||||
|
||||
// VCC and EXEC carry a per-lane boolean that every write to their registers refreshes.
|
||||
public sealed class Gen5WaveMaskLaneFlagTests
|
||||
{
|
||||
private const ulong ShaderAddress = 0x1_0000_0000;
|
||||
|
||||
[Fact]
|
||||
public void EmulatedWave64TestsTheLaneBitWithoutComposingTheMask()
|
||||
{
|
||||
var program = Decode(
|
||||
[
|
||||
0x7C020300, // v_cmp_lt_f32 vcc, v0, v1 (declares the lane input and writes a mask)
|
||||
0xBEFE0300, // s_mov_b32 exec_lo, s0
|
||||
0xBF810000, // s_endpgm
|
||||
]);
|
||||
Assert.Equal("VCmpLtF32", program.Instructions[0].Opcode);
|
||||
|
||||
var (plan, resources, layout) = ResourceTestProgram.Prepare(program, ShaderStage.Compute, 0, 0, 0);
|
||||
// A 64-thread group is the wave64-on-32-lanes case the translator emulates.
|
||||
var request = new ShaderCompileRequest(plan, resources, layout)
|
||||
{
|
||||
WaveSize = 64,
|
||||
LocalSizeX = 8,
|
||||
LocalSizeY = 8,
|
||||
LocalSizeZ = 1,
|
||||
};
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
|
||||
// The lane's bit lives in one of the two mask registers, so the refresh selects that
|
||||
// register and shifts inside it. Composing the 64-bit mask and masking it with a 64-bit
|
||||
// lane bit left 27 of these for this two-instruction program; the remaining six build
|
||||
// the mask value itself, which stays 64-bit because the guest register pair is.
|
||||
Assert.Equal(6, Count64BitBitwiseAnd(shader.Spirv));
|
||||
}
|
||||
|
||||
private static int Count64BitBitwiseAnd(byte[] spirv)
|
||||
{
|
||||
var words = new uint[spirv.Length / 4];
|
||||
Buffer.BlockCopy(spirv, 0, words, 0, spirv.Length);
|
||||
var wideTypes = new HashSet<uint>();
|
||||
var count = 0;
|
||||
for (var offset = 5; offset < words.Length;)
|
||||
{
|
||||
var wordCount = (int)(words[offset] >> 16);
|
||||
var opcode = (ushort)(words[offset] & 0xFFFF);
|
||||
if (opcode == (ushort)SpirvOp.TypeInt && words[offset + 2] == 64)
|
||||
{
|
||||
wideTypes.Add(words[offset + 1]);
|
||||
}
|
||||
else if (opcode == (ushort)SpirvOp.BitwiseAnd && wideTypes.Contains(words[offset + 1]))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
offset += wordCount;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static Gen5ShaderProgram Decode(IReadOnlyList<uint> words)
|
||||
{
|
||||
var memory = new TestCpuMemory(ShaderAddress, words.Count * sizeof(uint));
|
||||
var bytes = new byte[words.Count * sizeof(uint)];
|
||||
for (var index = 0; index < words.Count; index++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]);
|
||||
}
|
||||
|
||||
Assert.True(memory.TryWrite(ShaderAddress, bytes));
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, ShaderAddress, out var program, out var error), error);
|
||||
return program;
|
||||
}
|
||||
|
||||
private sealed class TestCpuMemory(ulong baseAddress, int size) : ICpuMemory
|
||||
{
|
||||
private readonly byte[] _storage = new byte[size];
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, destination.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
if (!TryResolve(virtualAddress, source.Length, out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
source.CopyTo(_storage.AsSpan(offset, source.Length));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryResolve(ulong address, int length, out int offset)
|
||||
{
|
||||
offset = 0;
|
||||
if (address < baseAddress || address - baseAddress > int.MaxValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset = (int)(address - baseAddress);
|
||||
return offset <= _storage.Length - length;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue