// 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 SharpEmu.HLE; using SharpEmu.ShaderCompiler; using SharpEmu.ShaderCompiler.Metal; using SharpEmu.ShaderCompiler.Resources; using SharpEmu.ShaderCompiler.Vulkan; const ulong ProgramAddress = 0x100000; 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 [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()) { 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 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 source) => false; }