586 lines
27 KiB
C#
586 lines
27 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
// Decodes synthetic programs and compiles resource plans for validation.
|
|
// Unexpected decode, plan, or emission results fail the run.
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.ShaderCompiler;
|
|
using SharpEmu.ShaderCompiler.Metal;
|
|
using SharpEmu.ShaderCompiler.Resources;
|
|
using SharpEmu.ShaderCompiler.Vulkan;
|
|
|
|
const ulong ProgramAddress = 0x100000;
|
|
|
|
// Plans a recorded program (input.ir.txt) and emits it with the default specialization:
|
|
// checks resource planning and SPIR-V emission of a shader the game rejected, offline.
|
|
// --plan-ir <input.ir.txt> <compute|pixel|vertex> <out.spv>
|
|
if (args.Length == 4 && args[0] == "--plan-ir")
|
|
{
|
|
var lines = File.ReadAllLines(args[1]);
|
|
var words = new List<uint>();
|
|
uint userBase = 0, userCount = 0;
|
|
ulong hash = 0;
|
|
foreach (var line in lines)
|
|
{
|
|
var header = Regex.Match(line, @"user_data_base=(\d+) user_data_count=(\d+)");
|
|
if (header.Success)
|
|
{
|
|
userBase = uint.Parse(header.Groups[1].Value);
|
|
userCount = uint.Parse(header.Groups[2].Value);
|
|
}
|
|
|
|
var hashMatch = Regex.Match(line, @"hash=0x([0-9A-Fa-f]+)");
|
|
if (hashMatch.Success && hash == 0) hash = Convert.ToUInt64(hashMatch.Groups[1].Value, 16);
|
|
var match = Regex.Match(line, @"^0x([0-9A-Fa-f]+) ([0-9A-Fa-f]{8}(?:_[0-9A-Fa-f]{8})*) ");
|
|
if (!match.Success) continue;
|
|
var pc = Convert.ToUInt32(match.Groups[1].Value, 16);
|
|
if (pc != words.Count * sizeof(uint))
|
|
throw new ArgumentException($"Non-contiguous recorded code at 0x{pc:X}.");
|
|
words.AddRange(match.Groups[2].Value.Split('_').Select(word => Convert.ToUInt32(word, 16)));
|
|
}
|
|
|
|
var stage = args[2] switch { "pixel" => ShaderStage.Pixel, "vertex" => ShaderStage.Vertex, _ => ShaderStage.Compute };
|
|
var memory = new FakeMemory();
|
|
memory.AddRegion(ProgramAddress, words.ToArray());
|
|
if (!Gen5ShaderTranslator.TryDecodeProgram(new CpuContext(memory, Generation.Gen5), ProgramAddress, out var program, out var decodeError))
|
|
throw new InvalidOperationException(decodeError);
|
|
var plan = ShaderResourcePlan.Extract(program!, stage, hash, userBase, userCount);
|
|
var resources = ResourceMaterializer.ApplyTo(plan, ResourceSpecialization.Default(plan.Info));
|
|
var layout = BindingLayout.Allocate(resources.Info,
|
|
BindingLayout.CollectUserDataRegisters(program!, userBase, userCount),
|
|
BindingLayout.UsesGlobalDataShare(program!),
|
|
ShaderCompileRequest.RequiresFlattenedTable(plan, resources),
|
|
BindingLayout.ReadsShaderBase(program!),
|
|
usesBindlessImages: true,
|
|
usesRuntimeBufferStrides: true);
|
|
var request = new ShaderCompileRequest(plan, resources, layout) { ThreadCountX = 1, ThreadCountY = 1, ThreadCountZ = 1 };
|
|
if (!Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var compileError))
|
|
throw new InvalidOperationException(compileError);
|
|
File.WriteAllBytes(args[3], shader.Spirv);
|
|
var runtime = plan.Memory.Entries.Count(entry => entry.RuntimeDescriptor);
|
|
Console.WriteLine($"hash=0x{hash:X16} instructions={program!.Instructions.Count} runtime_descriptor_accesses={runtime} images={resources.Info.Images.Count} spirv_bytes={shader.Spirv.Length}");
|
|
return;
|
|
}
|
|
|
|
// Recompile recorded compute code and its resource specialization without launching
|
|
// the game. The input IR contains original instruction words, not executable host IR.
|
|
if (args.Length == 4 && args[0] == "--replay-compute")
|
|
{
|
|
using var capture = JsonDocument.Parse(File.ReadAllText(args[2]));
|
|
var state = capture.RootElement;
|
|
if (state.GetProperty("Stage").GetString() != "compute")
|
|
throw new ArgumentException("Only recorded compute shaders are supported.");
|
|
var words = new List<uint>();
|
|
foreach (var line in File.ReadLines(args[1]))
|
|
{
|
|
var match = Regex.Match(line, @"^0x([0-9A-Fa-f]+) ([0-9A-Fa-f]{8}(?:_[0-9A-Fa-f]{8})*) ");
|
|
if (!match.Success) continue;
|
|
var pc = Convert.ToUInt32(match.Groups[1].Value, 16);
|
|
if (pc != words.Count * sizeof(uint))
|
|
throw new ArgumentException($"Non-contiguous recorded code at 0x{pc:X}.");
|
|
words.AddRange(match.Groups[2].Value.Split('_').Select(word => Convert.ToUInt32(word, 16)));
|
|
}
|
|
var memory = new FakeMemory();
|
|
memory.AddRegion(ProgramAddress, words.ToArray());
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
if (!Gen5ShaderTranslator.TryDecodeProgram(context, ProgramAddress, out var program, out var error))
|
|
throw new InvalidOperationException(error);
|
|
if (program!.Instructions.Count != state.GetProperty("InstructionCount").GetInt32())
|
|
throw new InvalidOperationException("Recorded and decoded instruction counts differ.");
|
|
var userCount = state.GetProperty("UserDataCount").GetUInt32();
|
|
var userBase = state.GetProperty("UserDataBase").GetUInt32();
|
|
var wave = state.GetProperty("WaveSize").GetUInt32();
|
|
var hash = Convert.ToUInt64(state.GetProperty("ShaderHash").GetString()![2..], 16);
|
|
var plan = ShaderResourcePlan.Extract(program, ShaderStage.Compute, hash, userBase, userCount, waveSize: wave);
|
|
var specialization = state.GetProperty("Specialization").Deserialize<ResourceSpecialization>()!;
|
|
var resources = ResourceMaterializer.ApplyTo(plan, specialization);
|
|
var layout = BindingLayout.Allocate(resources.Info,
|
|
BindingLayout.CollectUserDataRegisters(program, userBase, userCount),
|
|
BindingLayout.UsesGlobalDataShare(program),
|
|
ShaderCompileRequest.RequiresFlattenedTable(plan, resources),
|
|
BindingLayout.ReadsShaderBase(program),
|
|
state.GetProperty("PushDataStartDword").GetUInt32(),
|
|
state.GetProperty("UsesDispatchThreadLimits").GetBoolean(),
|
|
state.GetProperty("UsesBindlessImages").GetBoolean(),
|
|
state.TryGetProperty("UsesRuntimeBufferStrides", out var runtimeStrides) && runtimeStrides.GetBoolean());
|
|
var request = new ShaderCompileRequest(plan, resources, layout)
|
|
{
|
|
WaveSize = wave,
|
|
EnableExecGuardElision = state.GetProperty("EnableExecGuardElision").GetBoolean(),
|
|
SupportsSharedInt64Atomics = state.GetProperty("SupportsSharedInt64Atomics").GetBoolean(),
|
|
SupportsExactFloat16Conversions = state.GetProperty("SupportsExactFloat16Conversions").GetBoolean(),
|
|
SupportsNonUniformImageIndexing = state.GetProperty("SupportsNonUniformImageIndexing").GetBoolean(),
|
|
ComputeSystemRegisters = state.GetProperty("ComputeSystemRegisters").Deserialize<Gen5ComputeSystemRegisters?>(),
|
|
ScratchDwords = state.GetProperty("ScratchDwords").GetUInt32(),
|
|
LocalSizeX = state.GetProperty("LocalSizeX").GetUInt32(),
|
|
LocalSizeY = state.GetProperty("LocalSizeY").GetUInt32(),
|
|
LocalSizeZ = state.GetProperty("LocalSizeZ").GetUInt32(),
|
|
ThreadCountX = state.GetProperty("ThreadCountX").GetUInt32(),
|
|
ThreadCountY = state.GetProperty("ThreadCountY").GetUInt32(),
|
|
ThreadCountZ = state.GetProperty("ThreadCountZ").GetUInt32(),
|
|
};
|
|
if (!Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out error))
|
|
throw new InvalidOperationException(error);
|
|
File.WriteAllBytes(args[3], shader.Spirv);
|
|
Console.WriteLine($"hash=0x{hash:X16} instructions={program.Instructions.Count} spirv_bytes={shader.Spirv.Length}");
|
|
return;
|
|
}
|
|
|
|
if (args.Length >= 1 && string.Equals(args[0], "--inspect", StringComparison.Ordinal))
|
|
{
|
|
if (args.Length is < 2 or > 3)
|
|
{
|
|
Console.Error.WriteLine(
|
|
"Usage: SharpEmu.Tools.ShaderDump --inspect <shader.bin> [byte-count]");
|
|
Environment.ExitCode = 2;
|
|
return;
|
|
}
|
|
|
|
var inputPath = Path.GetFullPath(args[1]);
|
|
var input = File.ReadAllBytes(inputPath);
|
|
var requestedByteCount = input.Length;
|
|
if (args.Length >= 3)
|
|
{
|
|
var value = args[2];
|
|
requestedByteCount = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
|
? Convert.ToInt32(value[2..], 16)
|
|
: Convert.ToInt32(value, 10);
|
|
}
|
|
|
|
if (requestedByteCount <= 0 ||
|
|
requestedByteCount > input.Length ||
|
|
requestedByteCount % sizeof(uint) != 0)
|
|
{
|
|
Console.Error.WriteLine(
|
|
$"Invalid byte count {requestedByteCount}; expected a positive, " +
|
|
$"4-byte-aligned value no larger than {input.Length}.");
|
|
Environment.ExitCode = 2;
|
|
return;
|
|
}
|
|
|
|
var words = new uint[requestedByteCount / sizeof(uint)];
|
|
for (var index = 0; index < words.Length; index++)
|
|
{
|
|
words[index] = BinaryPrimitives.ReadUInt32LittleEndian(
|
|
input.AsSpan(index * sizeof(uint), sizeof(uint)));
|
|
}
|
|
|
|
var memory = new FakeMemory();
|
|
memory.AddRegion(ProgramAddress, words);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
if (!Gen5ShaderTranslator.TryDecodeProgram(
|
|
ctx,
|
|
ProgramAddress,
|
|
out var program,
|
|
out var decodeError))
|
|
{
|
|
Console.Error.WriteLine($"Decode failed: {decodeError}");
|
|
Environment.ExitCode = 1;
|
|
return;
|
|
}
|
|
|
|
Console.WriteLine(
|
|
$"path={inputPath} bytes={requestedByteCount} " +
|
|
$"instructions={program!.Instructions.Count}");
|
|
foreach (var instruction in program.Instructions)
|
|
{
|
|
Console.WriteLine(
|
|
$"pc=0x{instruction.Pc:X} enc={instruction.Encoding} " +
|
|
$"op={instruction.Opcode} " +
|
|
$"words={string.Join(',', instruction.Words.Select(word => $"{word:X8}"))} " +
|
|
$"src={string.Join('/', instruction.Sources)} " +
|
|
$"dst={string.Join('/', instruction.Destinations)} " +
|
|
$"control={instruction.Control?.ToString() ?? "-"}");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
(string Name, bool ExpectTranslate, uint[] Words)[] testPrograms =
|
|
[
|
|
("fmac", true, [
|
|
0x560A0501, // v_fmac_f32 v5, v1, v2
|
|
0x580A0501, 0x42280000, // v_fmamk_f32 v5, v1, 42.0, v2
|
|
0x5A0A0501, 0x42280000, // v_fmaak_f32 v5, v1, v2, 42.0
|
|
0xD52B0005, 0x00020501, // v_fmac_f32_e64 v5, v1, v2
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
("muls", true, [
|
|
0xD5690005, 0x00020501, // v_mul_lo_u32 v5, v1, v2
|
|
0xD56A0005, 0x00020501, // v_mul_hi_u32 v5, v1, v2
|
|
0xD56B0005, 0x00020501, // v_mul_lo_i32 v5, v1, v2
|
|
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
// Packed f16 (VOP3P) arithmetic, including the fused multiply-add. The
|
|
// constants pin the double-rounding regression from the VOP3P first slice:
|
|
// fma(0x4100, 0x7522, 0x04EA) must round once to 0x7A6B (an f32
|
|
// multiply-add then pack yields 0x7A6A). The last fma exercises the src2
|
|
// neg_lo/neg_hi modifier path.
|
|
("pk-f16", true, [
|
|
0x7E0002FF, 0x41004100, // v_mov_b32 v0, 0x41004100 (2.5 packed)
|
|
0x7E0202FF, 0x75227522, // v_mov_b32 v1, 0x75227522 (21024 packed)
|
|
0x7E0402FF, 0x04EA04EA, // v_mov_b32 v2, 0x04EA04EA (~7.496e-5 packed)
|
|
0xCC0E4003, 0x1C0A0300, // v_pk_fma_f16 v3, v0, v1, v2
|
|
0xCC0F4004, 0x18020500, // v_pk_add_f16 v4, v0, v2
|
|
0xCC104005, 0x18020300, // v_pk_mul_f16 v5, v0, v1
|
|
0xCC114006, 0x18020300, // v_pk_min_f16 v6, v0, v1
|
|
0xCC124007, 0x18020300, // v_pk_max_f16 v7, v0, v1
|
|
0xCC0E4408, 0x9C0A0300, // v_pk_fma_f16 v8, v0, v1, neg_lo:[0,0,1] neg_hi:[0,0,1] v2
|
|
0xCC0FC009, 0x18020000, // v_pk_add_f16 v9, v0, v0 clamp (2.5+2.5=5 -> saturates to 1.0)
|
|
0xCC0EC40A, 0x1C0A0300, // v_pk_fma_f16 v10, v0, v1, v2 clamp
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
("mrt", true, [
|
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
|
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
|
0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f
|
|
0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f
|
|
0x7E0802FF, 0x00000001, // v_mov_b32 v4, 1u
|
|
0x7E0A02FF, 0x00000002, // v_mov_b32 v5, 2u
|
|
0x7E0C02FF, 0x00000003, // v_mov_b32 v6, 3u
|
|
0x7E0E02FF, 0x00000004, // v_mov_b32 v7, 4u
|
|
0x7E1002FF, 0xFFFFFFFF, // v_mov_b32 v8, -1
|
|
0x7E1202FF, 0x00000002, // v_mov_b32 v9, 2
|
|
0x7E1402FF, 0xFFFFFFFD, // v_mov_b32 v10, -3
|
|
0x7E1602FF, 0x00000004, // v_mov_b32 v11, 4
|
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
|
0xF800003F, 0x07060504, // exp mrt3 v4, v5, v6, v7
|
|
0xF800086F, 0x0B0A0908, // exp mrt6 v8, v9, v10, v11 done
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
("mrt-float2", true, [
|
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
|
0x7E0202FF, 0x3E800000, // v_mov_b32 v1, 0.25f
|
|
0x7E0402FF, 0x3E800000, // v_mov_b32 v2, 0.25f
|
|
0x7E0602FF, 0x3F000000, // v_mov_b32 v3, 0.5f
|
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
|
0xF800081F, 0x03020100, // exp mrt1 v0, v1, v2, v3 done
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
("mrt8", true, [
|
|
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
|
|
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
|
|
0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f
|
|
0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f
|
|
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
|
|
0xF800001F, 0x03020100, // exp mrt1 v0, v1, v2, v3
|
|
0xF800002F, 0x03020100, // exp mrt2 v0, v1, v2, v3
|
|
0xF800003F, 0x03020100, // exp mrt3 v0, v1, v2, v3
|
|
0xF800004F, 0x03020100, // exp mrt4 v0, v1, v2, v3
|
|
0xF800005F, 0x03020100, // exp mrt5 v0, v1, v2, v3
|
|
0xF800006F, 0x03020100, // exp mrt6 v0, v1, v2, v3
|
|
0xF800087F, 0x03020100, // exp mrt7 v0, v1, v2, v3 done
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
("mrt-partial", true, [
|
|
0x7E0002FF, 0x3F4CCCCD, // v_mov_b32 v0, 0.8f
|
|
0x7E0202FF, 0x3F333333, // v_mov_b32 v1, 0.7f
|
|
0xF8000803, 0x03020100, // exp mrt0 v0, v1, off, off done
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
("mrt-partial-merge", true, [
|
|
0x7E0002FF, 0x3DCCCCCD, // v_mov_b32 v0, 0.1f
|
|
0x7E0202FF, 0x3E4CCCCD, // v_mov_b32 v1, 0.2f
|
|
0x7E0C02FF, 0x3E99999A, // v_mov_b32 v6, 0.3f
|
|
0x7E0E02FF, 0x3ECCCCCD, // v_mov_b32 v7, 0.4f
|
|
0xF8000003, 0x03020100, // exp mrt0 v0, v1, off, off
|
|
0xF800080C, 0x07060504, // exp mrt0 off, off, v6, v7 done
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
("sopp-hints", true, [
|
|
0xBFA10001, // s_clause 0x1
|
|
0xBFA30000, // s_waitcnt_depctr 0x0
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
// s_round_mode / s_denorm_mode write the FP MODE state and must keep
|
|
// failing decode loudly until their semantics are modeled (see #108);
|
|
// this program pins that behavior.
|
|
("sopp-mode", false, [
|
|
0xBFA40000, // s_round_mode 0x0
|
|
0xBFA50000, // s_denorm_mode 0x0
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
// Executable end-to-end test: compute with real ALU instructions, then
|
|
// buffer_store_dword results to guestBuffers[0] at offsets 0/4/8, prove
|
|
// that a store with EXEC=0 does not land (offset 12 stays sentinel), and
|
|
// that stores work again after EXEC is restored (offset 16). Offsets 20/24
|
|
// hold the packed fused f16 FMA and its negated-addend twin, whose exact
|
|
// results (0x7A6B7A6B / 0x7A6A7A6A) straddle an f16 midpoint and therefore
|
|
// catch any double-rounding regression on real hardware.
|
|
("exec", true, [
|
|
0xBFA10001, // s_clause 0x1 (hint no-op in an executed program, needs #108)
|
|
0x7E0002FF, 0x3FC00000, // v_mov_b32 v0, 1.5f
|
|
0x7E0202FF, 0x40100000, // v_mov_b32 v1, 2.25f
|
|
0x7E0402FF, 0x41200000, // v_mov_b32 v2, 10.0f
|
|
0x56040300, // v_fmac_f32 v2, v0, v1 -> v2 = fma(1.5, 2.25, 10.0)
|
|
0x7E0602FF, 0x7FFFFFFF, // v_mov_b32 v3, 0x7FFFFFFF
|
|
0x7E0802FF, 0x00010003, // v_mov_b32 v4, 0x00010003
|
|
0xD56C0005, 0x00020903, // v_mul_hi_i32 v5, v3, v4
|
|
0xD56B0006, 0x00020903, // v_mul_lo_i32 v6, v3, v4
|
|
0xE0700000, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0
|
|
0xE0700004, 0x80020500, // buffer_store_dword v5, off, s[8:11], 0 offset:4
|
|
0xE0700008, 0x80020600, // buffer_store_dword v6, off, s[8:11], 0 offset:8
|
|
0xBEFE0380, // s_mov_b32 exec_lo, 0 -> lane inactive
|
|
0xE070000C, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0 offset:12 (masked, must not land)
|
|
0xBEFE03C1, // s_mov_b32 exec_lo, -1 -> lane active again
|
|
0xE0700010, 0x80020000, // buffer_store_dword v0, off, s[8:11], 0 offset:16
|
|
0x7E0E02FF, 0x41004100, // v_mov_b32 v7, 0x41004100 (2.5 packed)
|
|
0x7E1002FF, 0x75227522, // v_mov_b32 v8, 0x75227522 (21024 packed)
|
|
0x7E1202FF, 0x04EA04EA, // v_mov_b32 v9, 0x04EA04EA (~7.496e-5 packed)
|
|
0xCC0E400A, 0x1C261107, // v_pk_fma_f16 v10, v7, v8, v9
|
|
0xCC0E440B, 0x9C261107, // v_pk_fma_f16 v11, v7, v8, neg_lo:[0,0,1] neg_hi:[0,0,1] v9
|
|
0xCC0EC00C, 0x1C261107, // v_pk_fma_f16 v12, v7, v8, v9 clamp (>=1 -> saturates to 1.0)
|
|
0xE0700014, 0x80020A00, // buffer_store_dword v10, off, s[8:11], 0 offset:20
|
|
0xE0700018, 0x80020B00, // buffer_store_dword v11, off, s[8:11], 0 offset:24
|
|
0xE070001C, 0x80020C00, // buffer_store_dword v12, off, s[8:11], 0 offset:28
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
// Typed accesses use the instruction format (32_32 float, then an unknown format
|
|
// that reads raw dwords, then an 8_8 store) while the descriptor in s11 says unorm bytes.
|
|
("typed-load", true, [
|
|
0xBE8B03FF, 0x00038FAC, // s_mov_b32 s11, 0x38FAC (descriptor format 56, swizzle xyzw)
|
|
0xEA010000, 0x80020200, // tbuffer_load_format_xy v[2:3], off, s[8:11], 0 format:64
|
|
0xE9F00008, 0x80020400, // tbuffer_load_format_x v4, off, s[8:11], 0 format:62 offset:8
|
|
0xE0700010, 0x80020200, // buffer_store_dword v2, off, s[8:11], 0 offset:16
|
|
0xE0700014, 0x80020300, // buffer_store_dword v3, off, s[8:11], 0 offset:20
|
|
0xE0700018, 0x80020400, // buffer_store_dword v4, off, s[8:11], 0 offset:24
|
|
0xE875001C, 0x80020200, // tbuffer_store_format_xy v[2:3], off, s[8:11], 0 format:14 offset:28
|
|
0xBF810000, // s_endpgm
|
|
]),
|
|
];
|
|
|
|
var outputDirectory = args.Length > 0
|
|
? args[0]
|
|
: Path.Combine(AppContext.BaseDirectory, "spv");
|
|
Directory.CreateDirectory(outputDirectory);
|
|
|
|
var failures = 0;
|
|
var mslLimitations = 0;
|
|
foreach (var (name, expectTranslate, words) in testPrograms)
|
|
{
|
|
var memory = new FakeMemory();
|
|
memory.AddRegion(ProgramAddress, words);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
|
|
Console.WriteLine(
|
|
$"[{name}] decode: " +
|
|
Gen5ShaderTranslator.Describe(ctx, ProgramAddress, ProgramAddress));
|
|
|
|
if (!Gen5ShaderTranslator.TryDecodeProgram(ctx, ProgramAddress, out var program, out var decodeError))
|
|
{
|
|
if (expectTranslate)
|
|
{
|
|
failures++;
|
|
Console.WriteLine($"[{name}] FAILED: decode error ({decodeError})");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"[{name}] decode failed as expected ({decodeError})");
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (!expectTranslate)
|
|
{
|
|
failures++;
|
|
Console.WriteLine(
|
|
$"[{name}] FAILED: decoded successfully but is pinned as a decode failure — " +
|
|
"if the new decode support is intentional, its semantics need verifying here first");
|
|
continue;
|
|
}
|
|
|
|
Gen5PixelOutputBinding[] pixelOutputs = name switch
|
|
{
|
|
"mrt" =>
|
|
[
|
|
new(0, 0, Gen5PixelOutputKind.Float),
|
|
new(3, 1, Gen5PixelOutputKind.Uint),
|
|
new(6, 2, Gen5PixelOutputKind.Sint),
|
|
],
|
|
"mrt-float2" =>
|
|
[
|
|
new(0, 0, Gen5PixelOutputKind.Float),
|
|
new(1, 1, Gen5PixelOutputKind.Float),
|
|
],
|
|
"mrt8" => Enumerable.Range(0, 8)
|
|
.Select(index => new Gen5PixelOutputBinding((uint)index, (uint)index, Gen5PixelOutputKind.Float)).ToArray(),
|
|
_ when name.StartsWith("mrt", StringComparison.Ordinal) => [new(0, 0, Gen5PixelOutputKind.Float)],
|
|
_ => [],
|
|
};
|
|
var stages = new List<(string Suffix, ShaderStage Stage)>
|
|
{
|
|
("cs", ShaderStage.Compute),
|
|
("vs", ShaderStage.Vertex),
|
|
};
|
|
if (pixelOutputs.Length != 0)
|
|
{
|
|
stages.Add(("ps", ShaderStage.Pixel));
|
|
}
|
|
|
|
foreach (var (suffix, stage) in stages)
|
|
{
|
|
ShaderCompileRequest request;
|
|
try
|
|
{
|
|
request = CreateCompileRequest(program!, stage, stage == ShaderStage.Pixel ? pixelOutputs : []);
|
|
}
|
|
catch (ResourcePlanException exception)
|
|
{
|
|
failures++;
|
|
Console.WriteLine($"[{name}] {suffix}: plan refused ({exception.Message})");
|
|
continue;
|
|
}
|
|
|
|
if (Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var shaderError))
|
|
{
|
|
var path = Path.Combine(outputDirectory, stage == ShaderStage.Vertex ? $"{name}.spv" : $"{name}-{suffix}.spv");
|
|
File.WriteAllBytes(path, shader.Spirv);
|
|
// Keep existing file names for validation scripts and external tooling.
|
|
File.WriteAllBytes(Path.Combine(outputDirectory, $"{name}-layout-{suffix}.spv"), shader.Spirv);
|
|
Console.WriteLine($"[{name}] {suffix} emit: success, {shader.Spirv.Length} bytes -> {path}");
|
|
}
|
|
else
|
|
{
|
|
failures++;
|
|
Console.WriteLine($"[{name}] {suffix} emit: FAILED ({shaderError})");
|
|
}
|
|
|
|
if (Gen5MslTranslator.TryCompileProgram(request, out var metalShader, out var metalError))
|
|
{
|
|
var path = Path.Combine(outputDirectory, $"{name}-layout-{suffix}.msl");
|
|
File.WriteAllText(path, metalShader.Source);
|
|
Console.WriteLine($"[{name}] {suffix} msl: success, {metalShader.Source.Length} chars -> {path}");
|
|
}
|
|
else if (IsExpectedMetalLimitation(name, metalError))
|
|
{
|
|
mslLimitations++;
|
|
Console.WriteLine($"[{name}] {suffix} msl: limitation ({metalError})");
|
|
}
|
|
else
|
|
{
|
|
failures++;
|
|
Console.WriteLine($"[{name}] {suffix} msl: FAILED ({metalError})");
|
|
}
|
|
}
|
|
|
|
if (name == "mrt")
|
|
{
|
|
var invalidRequest = CreateCompileRequest(program!, ShaderStage.Pixel,
|
|
[new(0, 0, Gen5PixelOutputKind.Float), new(3, 7, Gen5PixelOutputKind.Float)]);
|
|
if (Gen5SpirvTranslator.TryCompileProgram(invalidRequest, out _, out var invalidError))
|
|
{
|
|
failures++;
|
|
Console.WriteLine("[mrt] FAILED: sparse host locations were accepted");
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidError})");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hand-assembled fixed shaders pass through the same validator gate.
|
|
var faultShader = SpirvFixedShaders.CreateFaultBufferProcess();
|
|
var faultPath = Path.Combine(outputDirectory, "fault-buffer-process-cs.spv");
|
|
File.WriteAllBytes(faultPath, faultShader);
|
|
Console.WriteLine($"[fault-buffer-process] emit: {faultShader.Length} bytes -> {faultPath}");
|
|
|
|
// The tiler, depth-conversion, channel-swap and blit modules are assembled per variant.
|
|
var fixedModules = new List<(string Name, byte[] Spirv)>();
|
|
foreach (var shape in Enum.GetValues<TilerBlockShape>())
|
|
{
|
|
foreach (var bytes in new uint[] { 1, 2, 4, 8, 16 })
|
|
{
|
|
fixedModules.Add(($"tiler-{shape.ToString().ToLowerInvariant()}-{bytes}-detile", TilerShaders.CreateBlockCopy(shape, bytes, toTiled: false)));
|
|
fixedModules.Add(($"tiler-{shape.ToString().ToLowerInvariant()}-{bytes}-tile", TilerShaders.CreateBlockCopy(shape, bytes, toTiled: true)));
|
|
}
|
|
}
|
|
|
|
fixedModules.Add(("d16-widen-d24", TilerShaders.CreateDepthWiden(d32: false)));
|
|
fixedModules.Add(("d16-widen-d32", TilerShaders.CreateDepthWiden(d32: true)));
|
|
fixedModules.Add(("d16-narrow-d24", TilerShaders.CreateDepthNarrow(d32: false)));
|
|
fixedModules.Add(("d16-narrow-d32", TilerShaders.CreateDepthNarrow(d32: true)));
|
|
fixedModules.Add(("bgra16-swap", TilerShaders.CreateBgra16Swap()));
|
|
fixedModules.Add(("blit-vert", BlitShaders.CreateFullscreenTriangleVertex()));
|
|
fixedModules.Add(("blit-ms-depth-frag", BlitShaders.CreateColorToMultisampleDepthFragment()));
|
|
fixedModules.Add(("ms-depth-sample-readback-cs", BlitShaders.CreateMultisampleDepthSampleReadback()));
|
|
foreach (var (moduleName, spirv) in fixedModules)
|
|
{
|
|
var modulePath = Path.Combine(outputDirectory, $"{moduleName}.spv");
|
|
File.WriteAllBytes(modulePath, spirv);
|
|
Console.WriteLine($"[{moduleName}] emit: {spirv.Length} bytes -> {modulePath}");
|
|
}
|
|
|
|
Console.WriteLine($"MSL limitations: {mslLimitations}");
|
|
Console.WriteLine(failures == 0
|
|
? "RESULT: all programs behaved as expected"
|
|
: $"RESULT: {failures} unexpected outcome(s)");
|
|
Environment.ExitCode = failures == 0 ? 0 : 1;
|
|
|
|
static ShaderCompileRequest CreateCompileRequest(
|
|
Gen5ShaderProgram program, ShaderStage stage, Gen5PixelOutputBinding[] outputs)
|
|
{
|
|
var plan = ShaderResourcePlan.Extract(program, stage, 0x5348_4152_5045_4D55, 0, 16);
|
|
var resources = ResourceMaterializer.ApplyTo(plan, ResourceSpecialization.Default(plan.Info));
|
|
var layout = BindingLayout.Allocate(resources.Info,
|
|
BindingLayout.CollectUserDataRegisters(program, 0, 16),
|
|
BindingLayout.UsesGlobalDataShare(program),
|
|
ShaderCompileRequest.RequiresFlattenedTable(plan, resources),
|
|
BindingLayout.ReadsShaderBase(program));
|
|
return new ShaderCompileRequest(plan, resources, layout)
|
|
{
|
|
PixelOutputs = outputs,
|
|
ThreadCountX = 1,
|
|
ThreadCountY = 1,
|
|
ThreadCountZ = 1,
|
|
};
|
|
}
|
|
|
|
// Only these measured fixture limitations are accepted; other emission failures fail the run.
|
|
static bool IsExpectedMetalLimitation(string programName, string error) =>
|
|
(programName is "pk-f16" or "exec" && error.Contains("unsupported vector opcode VPkFmaF16", StringComparison.Ordinal)) ||
|
|
(programName == "sopp-hints" && error.Contains("SWaitcntDepctr: missing scalar destination", StringComparison.Ordinal));
|
|
|
|
internal sealed class FakeMemory : ICpuMemory
|
|
{
|
|
private readonly List<(ulong Base, byte[] Data)> _regions = [];
|
|
|
|
public void AddRegion(ulong baseAddress, uint[] words)
|
|
{
|
|
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]);
|
|
}
|
|
|
|
_regions.Add((baseAddress, bytes));
|
|
}
|
|
|
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
|
{
|
|
foreach (var (baseAddress, data) in _regions)
|
|
{
|
|
if (virtualAddress >= baseAddress &&
|
|
virtualAddress + (ulong)destination.Length <= baseAddress + (ulong)data.Length)
|
|
{
|
|
data.AsSpan(
|
|
(int)(virtualAddress - baseAddress),
|
|
destination.Length).CopyTo(destination);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) => false;
|
|
}
|