// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later // Synthetic-shader conformance dumper. // // Feeds hand-assembled Gen5 (gfx10) instruction words through the real // decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via // reflection so no emulator source changes are required) and writes the // resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs can then be // checked with spirv-val / spirv-dis. // // Programs that contain buffer_store_dword automatically get a single // global-memory binding covering every store, which the emitter exposes as // guestBuffers[0] (descriptor set 0, binding 0). // // Each program carries an expectation: ExpectTranslate=true programs must // decode and emit the requested stages; ExpectTranslate=false programs pin a decode // failure that must stay loud. Any unexpected outcome makes the tool exit // non-zero, so it can gate scripts/CI. // // Usage: SharpEmu.Tools.ShaderDump [output-directory] using System.Buffers.Binary; using System.Reflection; using SharpEmu.HLE; using SharpEmu.Libs.CxxAbi; const ulong ProgramAddress = 0x100000; (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 ]), ("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). ("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 0xBF810000, // s_endpgm ]), ]; var assembly = typeof(CxaGuardExports).Assembly; var shaderTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderTranslator") ?? throw new InvalidOperationException("Gen5ShaderTranslator not found"); var spirvTranslator = assembly.GetType("SharpEmu.Libs.Agc.Gen5SpirvTranslator") ?? throw new InvalidOperationException("Gen5SpirvTranslator not found"); var describe = shaderTranslator.GetMethod( "Describe", BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException("Gen5ShaderTranslator.Describe not found"); var tryDecode = shaderTranslator.GetMethod( "TryDecodeProgram", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("Gen5ShaderTranslator.TryDecodeProgram not found"); var stateType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderState") ?? throw new InvalidOperationException("Gen5ShaderState not found"); var evaluationType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ShaderEvaluation") ?? throw new InvalidOperationException("Gen5ShaderEvaluation not found"); var imageBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ImageBinding") ?? throw new InvalidOperationException("Gen5ImageBinding not found"); var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding") ?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found"); var pixelOutputBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputBinding") ?? throw new InvalidOperationException("Gen5PixelOutputBinding not found"); var pixelOutputKindType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputKind") ?? throw new InvalidOperationException("Gen5PixelOutputKind not found"); var tryCompile = spirvTranslator.GetMethod( "TryCompileVertexShader", BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found"); var tryCompilePixel = spirvTranslator.GetMethods(BindingFlags.Public | BindingFlags.Static) .Single(method => method.Name == "TryCompilePixelShader" && method.GetParameters()[2].ParameterType.IsGenericType); var tryCompileCompute = spirvTranslator.GetMethod( "TryCompileComputeShader", BindingFlags.Public | BindingFlags.Static) ?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileComputeShader not found"); var outputDirectory = args.Length > 0 ? args[0] : Path.Combine(AppContext.BaseDirectory, "spv"); Directory.CreateDirectory(outputDirectory); var failures = 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: " + (string)describe.Invoke(null, [ctx, ProgramAddress, ProgramAddress])!); object?[] decodeArgs = [ctx, ProgramAddress, null, null]; if (!(bool)tryDecode.Invoke(null, decodeArgs)!) { if (expectTranslate) { failures++; Console.WriteLine($"[{name}] FAILED: decode error ({decodeArgs[3]})"); } else { Console.WriteLine($"[{name}] decode failed as expected ({decodeArgs[3]})"); } 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; } // Buffer stores need a global-memory binding; the emitter resolves them by // instruction PC, so collect store PCs from the decoded program itself. var programObj = decodeArgs[2]!; var instructions = (System.Collections.IEnumerable)programObj .GetType().GetProperty("Instructions")!.GetValue(programObj)!; var storePcs = new List(); foreach (var instruction in instructions) { var op = (string)instruction.GetType().GetProperty("Opcode")!.GetValue(instruction)!; if (op.StartsWith("BufferStore", StringComparison.Ordinal)) { storePcs.Add((uint)instruction.GetType().GetProperty("Pc")!.GetValue(instruction)!); } } // The binding's scalar base (8 -> s[8:11]) must match the srsrc field of // the hand-assembled buffer_store words, and the 64-byte backing store // must cover every hand-assembled store offset. var globalBindings = Array.CreateInstance(globalBindingType, storePcs.Count > 0 ? 1 : 0); if (storePcs.Count > 0) { globalBindings.SetValue( Activator.CreateInstance( globalBindingType, 8u, 0UL, (IReadOnlyList)storePcs, new byte[64]), 0); } var state = Activator.CreateInstance( stateType, programObj, new uint[16], null, null, 0u)!; var evaluation = Activator.CreateInstance( evaluationType, new uint[256], new uint[256], new Dictionary>(), Array.CreateInstance(imageBindingType, 0), globalBindings, null, null, null)!; var compileArgs = PadWithDefaults(tryCompile, [state, evaluation, null, null]); if ((bool)tryCompile.Invoke(null, BindingFlags.OptionalParamBinding, null, compileArgs, null)!) { var shader = compileArgs[2]!; var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!; var path = Path.Combine(outputDirectory, $"{name}.spv"); File.WriteAllBytes(path, spirv); Console.WriteLine($"[{name}] emit: success, {spirv.Length} bytes -> {path}"); } else { failures++; Console.WriteLine($"[{name}] emit: FAILED ({compileArgs[3]})"); } var computeArgs = PadWithDefaults(tryCompileCompute, [state, evaluation, 1u, 1u, 1u, null, null]); if ((bool)tryCompileCompute.Invoke(null, BindingFlags.OptionalParamBinding, null, computeArgs, null)!) { var shader = computeArgs[5]!; var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!; var path = Path.Combine(outputDirectory, $"{name}-cs.spv"); File.WriteAllBytes(path, spirv); Console.WriteLine($"[{name}] compute emit: success, {spirv.Length} bytes -> {path}"); } else { failures++; Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})"); } if (name.StartsWith("mrt", StringComparison.Ordinal)) { (uint GuestSlot, uint HostLocation, string Kind)[] outputSpecs = name switch { "mrt" => new (uint GuestSlot, uint HostLocation, string Kind)[] { (0, 0, "Float"), (3, 1, "Uint"), (6, 2, "Sint"), }, "mrt-float2" => [(0, 0, "Float"), (1, 1, "Float")], "mrt8" => Enumerable.Range(0, 8) .Select(index => ((uint)index, (uint)index, "Float")) .ToArray(), _ => [(0, 0, "Float")], }; var pixelOutputs = Array.CreateInstance(pixelOutputBindingType, outputSpecs.Length); for (var index = 0; index < outputSpecs.Length; index++) { var spec = outputSpecs[index]; pixelOutputs.SetValue( Activator.CreateInstance( pixelOutputBindingType, spec.GuestSlot, spec.HostLocation, Enum.Parse(pixelOutputKindType, spec.Kind)), index); } var pixelArgs = PadWithDefaults( tryCompilePixel, [state, evaluation, pixelOutputs, null, null]); if ((bool)tryCompilePixel.Invoke( null, BindingFlags.OptionalParamBinding, null, pixelArgs, null)!) { var shader = pixelArgs[3]!; var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!; var path = Path.Combine(outputDirectory, $"{name}-ps.spv"); File.WriteAllBytes(path, spirv); Console.WriteLine($"[{name}] pixel emit: success, {spirv.Length} bytes -> {path}"); } else { failures++; Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelArgs[4]})"); } if (name == "mrt") { var invalidOutputs = Array.CreateInstance(pixelOutputBindingType, 2); invalidOutputs.SetValue( Activator.CreateInstance( pixelOutputBindingType, 0u, 0u, Enum.Parse(pixelOutputKindType, "Float")), 0); invalidOutputs.SetValue( Activator.CreateInstance( pixelOutputBindingType, 3u, 7u, Enum.Parse(pixelOutputKindType, "Float")), 1); var invalidPixelArgs = PadWithDefaults( tryCompilePixel, [state, evaluation, invalidOutputs, null, null]); if ((bool)tryCompilePixel.Invoke( null, BindingFlags.OptionalParamBinding, null, invalidPixelArgs, null)!) { failures++; Console.WriteLine("[mrt] FAILED: sparse host locations were accepted"); } else { Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidPixelArgs[4]})"); } } } } Console.WriteLine(failures == 0 ? "RESULT: all programs behaved as expected" : $"RESULT: {failures} unexpected outcome(s)"); Environment.ExitCode = failures == 0 ? 0 : 1; // Reflection Invoke does not apply C# default parameter values, so a newly // added optional parameter on a translator entry point would otherwise throw // TargetParameterCountException. Type.Missing + OptionalParamBinding lets the // runtime substitute the declared defaults; only a new *required* parameter // should force a tool update. static object?[] PadWithDefaults(MethodInfo method, object?[] arguments) { var parameters = method.GetParameters(); if (arguments.Length > parameters.Length) { throw new InvalidOperationException( $"{method.DeclaringType?.Name}.{method.Name} takes fewer parameters than the tool supplies"); } var padded = new object?[parameters.Length]; arguments.CopyTo(padded, 0); for (var i = arguments.Length; i < padded.Length; i++) { if (!parameters[i].IsOptional) { throw new InvalidOperationException( $"{method.DeclaringType?.Name}.{method.Name} gained a required parameter " + $"'{parameters[i].Name}' — the tool needs updating"); } padded[i] = Type.Missing; } return padded; } 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; }