sharpemu/tests/SharpEmu.Libs.Tests/Gpu/Vulkan/GlobalMemoryShaderTests.cs

718 lines
35 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Tests.Gpu.Images;
using SharpEmu.Libs.Tests.Gpu.Scheduling;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Tests.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
using Xunit.Abstractions;
namespace SharpEmu.Libs.Tests.Gpu.Vulkan;
[Collection(SchedulingStateCollection.Name)]
public sealed class GlobalMemoryShaderTests(HeadlessVulkanFixture fixture, ITestOutputHelper output)
: IClassFixture<HeadlessVulkanFixture>
{
private const ulong ShaderAddress = 0x1_0000_0000;
private const ulong BufferAddress = 0x2_0000_0000;
private const int BufferBytes = 192;
private const int MemoryOffset = 64;
private const int RegisterOutputOffset = 128;
private const uint SourceRegister = 4;
private const uint DestinationRegister = 12;
[Theory]
[InlineData(false, true, 14u)]
[InlineData(true, true, 14u)]
[InlineData(false, false, 14u)]
[InlineData(true, false, 14u)]
[InlineData(false, true, 8u)]
[InlineData(true, true, 8u)]
[InlineData(false, false, 8u)]
[InlineData(true, false, 8u)]
public void MissingDevicePageRecordsOnlyActiveLaneAccesses(bool flatAddress, bool laneEnabled, uint opcode)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var shader = CompileShader(flatAddress, opcode, false, DestinationRegister, laneEnabled);
var initial = CreateInput();
var result = RunOnce(vulkan, shader, initial, traceMissingPage: true, expectFault: laneEnabled);
if (!laneEnabled)
Assert.Equal(ExpectedResult(initial, opcode, false, DestinationRegister, false), result);
}
public static IEnumerable<object[]> MemoryCases()
{
foreach (var usesFlatAddress in new[] { false, true })
{
foreach (var opcode in new uint[] { 8, 9, 10, 11, 12, 13, 14, 15, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 50, 56 })
{
yield return [usesFlatAddress, opcode, false, false, true];
if (opcode is 50 or 56) yield return [usesFlatAddress, opcode, true, false, true];
}
yield return [usesFlatAddress, 14u, false, true, true];
yield return [usesFlatAddress, 37u, false, true, true];
yield return [usesFlatAddress, 50u, true, true, true];
yield return [usesFlatAddress, 56u, true, true, true];
foreach (var opcode in new uint[] { 14, 30, 50, 56 })
yield return [usesFlatAddress, opcode, opcode >= 50, false, false];
}
}
[Theory]
[MemberData(nameof(MemoryCases))]
public void MemoryInstructions_PreserveDataAndRegisterResultsOnTheDevice(
bool usesFlatAddress, uint opcode, bool returnsValue, bool sharesDataRegister, bool laneEnabled)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var destination = sharesDataRegister ? SourceRegister : DestinationRegister;
var initial = CreateInput();
var expected = ExpectedResult(initial, opcode, returnsValue, destination, laneEnabled);
var shader = CompileShader(usesFlatAddress, opcode, returnsValue, destination, laneEnabled);
Assert.Equal(expected, RunOnce(vulkan, shader, initial));
vulkan.AssertNoValidationMessages();
output.WriteLine($"Verified opcode={opcode}, flat={usesFlatAddress}, return={returnsValue}, shared={sharesDataRegister}, active={laneEnabled} on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
private const uint IdentitySwizzle = 4 | (5 << 3) | (6 << 6) | (7 << 9);
private const uint SwizzleXFromW = 7 | (5 << 3) | (6 << 6) | (7 << 9);
private const uint Format32Float = 22;
private const uint Format8x4Unorm = 56;
private const uint Format8x4Uint = 60;
private const uint Format32x4Float = 77;
public static IEnumerable<object[]> TypedCases()
{
// instruction format, descriptor word 3, byte offset, expected result kind
yield return [Format32x4Float, (Format8x4Unorm << 12) | IdentitySwizzle, (uint)MemoryOffset, "raw"];
yield return [Format32Float, (Format32x4Float << 12) | SwizzleXFromW, (uint)MemoryOffset, "first"];
yield return [Format32x4Float, 0u, (uint)MemoryOffset, "zero"];
yield return [Format32x4Float, (Format8x4Unorm << 12) | IdentitySwizzle, 0xF00u, "zero"];
yield return [Format8x4Unorm, (Format32x4Float << 12) | IdentitySwizzle, (uint)UnormOffset, "unorm"];
// An element that crosses the end of the binding reads as zero in every component.
yield return [Format32x4Float, (Format8x4Unorm << 12) | IdentitySwizzle, (uint)(BufferBytes - 8), "zero"];
}
// Bytes 0x00 and 0xFF convert to exact floats, so the comparison does not depend on the
// device's division rounding.
private const int UnormOffset = MemoryOffset + 32;
private const uint UnormWord = 0x00FF_FF00;
// The instruction format selects the components and their conversion; the descriptor only
// decides whether the buffer is bound, and the binding decides the bounds.
[Theory]
[MemberData(nameof(TypedCases))]
public void TypedBufferLoads_UseTheInstructionFormatOnTheDevice(
uint instructionFormat, uint descriptorWord3, uint offset, string expectedKind)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var initial = CreateInput();
WriteWord(initial, UnormOffset, UnormWord);
var expected = ExpectedTypedResult(initial, expectedKind, (int)offset);
var shader = CompileTypedShader(instructionFormat, descriptorWord3, offset);
var result = RunOnce(vulkan, shader, initial);
Assert.Equal(expected, result);
output.WriteLine($"Verified typed format={instructionFormat}, descriptor=0x{descriptorWord3:X}, offset=0x{offset:X}, kind={expectedKind} on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
[Theory]
[InlineData(true, 1u, 188u, IdentitySwizzle, true)]
[InlineData(true, 2u, 184u, IdentitySwizzle, true)]
[InlineData(true, 2u, 188u, IdentitySwizzle, false)]
[InlineData(false, 1u, 188u, IdentitySwizzle, true)]
[InlineData(false, 2u, 184u, IdentitySwizzle, true)]
[InlineData(false, 2u, 188u, IdentitySwizzle, false)]
[InlineData(false, 1u, 184u, 5u, true)]
[InlineData(false, 2u, 188u, 4u | (4u << 3), true)]
[InlineData(false, 2u, 3840u, 1u, true)]
public void FormattedLoads_CheckOnlySelectedComponentsOnTheDevice(
bool typed, uint componentCount, uint offset, uint selectors, bool inBounds)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var initial = CreateInput();
WriteWord(initial, 184, 0x3F800000);
WriteWord(initial, 188, 0x40000000);
var expected = (byte[])initial.Clone();
initial.AsSpan(0, 16).CopyTo(expected.AsSpan(MemoryOffset, 16));
for (uint destination = 0; destination < componentCount; destination++)
{
var selector = typed ? destination + 4 : (selectors >> (int)(destination * 3)) & 7;
var value = selector == 1 ? 0x3F800000u :
inBounds && selector >= 4 ? ReadWord(initial, checked((int)(offset + (selector - 4) * 4))) : 0u;
WriteWord(expected, MemoryOffset + (int)destination * 4, value);
}
var shader = CompileBoundaryAccess(typed, store: false, componentCount, offset, selectors);
Assert.Equal(expected, RunOnce(vulkan, shader, initial));
}
[Theory]
[InlineData(1u, 188u, true)]
[InlineData(2u, 184u, true)]
[InlineData(2u, 188u, false)]
public void TypedStores_CheckOnlyTransferredComponentsOnTheDevice(uint componentCount, uint offset, bool inBounds)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var initial = CreateInput();
var expected = (byte[])initial.Clone();
if (inBounds)
initial.AsSpan(0, checked((int)componentCount * 4)).CopyTo(expected.AsSpan((int)offset));
var shader = CompileBoundaryAccess(typed: true, store: true, componentCount, offset, IdentitySwizzle);
Assert.Equal(expected, RunOnce(vulkan, shader, initial));
}
private static ShaderFixture CompileBoundaryAccess(bool typed, bool store, uint componentCount, uint offset, uint selectors)
{
var words = new List<uint> { 0xE0380000, 0x80000400 };
var opcode = componentCount - 1 + (store ? 4u : 0u);
words.Add(typed
? 0xE8000000 | (Format32x4Float << 19) | (opcode << 16) | offset
: 0xE0000000 | (opcode << 18) | offset);
words.Add(0x80040400);
if (!store)
{
words.Add(0xE0780000 | MemoryOffset);
words.Add(0x80000400);
}
words.Add(0xBF810000);
var scalars = BaseScalars();
scalars[16] = scalars[0];
scalars[17] = scalars[1];
scalars[18] = BufferBytes;
scalars[19] = (Format32x4Float << 12) | selectors;
return CompileProgram(DecodeProgram(words), scalars);
}
private const uint Format8x2Unorm = 14;
private const uint Format16Float = 13;
private const uint Format8x4Snorm = 57;
private const uint Format8x4Sint = 61;
private const uint Format2x10x3Uint = 48;
private const uint Format10x11x2Float = 43;
private const uint BoundDescriptor = (Format32x4Float << 12) | IdentitySwizzle;
private const float Half = 0.5f;
public static IEnumerable<object[]> DescriptorStoreCases()
{
foreach (var testCase in TypedStoreCases())
{
var format = (uint)testCase[0];
testCase[1] = (uint)testCase[1] == 0 ? 0u : (format << 12) | IdentitySwizzle;
yield return testCase;
}
// 2_10_10_10 UNORM is R10G10B10A2: X=1023 in bits 0..9, Y=512 in 10..19, Z=0, W=3 in 30..31.
yield return [50u, (50u << 12) | IdentitySwizzle, 7u, (uint)MemoryOffset,
Floats(1f, Half, 0f, 1f), Bytes(0xFF, 0x03, 0x08, 0xC0)];
}
[Theory]
[MemberData(nameof(DescriptorStoreCases))]
public void FormattedBufferStores_UseDescriptorConversionWithoutChangingNeighborBytes(
uint format, uint descriptorWord3, uint opcode, uint offset, uint[] sources, byte[]? expectedBytes)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var initial = CreateInput();
for (var index = 0; index < sources.Length; index++)
WriteWord(initial, index * 4, sources[index]);
var expected = (byte[])initial.Clone();
initial.AsSpan(0, 64).CopyTo(expected.AsSpan(RegisterOutputOffset, 64));
expectedBytes?.CopyTo(expected.AsSpan((int)offset));
var shader = CompileTypedShader(format, descriptorWord3, offset, opcode, SourceRegister, typed: false);
Assert.Equal(expected, RunOnce(vulkan, shader, initial));
}
public static IEnumerable<object[]> TypedStoreCases()
{
// instruction format, descriptor word 3, opcode (4 = X .. 7 = XYZW), byte offset,
// the four source registers, the bytes expected at the offset (null = nothing written)
yield return [Format8x2Unorm, BoundDescriptor, 5u, (uint)MemoryOffset, Floats(1f, Half, 0, 0), Bytes(0xFF, 0x80)];
yield return [Format16Float, BoundDescriptor, 4u, (uint)MemoryOffset, Floats(1f, 0, 0, 0), Bytes(0x00, 0x3C)];
yield return [Format8x4Unorm, BoundDescriptor, 7u, (uint)MemoryOffset, Floats(1f, Half, 0f, -1f), Bytes(0xFF, 0x80, 0x00, 0x00)];
yield return [Format8x4Snorm, BoundDescriptor, 7u, (uint)MemoryOffset, Floats(1f, -1f, Half, -Half), Bytes(0x7F, 0x81, 0x40, 0xC0)];
yield return [Format8x4Sint, BoundDescriptor, 7u, (uint)MemoryOffset, Words(unchecked((uint)-1), 127, unchecked((uint)-128), 200), Bytes(0xFF, 0x7F, 0x80, 0x7F)];
yield return [Format2x10x3Uint, BoundDescriptor, 7u, (uint)MemoryOffset, Words(1, 2, 3, 1), Bytes(0x09, 0x30, 0x40, 0x00)];
yield return [Format10x11x2Float, BoundDescriptor, 7u, (uint)MemoryOffset, Floats(1f, 2f, Half, 0), Bytes(0xE0, 0x01, 0x10, 0x70)];
yield return [Format32x4Float, (Format8x4Unorm << 12) | IdentitySwizzle, 7u, (uint)MemoryOffset, Words(0xABCD1200, 0xABCD1201, 0xABCD1202, 0xABCD1203), Bytes(0x00, 0x12, 0xCD, 0xAB, 0x01, 0x12, 0xCD, 0xAB, 0x02, 0x12, 0xCD, 0xAB, 0x03, 0x12, 0xCD, 0xAB)];
yield return [Format32x4Float, 0u, 7u, (uint)MemoryOffset, Words(1, 2, 3, 4), null!];
// An element that crosses the end of the binding is not written at all.
yield return [Format32x4Float, BoundDescriptor, 7u, (uint)(BufferBytes - 8), Words(1, 2, 3, 4), null!];
yield return [Format8x2Unorm, BoundDescriptor, 5u, (uint)(BufferBytes - 1), Floats(1f, 1f, 0, 0), null!];
}
// A typed store converts each register with the instruction's number format, packs the
// components at their bit offsets and writes the whole element or nothing.
[Theory]
[MemberData(nameof(TypedStoreCases))]
public void TypedBufferStores_EncodeComponentsByTheInstructionFormatOnTheDevice(
uint instructionFormat, uint descriptorWord3, uint opcode, uint offset, uint[] sources, byte[]? expectedBytes)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var initial = CreateInput();
for (var index = 0; index < sources.Length; index++)
WriteWord(initial, index * 4, sources[index]);
var expected = (byte[])initial.Clone();
initial.AsSpan(0, 64).CopyTo(expected.AsSpan(RegisterOutputOffset, 64));
expectedBytes?.CopyTo(expected.AsSpan((int)offset));
var shader = CompileTypedShader(instructionFormat, descriptorWord3, offset, opcode, SourceRegister);
var result = RunOnce(vulkan, shader, initial);
Assert.Equal(expected, result);
output.WriteLine($"Verified typed store format={instructionFormat}, descriptor=0x{descriptorWord3:X}, opcode={opcode}, offset={offset} on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
// Four invocations each store one byte of the same dword; every byte survives because
// the sub-word merge is atomic. Typed 8-bit stores and BUFFER_STORE_BYTE share the path.
[Theory]
[InlineData(true)]
[InlineData(false)]
public void AdjacentByteStores_FromSeparateInvocationsKeepEveryByteOnTheDevice(bool typed)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
const uint threadCount = 4;
var initial = CreateInput();
var expected = (byte[])initial.Clone();
initial.AsSpan(0, 64).CopyTo(expected.AsSpan(RegisterOutputOffset, 64));
WriteWord(expected, RegisterOutputOffset, 0xFF);
WriteWord(expected, MemoryOffset, 0xFFFF_FFFF);
var shader = CompileAdjacentByteStoreShader(typed, threadCount);
var result = RunOnce(vulkan, shader, initial, threadCount);
Assert.Equal(expected, result);
output.WriteLine($"Verified {threadCount} adjacent byte stores, typed={typed} on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
private static uint[] Floats(float x, float y, float z, float w) =>
[BitConverter.SingleToUInt32Bits(x), BitConverter.SingleToUInt32Bits(y), BitConverter.SingleToUInt32Bits(z), BitConverter.SingleToUInt32Bits(w)];
private static uint[] Words(uint x, uint y, uint z, uint w) => [x, y, z, w];
private static byte[] Bytes(params byte[] bytes) => bytes;
// One plan resolves two descriptors; each format selects its own compiled specialization.
[Fact]
public void FormattedUntypedLoad_FollowsTheDescriptorAcrossSpecializations()
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
var shader = CompileDescriptorFollowingShader();
// The integer 8_8_8_8 format converts without a division, so the result is exact.
foreach (var (descriptorWord3, expectedKind) in new[]
{
((Format8x4Uint << 12) | IdentitySwizzle, "uint8"),
((Format32x4Float << 12) | IdentitySwizzle, "raw"),
})
{
var initial = CreateInput();
WriteWord(initial, UnormOffset, UnormWord);
WriteWord(initial, DescriptorOffset, (uint)(BufferAddress & uint.MaxValue));
WriteWord(initial, DescriptorOffset + 4, (uint)(BufferAddress >> 32));
WriteWord(initial, DescriptorOffset + 8, BufferBytes);
WriteWord(initial, DescriptorOffset + 12, descriptorWord3);
var expected = ExpectedTypedResult(initial, expectedKind, UnormOffset);
Assert.Equal(expected, RunOnce(vulkan, shader, initial));
}
vulkan.AssertNoValidationMessages();
output.WriteLine($"Verified one plan with two descriptor specializations on {vulkan.DeviceName}; validation={vulkan.ValidationEnabled}.");
}
private const int DescriptorOffset = 48;
private sealed record ShaderFixture(ShaderResourcePlan Plan, uint[] UserData, uint LocalSizeX);
private static byte[] RunOnce(HeadlessVulkan vulkan, ShaderFixture fixture, byte[] initial, uint threadCount = 1, bool traceMissingPage = false, bool expectFault = true)
{
bool ReadWordFromFixture(ulong address, out uint value)
{
value = 0;
if (address < BufferAddress || address - BufferAddress > (ulong)initial.Length - sizeof(uint)) return false;
value = ReadWord(initial, (int)(address - BufferAddress));
return true;
}
var plan = fixture.Plan;
var snapshot = new ResourceSnapshot();
var specialization = new ResourceSpecialization();
var inputs = ResourceTestProgram.Inputs(fixture.UserData, ReadWordFromFixture, ReadWordFromFixture);
Assert.True(ResourceMaterializer.Materialize(plan, inputs, ref snapshot, ref specialization, out var failure), failure.ToString());
var resources = ResourceMaterializer.ApplyTo(plan, specialization);
var layout = BindingLayout.Allocate(resources.Info,
BindingLayout.CollectUserDataRegisters(plan.Graph.Program, 0, 20), false,
ShaderCompileRequest.RequiresFlattenedTable(plan, resources), false);
var request = new ShaderCompileRequest(plan, resources, layout)
{
TraceDeviceAddressFaults = traceMissingPage,
LocalSizeX = fixture.LocalSizeX,
ThreadCountX = threadCount,
ThreadCountY = 1,
ThreadCountZ = 1,
};
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
using var harness = new ImageTestHarness(vulkan);
using var runner = new LayoutComputeRunner(harness, request, shader.Spirv);
var records = runner.CreateBuffer(initial);
var bindings = new Dictionary<DescriptorBindingKind, GpuBuffer[]>
{
[DescriptorBindingKind.Buffers] = snapshot.Buffers.Select(_ => records).ToArray(),
};
if (resources.Info.UsesDeviceAddresses)
{
var pageCount = (BufferAddress >> Gen5SpirvTranslator.DeviceAddressPageBits) + 1;
bindings[DescriptorBindingKind.DeviceAddressPageTable] =
[runner.CreatePageTable(pageCount, traceMissingPage ? [] : [(BufferAddress, records, 0ul)])];
bindings[DescriptorBindingKind.FaultBuffer] = [runner.CreateBuffer(((pageCount + 31) / 32) * sizeof(uint) + (traceMissingPage ? 32UL : 0UL))];
}
var flattenedTable = snapshot.FlattenedResourceTable.ToArray();
foreach (var slot in plan.WrittenRangeSlotByHandle.Values)
{
flattenedTable[slot] = (uint)(BufferAddress & uint.MaxValue);
flattenedTable[slot + 1] = (uint)(BufferAddress >> 32);
flattenedTable[slot + 2] = BufferBytes;
}
harness.Run(() => runner.Dispatch(fixture.UserData, bindings, 1, flattenedTable: flattenedTable));
var result = harness.ReadBack(records.Handle, 0, BufferBytes);
if (traceMissingPage)
{
var faults = bindings[DescriptorBindingKind.FaultBuffer][0];
var diagnostic = harness.ReadBack(faults.Handle, faults.Size - 32, 32);
Assert.Equal(expectFault ? 1u : 0u, ReadWord(diagnostic, 0));
if (expectFault)
{
Assert.Equal(plan.Hash, (ulong)ReadWord(diagnostic, 4) | ((ulong)ReadWord(diagnostic, 8) << 32));
var access = Assert.Single(plan.Graph.Program.Instructions, instruction => instruction.Control is Gen5GlobalMemoryControl);
Assert.Equal(access.Pc, ReadWord(diagnostic, 12));
Assert.Equal(BufferAddress + MemoryOffset, (ulong)ReadWord(diagnostic, 16) | ((ulong)ReadWord(diagnostic, 20) << 32));
Assert.Equal((uint)plan.Stage, ReadWord(diagnostic, 24));
}
else
{
Assert.All(harness.ReadBack(faults.Handle, 0, faults.Size), value => Assert.Equal(0, value));
}
}
harness.AssertNoValidationMessages();
vulkan.AssertNoValidationMessages();
return result;
}
private static byte[] ExpectedTypedResult(byte[] initial, string expectedKind, int loadOffset)
{
var expected = (byte[])initial.Clone();
initial.AsSpan(0, 64).CopyTo(expected.AsSpan(RegisterOutputOffset, 64));
var outputOffset = RegisterOutputOffset + checked((int)(DestinationRegister - SourceRegister) * 4);
var loaded = new uint[4];
switch (expectedKind)
{
case "raw":
for (var component = 0; component < 4; component++)
loaded[component] = ReadWord(initial, loadOffset + component * 4);
break;
case "first":
loaded[0] = ReadWord(initial, loadOffset);
break;
case "unorm":
for (var component = 0; component < 4; component++)
loaded[component] = BitConverter.SingleToUInt32Bits(initial[loadOffset + component] / 255f);
break;
case "uint8":
for (var component = 0; component < 4; component++)
loaded[component] = initial[loadOffset + component];
break;
case "zero":
break;
default:
throw new InvalidOperationException("Unexpected typed load expectation.");
}
for (var component = 0; component < 4; component++)
WriteWord(expected, outputOffset + component * 4, loaded[component]);
return expected;
}
private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset) =>
CompileTypedShader(instructionFormat, descriptorWord3, offset, opcode: 3, DestinationRegister);
private static ShaderFixture CompileTypedShader(uint instructionFormat, uint descriptorWord3, uint offset, uint opcode, uint dataRegister, bool typed = true)
{
const uint descriptorRegister = 16;
var words = new List<uint>();
for (uint group = 0; group < 4; group++)
{
words.Add(0xE038_0000 | (group * 16));
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
var accessPc = checked((uint)words.Count * 4);
// Keep the extended opcode bit clear; use the scalar operand for an odd byte offset.
words.Add(typed
? 0xE800_0000u | (instructionFormat << 19) | (opcode << 16) | offset
: 0xE000_0000u | (opcode << 18) | (offset & ~1u));
var scalarOffsetOperand = typed ? 0x80u : 0x80u + (offset & 1u);
words.Add((scalarOffsetOperand << 24) | ((descriptorRegister / 4) << 16) | (dataRegister << 8));
for (uint group = 0; group < 4; group++)
{
words.Add(0xE078_0000 | (RegisterOutputOffset + group * 16));
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
words.Add(0xBF81_0000);
var program = DecodeProgram(words);
var access = Assert.Single(program.Instructions, instruction => instruction.Pc == accessPc);
var control = Assert.IsType<Gen5BufferMemoryControl>(access.Control);
Assert.Equal(typed, control.Typed);
Assert.Equal(typed ? instructionFormat : 0u, control.TypedFormat);
Assert.Equal(descriptorRegister, control.ScalarResource);
var scalars = BaseScalars();
scalars[descriptorRegister] = scalars[0];
scalars[descriptorRegister + 1] = scalars[1];
scalars[descriptorRegister + 2] = BufferBytes;
scalars[descriptorRegister + 3] = descriptorWord3;
return CompileProgram(program, scalars);
}
// v4 = 0xFF, then each thread stores one byte at MemoryOffset + v0 (its thread index).
private static ShaderFixture CompileAdjacentByteStoreShader(bool typed, uint threadCount)
{
const uint descriptorRegister = 16;
const uint format8Uint = 5;
var words = new List<uint>();
for (uint group = 0; group < 4; group++)
{
words.Add(0xE038_0000 | (group * 16));
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
words.Add(0x7E00_02FF | (SourceRegister << 17));
words.Add(0xFF);
var accessPc = checked((uint)words.Count * 4);
const uint offsetEnabled = 1u << 12;
words.Add(typed
? 0xE800_0000u | (format8Uint << 19) | (4u << 16) | offsetEnabled | MemoryOffset
: 0xE000_0000u | (24u << 18) | offsetEnabled | MemoryOffset);
words.Add((0x80u << 24) | ((descriptorRegister / 4) << 16) | (SourceRegister << 8));
for (uint group = 0; group < 4; group++)
{
words.Add(0xE078_0000 | (RegisterOutputOffset + group * 16));
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
words.Add(0xBF81_0000);
var program = DecodeProgram(words);
var access = Assert.Single(program.Instructions, instruction => instruction.Pc == accessPc);
var control = Assert.IsType<Gen5BufferMemoryControl>(access.Control);
Assert.Equal(typed, control.Typed);
Assert.Equal(typed ? "TBufferStoreFormatX" : "BufferStoreByte", access.Opcode);
Assert.True(control.OffsetEnabled);
Assert.Equal(0u, control.VectorAddress);
var scalars = BaseScalars();
scalars[descriptorRegister] = scalars[0];
scalars[descriptorRegister + 1] = scalars[1];
scalars[descriptorRegister + 2] = BufferBytes;
scalars[descriptorRegister + 3] = BoundDescriptor;
return CompileProgram(program, scalars, threadCount);
}
private static ShaderFixture CompileDescriptorFollowingShader()
{
var words = new List<uint>();
for (uint group = 0; group < 4; group++)
{
words.Add(0xE038_0000 | (group * 16));
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
var loadDescriptorPc = checked((uint)words.Count * 4);
// s_load_dwordx4 s[16:19], s[0:1], 0x30
words.Add(0xF400_0000u | (2u << 18) | (16u << 6));
words.Add(0xFA00_0000u | DescriptorOffset);
var accessPc = checked((uint)words.Count * 4);
// buffer_load_format_xyzw v[12:15], off, s[16:19], 0 offset:96
words.Add(0xE000_0000u | (3u << 18) | UnormOffset);
words.Add((0x80u << 24) | (4u << 16) | (DestinationRegister << 8));
for (uint group = 0; group < 4; group++)
{
words.Add(0xE078_0000 | (RegisterOutputOffset + group * 16));
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
words.Add(0xBF81_0000);
var program = DecodeProgram(words);
var loadDescriptor = Assert.Single(program.Instructions, instruction => instruction.Pc == loadDescriptorPc);
Assert.Equal("SLoadDwordx4", loadDescriptor.Opcode);
var access = Assert.Single(program.Instructions, instruction => instruction.Pc == accessPc);
var control = Assert.IsType<Gen5BufferMemoryControl>(access.Control);
Assert.False(control.Typed);
Assert.Equal(16u, control.ScalarResource);
return CompileProgram(program, BaseScalars());
}
private static Gen5ShaderProgram DecodeProgram(List<uint> words)
{
var memory = new FakeCpuMemory(ShaderAddress, 0x1000);
var code = new byte[words.Count * 4];
for (var index = 0; index < words.Count; index++) WriteWord(code, index * 4, words[index]);
Assert.True(memory.TryWrite(ShaderAddress, code));
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(new CpuContext(memory, Generation.Gen5), ShaderAddress,
out var program, out var decodeError), decodeError);
return program;
}
private static uint[] BaseScalars()
{
var scalars = new uint[256];
scalars[0] = (uint)(BufferAddress & uint.MaxValue);
scalars[1] = (uint)(BufferAddress >> 32);
scalars[2] = BufferBytes;
return scalars;
}
// Every memory access of the program resolves to the one bound buffer.
private static ShaderFixture CompileProgram(Gen5ShaderProgram program, uint[] scalars, uint localSizeX = 1)
{
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, 1, 0, 20);
return new ShaderFixture(plan, scalars[..20], localSizeX);
}
private static byte[] CreateInput()
{
var bytes = new byte[BufferBytes];
Array.Fill(bytes, (byte)0xCD);
for (var index = 0; index < 16; index++)
WriteWord(bytes, index * 4, 0xABCD_1200u + (uint)index);
WriteWord(bytes, 0, 0xF000_0005);
WriteWord(bytes, MemoryOffset, 0x8000_FFE1);
WriteWord(bytes, MemoryOffset + 4, 0x1234_5678);
WriteWord(bytes, MemoryOffset + 8, 0xFEDC_BA98);
WriteWord(bytes, MemoryOffset + 12, 0x8765_4321);
return bytes;
}
private static byte[] ExpectedResult(byte[] initial, uint opcode, bool returnsValue, uint destination, bool laneEnabled)
{
var expected = (byte[])initial.Clone();
initial.AsSpan(0, 64).CopyTo(expected.AsSpan(RegisterOutputOffset, 64));
if (!laneEnabled) return expected;
var outputOffset = RegisterOutputOffset + checked((int)(destination - SourceRegister) * 4);
var original = ReadWord(initial, MemoryOffset);
var source = ReadWord(initial, 0);
var componentCount = opcode switch { 13 or 29 => 2, 14 or 30 => 4, 15 or 31 => 3, _ => 1 };
if (opcode is 50 or 56)
{
WriteWord(expected, MemoryOffset, opcode == 50 ? unchecked(original + source) : Math.Max(original, source));
if (returnsValue) WriteWord(expected, outputOffset, original);
}
else if (opcode is >= 24 and <= 31)
{
var shifted = opcode is 25 or 27 ? source >> 16 : source;
if (opcode is 24 or 25) expected[MemoryOffset] = (byte)shifted;
else if (opcode is 26 or 27) BinaryPrimitives.WriteUInt16LittleEndian(expected.AsSpan(MemoryOffset), (ushort)shifted);
else initial.AsSpan(0, componentCount * 4).CopyTo(expected.AsSpan(MemoryOffset));
}
else if (opcode is >= 12 and <= 15)
{
initial.AsSpan(MemoryOffset, componentCount * 4).CopyTo(expected.AsSpan(outputOffset));
}
else
{
var loaded = opcode switch
{
8 or 32 or 33 => (uint)(byte)original,
9 or 34 or 35 => unchecked((uint)(sbyte)original),
10 or 36 or 37 => (uint)(ushort)original,
11 => unchecked((uint)(short)original),
_ => throw new InvalidOperationException("Unexpected memory opcode."),
};
if (opcode >= 32)
{
var previous = ReadWord(expected, outputOffset);
loaded = (opcode & 1) != 0
? (previous & 0xFFFF) | ((loaded & 0xFFFF) << 16)
: (previous & 0xFFFF_0000) | (loaded & 0xFFFF);
}
WriteWord(expected, outputOffset, loaded);
}
return expected;
}
private static ShaderFixture CompileShader(bool usesFlatAddress, uint opcode, bool returnsValue, uint destination, bool laneEnabled)
{
var words = new List<uint>();
for (uint group = 0; group < 4; group++)
{
words.Add(0xE038_0000 | (group * 16));
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
if (usesFlatAddress)
{
words.Add(0x7E00_0200 | (22u << 17) | (128 + MemoryOffset));
words.Add(0xD70F_6A14);
words.Add(16u | ((256u + 22) << 9));
words.Add(0x7E00_0200 | (21u << 17) | 17u);
}
else
{
words.Add(0x7E00_0200 | (20u << 17) | (128 + MemoryOffset));
}
if (!laneEnabled) words.Add(0xBEFE_0380);
var accessPc = checked((uint)words.Count * 4);
words.Add(0xDC00_0000u | (opcode << 18) | (usesFlatAddress ? 0 : 2u << 14) | (returnsValue ? 1u << 16 : 0));
words.Add((destination << 24) | (16u << 16) | (SourceRegister << 8) | 20u);
if (!laneEnabled) words.Add(0xBEFE_03C1);
for (uint group = 0; group < 4; group++)
{
words.Add(0xE078_0000 | (RegisterOutputOffset + group * 16));
words.Add(0x8000_0000 | ((SourceRegister + group * 4) << 8));
}
words.Add(0xBF81_0000);
var memory = new FakeCpuMemory(ShaderAddress, 0x1000);
var code = new byte[words.Count * 4];
for (var index = 0; index < words.Count; index++) WriteWord(code, index * 4, words[index]);
Assert.True(memory.TryWrite(ShaderAddress, code));
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(new CpuContext(memory, Generation.Gen5), ShaderAddress,
out var program, out var decodeError), decodeError);
var access = Assert.Single(program.Instructions, instruction => instruction.Pc == accessPc);
var control = Assert.IsType<Gen5GlobalMemoryControl>(access.Control);
Assert.Equal(SourceRegister, control.SourceVectorRegister);
Assert.Equal(destination, control.DestinationVectorRegister);
Assert.Equal(16u, control.ScalarAddress);
Assert.Equal(usesFlatAddress, control.UsesFlatAddress);
var scalars = new uint[256];
scalars[0] = (uint)(BufferAddress & uint.MaxValue);
scalars[1] = (uint)(BufferAddress >> 32);
scalars[2] = BufferBytes;
scalars[16] = scalars[0];
scalars[17] = scalars[1];
return CompileProgram(program, scalars);
}
private static uint ReadWord(byte[] bytes, int offset) => BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(offset));
private static void WriteWord(byte[] bytes, int offset, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(offset), value);
}