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

96 lines
4.1 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.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Xunit;
using static SharpEmu.ShaderCompiler.Tests.Resources.ResourceTestProgram;
namespace SharpEmu.Libs.Tests.Gpu.Vulkan;
// The wave-wide maximum UE's motion blur takes of its per-lane sample counts: row_shr 1, 2,
// 4 and 8 through DPP, v_permlanex16_b32 across the row pairs, then v_readlane_b32 of lanes
// 31 and 63. The result bounds a sample loop, so a wrong maximum can hang the GPU.
public sealed class WaveMaxReductionDeviceTests(HeadlessVulkanFixture fixture) : IClassFixture<HeadlessVulkanFixture>
{
private static readonly uint[] Reduction =
[
0x280404FA, 0xFF011102, // v_max_u32_dpp v2, v2, v2 row_shr:1
0x280404FA, 0xFF011202, // row_shr:2
0x280404FA, 0xFF011402, // row_shr:4
0x280404FA, 0xFF011802, // row_shr:8
0xD7781004, 0x03058302, // v_permlanex16_b32 v4, v2, -1, -1
0x28040902, // v_max_u32 v2, v2, v4
0xD760000A, 0x00013F02, // v_readlane_b32 s10, v2, 31
0xD760000B, 0x00017F02, // v_readlane_b32 s11, v2, 63
0x84940B0A, // s_max_u32 s20, s10, s11
];
[Theory]
[InlineData(0x7E040300u, 63u)] // v_mov_b32 v2, v0: the maximum is in the last lane
[InlineData(0x4C0400BFu, 63u)] // v_sub_nc_u32 v2, 63, v0: the maximum is in lane 0
public void ReductionYieldsTheWaveMaximum(uint prepareValue, uint expected)
{
var vulkan = fixture.Vulkan;
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
uint[] words =
[
prepareValue,
.. Reduction,
0x7E060214, // v_mov_b32 v3, s20
0x340A0082, // v_lshlrev_b32 v5, 2, v0
0xE0701000, 0x80010305, // buffer_store_dword v3, v5, s[4:7], 0 offen
0xBF810000, // s_endpgm
];
var bytes = new byte[words.Length * sizeof(uint)];
for (var index = 0; index < words.Length; index++)
{
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)), words[index]);
}
var context = new CpuContext(new InstructionMemory(bytes), Generation.Gen5);
Assert.True(Gen5ShaderTranslator.TryDecodeProgram(context, 0x1000, out var program, out var decodeError), decodeError);
var (plan, resources, layout) = Prepare(program);
var request = new ShaderCompileRequest(plan, resources, layout)
{
LocalSizeX = 64, ThreadCountX = 64, WaveSize = 64,
};
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 output = runner.CreateBuffer(256);
var registers = new uint[256];
registers[6] = 256;
harness.Run(() => runner.Dispatch(registers,
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [output] }, 1));
var result = runner.ReadBack(output, 0, 256);
for (var lane = 0; lane < 64; lane++)
{
Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(result.AsSpan(lane * sizeof(uint))));
}
harness.AssertNoValidationMessages();
}
private sealed class InstructionMemory(byte[] bytes) : ICpuMemory
{
public bool TryRead(ulong address, Span<byte> destination)
{
if (address < 0x1000 || address - 0x1000 + (ulong)destination.Length > (ulong)bytes.Length)
{
return false;
}
bytes.AsSpan((int)(address - 0x1000), destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong address, ReadOnlySpan<byte> source) => false;
}
}