sharpemu/tools/SharpEmu.Tools.ShaderDump/Program.cs

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;
}