// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using System.Globalization; using System.Text; using SharpEmu.ShaderCompiler; namespace SharpEmu.ShaderCompiler.Metal; /// /// Gen5 (gfx10) -> Metal Shading Language codegen. Consumes the backend-neutral /// (Gen5ShaderState, Gen5ShaderEvaluation) contract out of Gen5ShaderTranslator / /// Gen5ShaderScalarEvaluator and emits MSL source text; the Metal renderer compiles /// it with MTLLibrary at bind time. /// /// The execution model mirrors Gen5SpirvTranslator: one GPU invocation is one GCN /// lane (wave32 — natively the Apple simdgroup width), the register file is typeless /// 32-bit uints (float ALU bitcasts through as_type<float>), and control flow is a /// PC-dispatcher loop — a bounded while over a switch of GCN basic blocks — rather /// than reconstructed structured control flow. EXEC/VCC live in their architectural /// SGPRs (s106/s107, s126/s127) as raw data, with per-lane bools as synced views. /// Graphics stages model a single logical wave lane (lane 0, ballots degrade to bit /// 0, shuffle-family selects resolve to the lane's own value) — the SPIR-V /// translator's no-subgroup fallback — because Metal leaves simdgroup ops undefined /// inside the divergent dispatcher loop. Compute threads map one-to-one onto real /// simdgroup lanes and shuffle for real. /// /// Wave64: 64-bit masks are carried faithfully as data — every B64 mask op, /// saveexec, and VCCZ/EXECZ test reads and writes the full register pair. A /// wave64 guest wave is two 32-wide Apple simdgroups co-resident in one /// threadgroup; cross-lane ops that span the full 64 lanes (ballots into /// EXEC/VCC, read-first-lane) rendezvous the two halves through threadgroup /// scratch with a barrier — the guest's scalar PC keeps all 64 lanes lockstep /// through the dispatcher, so the barriers are reached uniformly. This mirrors /// the SPIR-V translator's bridge and shares its scope: the scratch is indexed /// by half, so it is correct for a one-wave (64-thread) workgroup; readlane /// across halves stays a 32-wide shuffle (same as the SPIR-V path). Wave- /// agnostic wave64 kernels translate per-thread unchanged. /// /// Buffer argument contract (documented for the Metal backend): /// [[buffer(globalBufferBase + i)]] global memory binding i, in /// Gen5ShaderEvaluation.GlobalMemoryBindings order /// [[buffer(uniformsIndex)]] one SharpEmuUniforms constant buffer holding the /// compute dispatch limit and per-buffer byte /// lengths, where uniformsIndex is /// globalBufferBase + totalGlobalBufferCount /// public static partial class Gen5MslTranslator { private const uint ScalarRegisterFileCount = 128; private const uint VectorRegisterFileCount = 256; private const uint LdsDwordCount = 8192; private const uint LdsDwordMask = LdsDwordCount - 1; // Graphics stages model LDS as per-invocation scratch; a full 32 KB array // per fragment/vertex invocation risks Metal compile limits, and // per-invocation write-then-read correctness only needs deterministic // address masking (mirrors the SPIR-V translator's Private-array choice). private const uint PrivateLdsDwordCount = 2048; private const uint VccLoRegister = 106; private const uint VccHiRegister = 107; private const uint ExecLoRegister = 126; private const uint ExecHiRegister = 127; public static bool TryCompilePixelShader( Gen5ShaderState state, Gen5ShaderEvaluation evaluation, Gen5PixelOutputKind outputKind, out Gen5MslShader shader, out string error, int globalBufferBase = 0, int totalGlobalBufferCount = -1, int imageBindingBase = 0, int initialScalarBufferIndex = -1, int pixelRenderTargetSlot = 0, uint pixelInputEnable = 0, uint pixelInputAddress = 0, IReadOnlyList? pixelInputCntl = null, ulong storageBufferOffsetAlignment = 1) => TryCompilePixelShader( state, evaluation, [new Gen5PixelOutputBinding((uint)pixelRenderTargetSlot, 0, outputKind)], out shader, out error, globalBufferBase, totalGlobalBufferCount, imageBindingBase, initialScalarBufferIndex, pixelInputEnable, pixelInputAddress, pixelInputCntl, storageBufferOffsetAlignment); public static bool TryCompilePixelShader( Gen5ShaderState state, Gen5ShaderEvaluation evaluation, IReadOnlyList outputs, out Gen5MslShader shader, out string error, int globalBufferBase = 0, int totalGlobalBufferCount = -1, int imageBindingBase = 0, int initialScalarBufferIndex = -1, uint pixelInputEnable = 0, uint pixelInputAddress = 0, IReadOnlyList? pixelInputCntl = null, ulong storageBufferOffsetAlignment = 1) { shader = default!; error = string.Empty; if (outputs.Count > 8) { error = "pixel outputs must contain at most eight guest slots in the 0..7 range"; return false; } for (var index = 0; index < outputs.Count; index++) { if (outputs[index].GuestSlot > 7) { error = "pixel outputs must contain at most eight guest slots in the 0..7 range"; return false; } for (var other = index + 1; other < outputs.Count; other++) { if (outputs[other].GuestSlot == outputs[index].GuestSlot || outputs[other].HostLocation == outputs[index].HostLocation) { error = "pixel output guest slots and host locations must be unique"; return false; } } } // Host locations must be dense 0..N-1 so [[color(n)]] attachments match. for (uint location = 0; location < outputs.Count; location++) { var found = false; foreach (var output in outputs) { found |= output.HostLocation == location; } if (!found) { error = "pixel output host locations must be dense in the 0..N-1 range"; return false; } } var context = new CompilationContext( Gen5MslStage.Pixel, state, evaluation, 1, 1, 1, globalBufferBase, totalGlobalBufferCount, initialScalarBufferIndex, waveLaneCount: 32, storageBufferOffsetAlignment, pixelOutputBindings: outputs, imageBindingBase: imageBindingBase, pixelInputEnable: pixelInputEnable, pixelInputAddress: pixelInputAddress, pixelInputCntl: pixelInputCntl); return context.TryCompile(out shader, out error); } public static bool TryCompileVertexShader( Gen5ShaderState state, Gen5ShaderEvaluation evaluation, out Gen5MslShader shader, out string error, int globalBufferBase = 0, int totalGlobalBufferCount = -1, int imageBindingBase = 0, int initialScalarBufferIndex = -1, int requiredVertexOutputCount = 0, ulong storageBufferOffsetAlignment = 1) { var context = new CompilationContext( Gen5MslStage.Vertex, state, evaluation, 1, 1, 1, globalBufferBase, totalGlobalBufferCount, initialScalarBufferIndex, waveLaneCount: 32, storageBufferOffsetAlignment, imageBindingBase: imageBindingBase, requiredVertexOutputCount: requiredVertexOutputCount); return context.TryCompile(out shader, out error); } public static bool TryCompileComputeShader( Gen5ShaderState state, Gen5ShaderEvaluation evaluation, uint localSizeX, uint localSizeY, uint localSizeZ, out Gen5MslShader shader, out string error, int totalGlobalBufferCount = -1, int initialScalarBufferIndex = -1, uint waveLaneCount = 32, ulong storageBufferOffsetAlignment = 1) { var context = new CompilationContext( Gen5MslStage.Compute, state, evaluation, Math.Max(localSizeX, 1), Math.Max(localSizeY, 1), Math.Max(localSizeZ, 1), globalBufferBase: 0, totalGlobalBufferCount, initialScalarBufferIndex, waveLaneCount, storageBufferOffsetAlignment); return context.TryCompile(out shader, out error); } private sealed partial class CompilationContext { // Safety valve for the PC-dispatcher loop, mirroring the SPIR-V // translator: a mistranslated loop-exit condition must terminate the // invocation instead of wedging the GPU queue. private static readonly int _maxDispatcherSteps = int.TryParse( Environment.GetEnvironmentVariable("SHARPEMU_SHADER_MAX_STEPS"), out var maxSteps) && maxSteps >= 0 ? maxSteps : 100_000; private const long InitialScalarDefinition = -1; private const long ConflictingScalarDefinition = -2; private const long UnreachableScalarDefinition = -3; private readonly Gen5MslStage _stage; private readonly Gen5ShaderState _state; private readonly Gen5ShaderEvaluation _evaluation; private readonly uint _localSizeX; private readonly uint _localSizeY; private readonly uint _localSizeZ; private readonly int _globalBufferBase; private readonly int _totalGlobalBufferCount; private readonly int _initialScalarBufferIndex; private readonly uint _waveLaneCount; private readonly ulong _storageBufferOffsetAlignment; private readonly Dictionary _scalarDefinitionsBeforePc = []; private readonly IReadOnlyList _pixelOutputBindings; private readonly int _imageBindingBase; private readonly uint _pixelInputEnable; private readonly uint _pixelInputAddress; private readonly uint[] _pixelInputCntl; private readonly Dictionary _imageBindingByPc = []; private readonly Dictionary _bufferBindingByPc = []; private readonly List<(bool IsStorage, string ComponentKind)> _imageKinds = []; // Per storage-image binding: whether the body reads it, writes it, or // both. Metal caps access::read_write textures at 8 per function, so each // binding is declared with the minimal access it actually uses. private bool[] _imageBindingReads = []; private bool[] _imageBindingWrites = []; private readonly SortedSet _pixelAttributes = []; private readonly SortedSet _vertexOutputs = []; private readonly Dictionary _vertexInputsByPc = []; private readonly int _requiredVertexOutputCount; private readonly StringBuilder _body = new(); private int _indent; private int _nextTemp; private bool _usesLds; private bool _usesFormatLoads; private bool _usesWaveScratch; /// True when this stage emulates a 64-lane guest wave across two /// 32-wide Apple simdgroups (compute only; graphics stages use the /// single-lane model regardless of guest wave size). private bool IsWave64 => _waveLaneCount == 64 && _stage == Gen5MslStage.Compute; public CompilationContext( Gen5MslStage stage, Gen5ShaderState state, Gen5ShaderEvaluation evaluation, uint localSizeX, uint localSizeY, uint localSizeZ, int globalBufferBase, int totalGlobalBufferCount, int initialScalarBufferIndex, uint waveLaneCount, ulong storageBufferOffsetAlignment, IReadOnlyList? pixelOutputBindings = null, int imageBindingBase = 0, uint pixelInputEnable = 0, uint pixelInputAddress = 0, IReadOnlyList? pixelInputCntl = null, int requiredVertexOutputCount = 0) { _pixelOutputBindings = pixelOutputBindings ?? []; _imageBindingBase = imageBindingBase; _pixelInputEnable = pixelInputEnable; _pixelInputAddress = pixelInputAddress; _pixelInputCntl = new uint[32]; for (uint i = 0; i < 32u; i++) { _pixelInputCntl[i] = pixelInputCntl is not null && i < (uint)pixelInputCntl.Count ? pixelInputCntl[(int)i] : i; } _requiredVertexOutputCount = requiredVertexOutputCount; _stage = stage; _state = state; _evaluation = evaluation; _localSizeX = localSizeX; _localSizeY = localSizeY; _localSizeZ = localSizeZ; _globalBufferBase = globalBufferBase; _totalGlobalBufferCount = totalGlobalBufferCount < 0 ? evaluation.GlobalMemoryBindings.Count : totalGlobalBufferCount; _initialScalarBufferIndex = initialScalarBufferIndex; _waveLaneCount = waveLaneCount == 64 ? 64u : 32u; if (storageBufferOffsetAlignment == 0 || (storageBufferOffsetAlignment & (storageBufferOffsetAlignment - 1)) != 0 || storageBufferOffsetAlignment > uint.MaxValue) { throw new ArgumentOutOfRangeException( nameof(storageBufferOffsetAlignment), storageBufferOffsetAlignment, "storage-buffer offset alignment must be a uint-sized power of two"); } _storageBufferOffsetAlignment = storageBufferOffsetAlignment; } public bool TryCompile(out Gen5MslShader shader, out string error) { shader = default!; error = string.Empty; try { // A 64-lane guest wave that uses cross-lane ops needs the // threadgroup-scratch bridge (ballots span both 32-wide halves, // read-first-lane broadcasts across them). Programs without such // ops are wave-size-agnostic and need no scratch. _usesWaveScratch = IsWave64 && UsesWaveSensitiveOperations(); var blocks = BuildBasicBlocks(_state.Program.Instructions); if (blocks.Count == 0) { error = "shader contains no executable blocks"; return false; } BuildScalarDefinitionInfo(blocks, _state.Program.Instructions); DeclareImageKinds(); foreach (var instruction in _state.Program.Instructions) { _usesLds |= instruction.Control is Gen5DataShareControl { Gds: false }; _usesFormatLoads |= IsFormatBufferLoad(instruction.Opcode); if (instruction.Control is Gen5InterpolationControl interpolationControl) { _pixelAttributes.Add(interpolationControl.Attribute); } if (_stage == Gen5MslStage.Vertex && instruction.Control is Gen5ExportControl { Target: >= 32 and < 64 } vertexExport) { _vertexOutputs.Add(vertexExport.Target - 32); } } if (_stage == Gen5MslStage.Vertex) { // Cover every location the paired fragment shader reads, // even ones this vertex program never exports, so Metal's // exact vertex-out/fragment-in interface match succeeds. // Extras stay zero-filled. for (uint location = 0; location < _requiredVertexOutputCount; location++) { _vertexOutputs.Add(location); } foreach (var input in _evaluation.VertexInputs ?? []) { if (input.ComponentCount is >= 1 and <= 4) { _vertexInputsByPc.TryAdd(input.Pc, input); foreach (var aliasPc in input.AliasPcs ?? []) { _vertexInputsByPc.TryAdd(aliasPc, input); } } } } // Emit the dispatcher body first: block translation discovers // nothing that changes the signature in the compute stage, but // keeping the order body-then-wrap matches how the pixel/vertex // stages will need it (their IO discovery happens during block // translation). _indent = 2; for (var index = 0; index < blocks.Count; index++) { Line($"case {index}u:"); Line("{"); _indent++; if (!TryEmitBlock(blocks, index, out error)) { error = $"block=0x{blocks[index].StartPc:X}: {error}"; return false; } _indent--; Line("}"); Line("break;"); } var source = new StringBuilder(); EmitModule(source, blocks.Count); shader = new Gen5MslShader( source.ToString(), EntryPointName, _stage, _evaluation.GlobalMemoryBindings, _evaluation.ImageBindings, AttributeCount: _stage switch { Gen5MslStage.Pixel => (uint)_pixelAttributes.Count, Gen5MslStage.Vertex => (uint)_vertexOutputs.Count, _ => 0, }, VertexInputs: _stage == Gen5MslStage.Vertex ? _evaluation.VertexInputs ?? [] : [], _localSizeX, _localSizeY, _localSizeZ, UniformsBufferIndex: UniformsBufferIndex, ImageBindingBase: _imageBindingBase, SamplerSlots: _samplerSlots, SamplerCount: _samplerCount, SamplerArgBufferIndex: _samplerCount > 0 ? SamplerArgBufferIndex : -1); return true; } catch (Exception exception) { error = exception.Message; return false; } } /// Mirrors the SPIR-V translator's subgroup-usage predicates: /// the ops whose results depend on the wave width or on other lanes. /// A program without them is wave-size-agnostic. private bool UsesWaveSensitiveOperations() { foreach (var instruction in _state.Program.Instructions) { if (instruction.Control is Gen5DppControl or Gen5Dpp8Control || instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32" or "VReadlaneB32" or "VReadfirstlaneB32" or "VMbcntLoU32B32" or "VMbcntHiU32B32" || instruction.Opcode.Contains("Saveexec", StringComparison.Ordinal) || instruction.Opcode.StartsWith("SCbranchExec", StringComparison.Ordinal) || instruction.Opcode.StartsWith("SCbranchVcc", StringComparison.Ordinal) || instruction.Opcode.StartsWith("VCmpx", StringComparison.Ordinal)) { return true; } foreach (var operand in instruction.Sources) { if (IsWaveMaskOperand(operand)) { return true; } } foreach (var operand in instruction.Destinations) { if (IsWaveMaskOperand(operand)) { return true; } } } return false; } private static bool IsWaveMaskOperand(Gen5Operand operand) => operand.Kind == Gen5OperandKind.ScalarRegister && operand.Value is VccLoRegister or VccHiRegister or ExecLoRegister or ExecHiRegister; /// Rewrites the trailing comma of the last emitted parameter /// line into the closing parenthesis. Every stage emits each entry /// parameter with a trailing comma so optional parameters never need /// to know whether they are last. private static void CloseParameterList(StringBuilder source) { var index = source.Length - 1; while (index >= 0 && (source[index] == '\n' || source[index] == '\r')) { index--; } if (index >= 0 && source[index] == ',') { source.Remove(index, source.Length - index); source.AppendLine(")"); } } private string EntryPointName => _stage switch { Gen5MslStage.Vertex => "gen5_vs", Gen5MslStage.Pixel => "gen5_ps", _ => "gen5_cs", }; private int UniformsBufferIndex => _globalBufferBase + _totalGlobalBufferCount; private void EmitModule(StringBuilder source, int blockCount) { source.AppendLine("// Generated by SharpEmu Gen5MslTranslator."); source.AppendLine("#include "); source.AppendLine(); source.AppendLine("using namespace metal;"); source.AppendLine(); // Uniforms: dispatch bounds plus per-buffer byte lengths. Metal has // no OpArrayLength equivalent, so buffer extents travel with the // dispatch instead of being queried in-shader. if (_usesFormatLoads) { EmitFormatLoadPrelude(source); } source.AppendLine("struct SharpEmuUniforms"); source.AppendLine("{"); source.AppendLine(" uint dispatch_limit_x;"); source.AppendLine(" uint dispatch_limit_y;"); source.AppendLine(" uint dispatch_limit_z;"); source.AppendLine(" uint reserved;"); source.AppendLine($" uint buffer_bytes[{Math.Max(_totalGlobalBufferCount, 1)}];"); source.AppendLine("};"); source.AppendLine(); EmitSamplerArgumentBufferStruct(source); EmitPrelude(source); source.AppendLine(); if (_stage == Gen5MslStage.Vertex) { // Stage IO structs: fetched attributes from the evaluated vertex // inputs (bound via MTLVertexDescriptor), position plus the // param outputs the paired fragment shader reads. if (_vertexInputsByPc.Count != 0) { source.AppendLine("struct Gen5VsIn"); source.AppendLine("{"); var declared = new HashSet(); foreach (var input in _vertexInputsByPc.Values) { if (!declared.Add(input.Location)) { continue; } var fieldType = input.ComponentCount == 1 ? "float" : $"float{input.ComponentCount}"; source.AppendLine( $" {fieldType} in{input.Location} [[attribute({input.Location})]];"); } source.AppendLine("};"); source.AppendLine(); } source.AppendLine("struct Gen5VsOut"); source.AppendLine("{"); source.AppendLine(" float4 sharpemu_position [[position]];"); foreach (var location in _vertexOutputs) { source.AppendLine($" float4 param{location} [[user(locn{location})]];"); } source.AppendLine("};"); source.AppendLine(); source.AppendLine($"vertex Gen5VsOut {EntryPointName}("); if (_vertexInputsByPc.Count != 0) { source.AppendLine(" Gen5VsIn sharpemu_vin [[stage_in]],"); } } else if (_stage == Gen5MslStage.Pixel) { // Stage IO structs: interpolated attributes discovered from the // program's V_INTERP controls, MRT outputs from the bindings. source.AppendLine("struct Gen5PsIn"); source.AppendLine("{"); source.AppendLine(" float4 sharpemu_frag_coord [[position]];"); foreach (var attribute in _pixelAttributes) { var cntl = attribute < (uint)_pixelInputCntl.Length ? _pixelInputCntl[attribute] : attribute; var location = cntl & 0x1Fu; var flat = (cntl & 0x400u) != 0 ? ", flat" : string.Empty; source.AppendLine( $" float4 attr{attribute} [[user(locn{location}){flat}]];"); } source.AppendLine("};"); source.AppendLine(); source.AppendLine("struct Gen5PsOut"); source.AppendLine("{"); foreach (var binding in _pixelOutputBindings) { var fieldType = binding.Kind switch { Gen5PixelOutputKind.Uint => "uint4", Gen5PixelOutputKind.Sint => "int4", _ => "float4", }; source.AppendLine( $" {fieldType} mrt{binding.GuestSlot} [[color({binding.HostLocation})]];"); } source.AppendLine("};"); source.AppendLine(); source.AppendLine($"fragment Gen5PsOut {EntryPointName}("); source.AppendLine(" Gen5PsIn sharpemu_in [[stage_in]],"); } else { source.AppendLine($"kernel void {EntryPointName}("); } for (var index = 0; index < _evaluation.GlobalMemoryBindings.Count; index++) { source.AppendLine( $" device uint* b{index} [[buffer({_globalBufferBase + index})]],"); } if (_initialScalarBufferIndex >= 0) { // The per-dispatch scalar-state buffer sits at its flat slot, // past every stage's global bindings; the shader only reads it. source.AppendLine( $" const device uint* b{_initialScalarBufferIndex} " + $"[[buffer({_initialScalarBufferIndex})]],"); } source.AppendLine( $" constant SharpEmuUniforms& sharpemu_uniforms [[buffer({UniformsBufferIndex})]],"); EmitImageArguments(source); if (_stage == Gen5MslStage.Compute) { source.AppendLine(" uint3 sharpemu_local_id [[thread_position_in_threadgroup]],"); source.AppendLine(" uint3 sharpemu_group_id [[threadgroup_position_in_grid]],"); } if (_stage == Gen5MslStage.Vertex) { source.AppendLine(" uint sharpemu_vertex_id [[vertex_id]],"); source.AppendLine(" uint sharpemu_instance_id [[instance_id]],"); } else if (_stage == Gen5MslStage.Compute && IsWave64) { // A 64-lane guest wave is two 32-wide Apple simdgroups. Metal // packs a threadgroup's threads into simdgroups in ascending // thread_index order, so thread_index_in_threadgroup & 63 is the // guest lane and its low bit-5 selects the half. Both halves sit // in one threadgroup, so a threadgroup_barrier rendezvous bridges // the wave for 64-wide ballots (see EmitWave64Ballot). source.AppendLine(" uint sharpemu_tg_index [[thread_index_in_threadgroup]],"); } else if (_stage == Gen5MslStage.Compute) { // Compute threads map one-to-one onto guest lanes, so wave ops // address the invocation's real simdgroup lane. source.AppendLine(" uint sharpemu_lane [[thread_index_in_simdgroup]],"); } CloseParameterList(source); source.AppendLine("{"); if (_stage == Gen5MslStage.Compute && IsWave64) { source.AppendLine(" uint sharpemu_lane = sharpemu_tg_index & 63u;"); if (_usesWaveScratch && !_usesLds) { // Two dwords bridge each half's ballot; the third carries a // broadcast value for read-first-lane. Indexed only by half, // so correct for a one-wave (64-thread) workgroup — larger // workgroups would need per-wave scratch (matches the SPIR-V // translator's bridge scope). When the shader also uses LDS the // bridge instead aliases the top of that allocation (below) so // total threadgroup memory stays within Metal's 32 KB limit. source.AppendLine(" threadgroup uint sharpemu_wave_scratch[3];"); } } if (_stage != Gen5MslStage.Compute) { // Graphics stages model a single logical wave lane — the SPIR-V // translator's no-subgroup fallback — because Metal leaves // simdgroup ops undefined inside the divergent dispatcher loop. // Ballots degrade to bit 0 in the prelude and shuffle-family // selects resolve to the lane's own value. source.AppendLine(" const uint sharpemu_lane = 0u;"); } if (_usesLds) { if (_stage == Gen5MslStage.Compute) { // 32 KB of guest LDS as workgroup-shared memory; the address // is masked into bounds like the SPIR-V translator. source.AppendLine($" threadgroup uint sharpemu_lds[{LdsDwordCount}];"); } else { // Graphics stages model LDS as per-invocation scratch (the // SPIR-V translator's Private-array trick), sized smaller // because only write-then-read correctness is needed. source.AppendLine($" thread uint sharpemu_lds[{PrivateLdsDwordCount}] = {{}};"); } } if (_usesWaveScratch && _usesLds && _stage == Gen5MslStage.Compute) { // Reuse the final three dwords of the LDS allocation for the // wave64 bridge. A separate threadgroup array would push total // threadgroup memory past Metal's 32 KB limit for shaders that // request the full LDS (mirrors the SPIR-V translator). Guest LDS // accesses are bounds-masked into the same allocation, so this // trades a rare top-of-LDS collision for a compilable pipeline. source.AppendLine( $" threadgroup uint* sharpemu_wave_scratch = &sharpemu_lds[{LdsDwordCount - 3}u];"); } EmitRegisterFile(source); if (_stage == Gen5MslStage.Pixel) { source.AppendLine(" Gen5PsOut sharpemu_out = {};"); } else if (_stage == Gen5MslStage.Vertex) { // Zero-initialized: param outputs the program never exports // stay (0,0,0,0) to satisfy the fragment interface. source.AppendLine(" Gen5VsOut sharpemu_out = {};"); } EmitInitialState(source); source.AppendLine(); source.AppendLine(" while (active)"); source.AppendLine(" {"); source.AppendLine(" switch (pc)"); source.AppendLine(" {"); source.Append(_body); source.AppendLine(" default:"); source.AppendLine(" active = false;"); source.AppendLine(" break;"); source.AppendLine(" }"); if (_maxDispatcherSteps > 0) { source.AppendLine($" if (++steps >= {_maxDispatcherSteps}u)"); source.AppendLine(" {"); source.AppendLine(" active = false;"); source.AppendLine(" }"); } source.AppendLine(" }"); if (_stage == Gen5MslStage.Pixel) { // A lane still removed from EXEC when the guest shader exits is // a killed fragment; it must not contribute color or blending. source.AppendLine(" if (!exec)"); source.AppendLine(" {"); source.AppendLine(" discard_fragment();"); source.AppendLine(" }"); source.AppendLine(" return sharpemu_out;"); } else if (_stage == Gen5MslStage.Vertex) { source.AppendLine(" return sharpemu_out;"); } source.AppendLine("}"); } // Shared helpers: MSL allows free functions, so unaligned and subdword // access is a byte-pointer cast instead of the manual word-combining // the SPIR-V translator inlines at every site. All access is // range-checked against the binding's byte length; loads outside the // buffer produce zero and stores are dropped, matching the SPIR-V // translator's robust-access behavior. The static text lives in // Templates/prelude.msl; only the wave-ballot expression varies. // // Graphics stages model one logical wave lane (lane 0), so a ballot is // just that lane's bit: "value ? 1 : 0". A real simd_ballot cannot be // used there: the translated program runs inside the divergent // while(active){switch(pc)} dispatcher, where Metal leaves cross-lane // ops undefined, so lanes at different pc corrupt each other's EXEC/VCC // reconstruction and kill whole quads (all fragments discarded). This // is the SPIR-V translator's no-subgroup fallback model. Compute // mirrors the SPIR-V translator's compute path instead: threads map // one-to-one onto real simdgroup lanes and ballots are real, so masks // parked in VCC/EXEC hold each lane's actual bit. private void EmitPrelude(StringBuilder source) => source.Append(MslTemplates.Render( "prelude", ("ballot_return", _stage == Gen5MslStage.Compute ? "(uint)(uint64_t)simd_ballot(value)" : "value ? 1u : 0u"))); // The GFX10 unified-format table is baked from the same authoritative // decoder descriptor evaluation uses (dataFormat | numberFormat << 8); // the descriptor is read at execution time, so decoding stays dynamic — // compiled shaders may be reused with new SRDs. The static conversion // functions live in Templates/format_prelude.msl. private static void EmitFormatLoadPrelude(StringBuilder source) { var table = new StringBuilder(); for (uint format = 0; format < 128; format++) { Gfx10UnifiedFormat.TryDecode(format, out var dataFormat, out var numberFormat); if ((format & 15) == 0) { if (format != 0) { table.AppendLine(); } table.Append(" "); } table.Append($" 0x{dataFormat | (numberFormat << 8):X}u,"); } var layoutCases = new StringBuilder(); var first = true; foreach (var (component, format, bytes, bitOffset, bitCount) in FormatComponentLayouts()) { if (!first) { layoutCases.AppendLine(); } first = false; layoutCases.Append( $" case {component * 16 + format}u: byteOff = {bytes}u; bitOff = {bitOffset}u; bits = {bitCount}u; break;"); } source.AppendLine(MslTemplates.Render( "format_prelude", ("format_table", table.ToString()), ("layout_cases", layoutCases.ToString()))); } /// /// The legacy DATA_FORMAT component layouts the SPIR-V translator encodes /// in LoadGfx10BufferFormatComponent, as (component, format, byteOffset, /// bitOffset, bitCount) tuples. /// private static IEnumerable<(uint Component, uint Format, uint Bytes, uint BitOffset, uint BitCount)> FormatComponentLayouts() { // Component 0. yield return (0, 1, 0, 0, 8); yield return (0, 2, 0, 0, 16); yield return (0, 3, 0, 0, 8); yield return (0, 4, 0, 0, 32); yield return (0, 5, 0, 0, 16); yield return (0, 6, 0, 0, 10); yield return (0, 7, 0, 0, 11); yield return (0, 8, 0, 0, 10); yield return (0, 9, 0, 0, 2); yield return (0, 10, 0, 0, 8); yield return (0, 11, 0, 0, 32); yield return (0, 12, 0, 0, 16); yield return (0, 13, 0, 0, 32); yield return (0, 14, 0, 0, 32); // Component 1. yield return (1, 3, 1, 0, 8); yield return (1, 5, 2, 0, 16); yield return (1, 6, 0, 10, 11); yield return (1, 7, 0, 11, 11); yield return (1, 8, 0, 10, 10); yield return (1, 9, 0, 2, 10); yield return (1, 10, 1, 0, 8); yield return (1, 11, 4, 0, 32); yield return (1, 12, 2, 0, 16); yield return (1, 13, 4, 0, 32); yield return (1, 14, 4, 0, 32); // Component 2. yield return (2, 6, 0, 21, 11); yield return (2, 7, 0, 22, 10); yield return (2, 8, 0, 20, 10); yield return (2, 9, 0, 12, 10); yield return (2, 10, 2, 0, 8); yield return (2, 12, 4, 0, 16); yield return (2, 13, 8, 0, 32); yield return (2, 14, 8, 0, 32); // Component 3. yield return (3, 8, 0, 30, 2); yield return (3, 9, 0, 22, 10); yield return (3, 10, 3, 0, 8); yield return (3, 12, 6, 0, 16); yield return (3, 14, 12, 0, 32); } private void EmitRegisterFile(StringBuilder source) { source.AppendLine($" uint s[{ScalarRegisterFileCount}] = {{}};"); source.AppendLine($" uint v[{VectorRegisterFileCount}] = {{}};"); source.AppendLine(" bool exec = true;"); source.AppendLine(" bool vcc = false;"); source.AppendLine(" bool scc = false;"); source.AppendLine(" uint pc = 0u;"); source.AppendLine(" bool active = true;"); source.AppendLine(" uint steps = 0u;"); } private void EmitInitialState(StringBuilder source) { if (_initialScalarBufferIndex >= 0) { // Initial scalar registers arrive in a per-dispatch buffer so // animated user data reuses one translation, mirroring the // SPIR-V translator. Word 256+i of the same buffer carries the // per-binding byte bias for suballocated guest buffers. var consumed = Gen5ShaderTranslator.ComputeConsumedScalarMask(_state.Program); for (uint index = 0; index < _evaluation.InitialScalarRegisters.Count && index < ScalarRegisterFileCount; index++) { if (Gen5ShaderTranslator.IsScalarConsumed(consumed, index)) { source.AppendLine( $" s[{index}] = b{_initialScalarBufferIndex}[{index}];"); } } var biasCount = _globalBufferBase + _evaluation.GlobalMemoryBindings.Count; source.AppendLine($" uint bias[{Math.Max(biasCount, 1)}] = {{}};"); for (var binding = 0; binding < biasCount; binding++) { source.AppendLine( $" bias[{binding}] = b{_initialScalarBufferIndex}[{256 + binding}];"); } } else { for (uint index = 0; index < _evaluation.InitialScalarRegisters.Count && index < ScalarRegisterFileCount; index++) { var value = _evaluation.InitialScalarRegisters[(int)index]; if (value != 0) { source.AppendLine($" s[{index}] = 0x{value:X}u;"); } } var biasCount = _globalBufferBase + _evaluation.GlobalMemoryBindings.Count; source.AppendLine($" uint bias[{Math.Max(biasCount, 1)}] = {{}};"); } if (_stage == Gen5MslStage.Compute) { source.AppendLine(" v[0] = sharpemu_local_id.x;"); source.AppendLine(" v[1] = sharpemu_local_id.y;"); source.AppendLine(" v[2] = sharpemu_local_id.z;"); // Partial-group guard: lanes whose global id falls outside the // guest dispatch stay inactive, matching the SPIR-V bounds // check driven by the same uniform. source.AppendLine( $" active = (sharpemu_group_id.x * {_localSizeX}u + sharpemu_local_id.x) < sharpemu_uniforms.dispatch_limit_x"); source.AppendLine( $" && (sharpemu_group_id.y * {_localSizeY}u + sharpemu_local_id.y) < sharpemu_uniforms.dispatch_limit_y"); source.AppendLine( $" && (sharpemu_group_id.z * {_localSizeZ}u + sharpemu_local_id.z) < sharpemu_uniforms.dispatch_limit_z;"); if (_state.ComputeSystemRegisters is { } registers) { EmitComputeSystemRegister(source, registers.WorkGroupXRegister, "sharpemu_group_id.x"); EmitComputeSystemRegister(source, registers.WorkGroupYRegister, "sharpemu_group_id.y"); EmitComputeSystemRegister(source, registers.WorkGroupZRegister, "sharpemu_group_id.z"); if (registers.ThreadGroupSizeRegister is { } sizeRegister && sizeRegister < ScalarRegisterFileCount) { source.AppendLine( $" s[{sizeRegister}] = {checked(_localSizeX * _localSizeY * _localSizeZ)}u;"); } } } else if (_stage == Gen5MslStage.Pixel) { EmitPixelInputState(source); } else if (_stage == Gen5MslStage.Vertex) { // Hardware-selected VGPRs for the vertex and instance indices. source.AppendLine(" v[5] = sharpemu_vertex_id;"); source.AppendLine(" v[8] = sharpemu_instance_id;"); } // VCC/EXEC live in their architectural SGPRs (see StoreScalar); // establish the entry state over whatever the initial-scalar block // carried so the register file and the bool views agree from the // first instruction. source.AppendLine($" s[{VccLoRegister}] = 0u;"); source.AppendLine($" s[{VccHiRegister}] = 0u;"); if (IsWave64) { // All 64 lanes are active at entry (before the dispatcher masks // any off), and this runs at a uniform point, so the bridge // barriers are safe here too. EmitBallotStoreAtEntry(source, ExecLoRegister, "true"); } else { source.AppendLine($" s[{ExecLoRegister}] = sharpemu_ballot(true);"); source.AppendLine($" s[{ExecHiRegister}] = 0u;"); } } /// Entry-time form of writing to /// at the fixed indent of the module prologue. private void EmitBallotStoreAtEntry(StringBuilder source, uint loRegister, string condition) { if (!_usesWaveScratch) { // No cross-lane ops: the high half stays zero and the low half // is this simdgroup's ballot, matching the wave-agnostic path. source.AppendLine($" s[{loRegister}] = sharpemu_ballot({condition});"); source.AppendLine($" s[{loRegister + 1}] = 0u;"); return; } source.AppendLine( $" sharpemu_wave_scratch[(sharpemu_lane >> 5) & 1u] = sharpemu_ballot({condition});"); source.AppendLine(" threadgroup_barrier(mem_flags::mem_threadgroup);"); source.AppendLine($" s[{loRegister}] = sharpemu_wave_scratch[0];"); source.AppendLine($" s[{loRegister + 1}] = sharpemu_wave_scratch[1];"); source.AppendLine(" threadgroup_barrier(mem_flags::mem_threadgroup);"); } private static void EmitComputeSystemRegister( StringBuilder source, uint? scalarRegister, string expression) { if (scalarRegister is { } register && register < ScalarRegisterFileCount) { source.AppendLine($" s[{register}] = {expression};"); } } // ---- dispatcher blocks ---- private bool TryEmitBlock( IReadOnlyList blocks, int blockIndex, out string error) { error = string.Empty; var block = blocks[blockIndex]; var instructions = _state.Program.Instructions; for (var index = block.StartIndex; index < block.EndIndex; index++) { var instruction = instructions[index]; var isTerminator = index == block.EndIndex - 1; if (instruction.Opcode == "SEndpgm") { Line("active = false;"); return true; } if (instruction.Opcode == "SBranch") { // A branch to (or past) the program's end is an exit — the // pattern sprite alpha-kill shaders use to skip their tail. if (IsExitBranchTarget(instructions, instruction)) { Line("active = false;"); return true; } if (!TryGetBranchTargetBlock(blocks, instruction, out var target)) { error = $"branch target outside program at pc=0x{instruction.Pc:X}"; return false; } Line($"pc = {target}u;"); return true; } if (instruction.Opcode.StartsWith("SCbranch", StringComparison.Ordinal)) { if (!TryGetBranchCondition(instruction.Opcode, out var condition)) { error = $"unsupported conditional branch {instruction.Opcode}"; return false; } var fallthrough = blockIndex + 1; if (IsExitBranchTarget(instructions, instruction)) { // Taken → exit; not taken → fall through (or exit when // this is the last block anyway). if (fallthrough >= blocks.Count) { Line("active = false;"); } else { Line($"pc = {fallthrough}u;"); Line($"active = !({condition});"); } return true; } if (!TryGetBranchTargetBlock(blocks, instruction, out var target)) { error = $"branch target outside program at pc=0x{instruction.Pc:X}"; return false; } if (fallthrough >= blocks.Count) { Line($"pc = ({condition}) ? {target}u : 0xFFFFFFFFu;"); Line($"active = ({condition});"); } else { Line($"pc = ({condition}) ? {target}u : {fallthrough}u;"); } return true; } if (!TryEmitInstruction(instruction, out error)) { error = $"pc=0x{instruction.Pc:X4} {instruction.Opcode}: {error}"; return false; } if (isTerminator) { // Fall through to the next block (or exit at program end). if (blockIndex + 1 < blocks.Count) { Line($"pc = {blockIndex + 1}u;"); } else { Line("active = false;"); } } } return true; } private bool TryGetBranchCondition(string opcode, out string condition) { condition = opcode switch { "SCbranchScc0" => "!scc", "SCbranchScc1" => "scc", // VCCZ/EXECZ test the full architectural register pair, which // also covers programs that parked plain data in VCC. "SCbranchVccz" => $"(s[{VccLoRegister}] | s[{VccHiRegister}]) == 0u", "SCbranchVccnz" => $"(s[{VccLoRegister}] | s[{VccHiRegister}]) != 0u", "SCbranchExecz" => $"(s[{ExecLoRegister}] | s[{ExecHiRegister}]) == 0u", "SCbranchExecnz" => $"(s[{ExecLoRegister}] | s[{ExecHiRegister}]) != 0u", _ => string.Empty, }; return condition.Length != 0; } private static bool TryGetBranchTargetBlock( IReadOnlyList blocks, Gen5ShaderInstruction instruction, out int block) { block = -1; return TryGetBranchTargetPc(instruction, out var targetPc) && TryFindBlock(blocks, targetPc, out block); } /// True when the branch lands at or past the last instruction's /// end — an exit, matching the SPIR-V translator's handling. private static bool IsExitBranchTarget( IReadOnlyList instructions, Gen5ShaderInstruction instruction) { if (instructions.Count == 0 || !TryGetBranchTargetPc(instruction, out var targetPc)) { return false; } var last = instructions[^1]; var lastEndPc = last.Pc + (uint)(last.Words.Count * sizeof(uint)); return targetPc >= lastEndPc; } // ---- instruction dispatch ---- private bool TryEmitInstruction( Gen5ShaderInstruction instruction, out string error) { error = string.Empty; switch (instruction.Opcode) { case "SNop": case "SWaitcnt": case "SInstPrefetch": case "STtraceData": case "SClause": case "VNop": // NGG shaders bracket their exports with s_sendmsg // (GS_ALLOC_REQ/DEALLOC) to reserve hardware export space; // exports are translated directly, so the message is moot. case "SSendmsg": return true; case "SBarrier": Line("threadgroup_barrier(mem_flags::mem_threadgroup | mem_flags::mem_device);"); return true; } if (instruction.Control is Gen5ImageControl imageControl) { return TryEmitImage(instruction, imageControl, out error); } if (instruction.Control is Gen5ExportControl exportControl) { return TryEmitExport(instruction, exportControl, out error); } if (instruction.Control is Gen5InterpolationControl interpolationControl) { return TryEmitInterpolation(instruction, interpolationControl, out error); } if (instruction.Control is Gen5DataShareControl dataShare) { return TryEmitDataShare(instruction, dataShare, out error); } if (instruction.Control is Gen5ScalarMemoryControl scalarMemory) { return TryEmitScalarMemory(instruction, scalarMemory, out error); } if (instruction.Control is Gen5GlobalMemoryControl globalMemory) { return TryEmitGlobalMemory(instruction, globalMemory, out error); } if (instruction.Control is Gen5BufferMemoryControl bufferMemory) { return TryEmitBufferMemory(instruction, bufferMemory, out error); } if (instruction.Opcode.StartsWith("V", StringComparison.Ordinal)) { return TryEmitVectorAlu(instruction, out error); } if (instruction.Opcode.StartsWith("S", StringComparison.Ordinal)) { return TryEmitScalarAlu(instruction, out error); } error = "unsupported instruction"; return false; } // ---- memory ---- private bool TryEmitScalarMemory( Gen5ShaderInstruction instruction, Gen5ScalarMemoryControl control, out string error) { error = string.Empty; var scalarAddress = instruction.Sources.Count != 0 && instruction.Sources[0].Kind == Gen5OperandKind.ScalarRegister ? instruction.Sources[0].Value : uint.MaxValue; if (!TryResolveDominatingBufferBinding( instruction.Pc, scalarAddress, registerCount: instruction.Opcode.StartsWith( "SBufferLoad", StringComparison.Ordinal) ? 4u : 2u, out var bindingIndex)) { foreach (var destination in instruction.Destinations) { if (destination.Kind == Gen5OperandKind.ScalarRegister) { StoreScalar(destination.Value, "0u"); } } return true; } var offset = control.DynamicOffsetRegister is { } register ? $"(s[{register}] + 0x{unchecked((uint)control.ImmediateOffsetBytes):X}u)" : $"0x{unchecked((uint)control.ImmediateOffsetBytes):X}u"; var address = Temp("uint", ApplyByteBias(bindingIndex, offset)); for (var index = 0; index < instruction.Destinations.Count; index++) { var destination = instruction.Destinations[index]; if (destination.Kind != Gen5OperandKind.ScalarRegister) { error = "invalid scalar-memory destination"; return false; } StoreScalar( destination.Value, LoadWord(bindingIndex, $"({address} + {index * 4}u)")); } return true; } private bool TryEmitGlobalMemory( Gen5ShaderInstruction instruction, Gen5GlobalMemoryControl control, out string error) { error = string.Empty; if (!TryResolveDominatingBufferBinding( instruction.Pc, control.ScalarAddress, registerCount: 2, out var bindingIndex)) { error = "missing global-memory binding"; return false; } var address = Temp( "uint", ApplyByteBias( bindingIndex, $"(v[{control.VectorAddress}] + 0x{unchecked((uint)control.OffsetBytes):X}u)")); return TryEmitResolvedMemoryAccess( instruction.Opcode, bindingIndex, address, control.VectorData, control.DwordCount, control.Glc, out error); } private bool TryEmitBufferMemory( Gen5ShaderInstruction instruction, Gen5BufferMemoryControl control, out string error) { error = string.Empty; if (_stage == Gen5MslStage.Vertex && _vertexInputsByPc.TryGetValue(instruction.Pc, out var vertexInput)) { return TryEmitVertexInputFetch(control, vertexInput, out error); } if (!TryResolveDominatingBufferBinding( instruction.Pc, control.ScalarResource, registerCount: 4, out var bindingIndex)) { error = "missing buffer-memory binding"; return false; } var scalarOffset = instruction.Sources.Count > 2 ? SourceExpression(instruction.Sources[2], instruction) : "0u"; var stride = $"((s[{control.ScalarResource + 1}] >> 16) & 0x3FFFu)"; var vectorIndex = control.IndexEnabled ? $"v[{control.VectorAddress}]" : "0u"; var vectorOffset = control.OffsetEnabled ? $"v[{control.VectorAddress + (control.IndexEnabled ? 1u : 0u)}]" : "0u"; var address = Temp( "uint", ApplyByteBias( bindingIndex, $"(0x{unchecked((uint)control.OffsetBytes):X}u + {scalarOffset} + {vectorOffset} + ({vectorIndex} * {stride}))")); // Typed MUBUF/MTBUF loads convert through the descriptor's unified // format; raw dword loads and every store take the byte path below // (format stores write raw dwords, matching the SPIR-V translator). if (IsFormatBufferLoad(instruction.Opcode) && !instruction.Opcode.StartsWith("BufferStore", StringComparison.Ordinal)) { EmitBufferFormatLoad( bindingIndex, address, control.ScalarResource, control.VectorData, control.DwordCount); return true; } return TryEmitResolvedMemoryAccess( instruction.Opcode, bindingIndex, address, control.VectorData, control.DwordCount, control.Glc, out error); } private void EmitBufferFormatLoad( int bindingIndex, string byteAddress, uint scalarResource, uint vectorData, uint componentCount) { // Format and destination swizzle come from descriptor word 3 at // execution time; the prelude table decodes the unified format the // same way descriptor evaluation does. var word3 = Temp("uint", ScalarExpression(scalarResource + 3)); var entry = Temp("uint", $"sharpemu_gfx10_formats[({word3} >> 12) & 0x7Fu]"); var dataFormat = Temp("uint", $"{entry} & 0xFFu"); var numberFormat = Temp("uint", $"({entry} >> 8) & 0xFFu"); var canonical = new string[4]; for (var component = 0; component < 4; component++) { var byteOff = Temp("uint", "0u"); var bitOff = Temp("uint", "0u"); var bits = Temp("uint", "0u"); Line($"sharpemu_format_layout({dataFormat}, {component}u, {byteOff}, {bitOff}, {bits});"); var packed = Temp( "uint", LoadWord(bindingIndex, $"({byteAddress} + {byteOff})")); var raw = Temp( "uint", $"{bits} == 0u ? 0u : extract_bits({packed}, {bitOff}, {bits})"); var missing = component == 3 ? $"sharpemu_format_one({numberFormat})" : "0u"; canonical[component] = Temp( "uint", $"{bits} == 0u ? {missing} : sharpemu_format_convert({raw}, {bits}, {numberFormat}, {dataFormat})"); } for (uint destination = 0; destination < componentCount; destination++) { var selector = Temp("uint", $"({word3} >> {destination * 3}u) & 7u"); StoreVector( vectorData + destination, $"{selector} == 1u ? sharpemu_format_one({numberFormat}) : " + $"{selector} == 4u ? {canonical[0]} : " + $"{selector} == 5u ? {canonical[1]} : " + $"{selector} == 6u ? {canonical[2]} : " + $"{selector} == 7u ? {canonical[3]} : 0u"); } } private bool TryEmitDataShare( Gen5ShaderInstruction instruction, Gen5DataShareControl control, out string error) { error = string.Empty; if (control.Gds) { error = "GDS data share is not implemented"; return false; } var ldsMask = _stage == Gen5MslStage.Compute ? LdsDwordMask : PrivateLdsDwordCount - 1; string LdsIndex(string address, uint offsetBytes) => offsetBytes == 0 ? $"((({address}) >> 2) & {ldsMask}u)" : $"(((({address}) + {offsetBytes}u) >> 2) & {ldsMask}u)"; void StoreLds(string index, string value) { // Exec-guarded like every other lane-visible write. Line($"if (exec) {{ sharpemu_lds[{index}] = {value}; }}"); } switch (instruction.Opcode) { case "DsAddU32": { var address = Temp("uint", RawSource(instruction, 0)); var value = Temp("uint", RawSource(instruction, 1)); Line("if (exec)"); Line("{"); _indent++; Line($"atomic_fetch_add_explicit((threadgroup atomic_uint*)&sharpemu_lds[{LdsIndex(address, control.Offset0)}], {value}, memory_order_relaxed);"); _indent--; Line("}"); return true; } case "DsWriteB32": { var address = Temp("uint", RawSource(instruction, 0)); StoreLds(LdsIndex(address, control.Offset0), RawSource(instruction, 1)); return true; } case "DsWriteB64": { var address = Temp("uint", RawSource(instruction, 0)); StoreLds(LdsIndex(address, control.Offset0), RawSource(instruction, 1)); StoreLds(LdsIndex(address, control.Offset0 + sizeof(uint)), RawSource(instruction, 2)); return true; } case "DsWriteB96": case "DsWriteB128": { var dwordCount = instruction.Opcode == "DsWriteB128" ? 4 : 3; var address = Temp("uint", RawSource(instruction, 0)); for (var dword = 0; dword < dwordCount; dword++) { StoreLds( LdsIndex(address, control.Offset0 + (uint)(dword * sizeof(uint))), RawSource(instruction, 1 + dword)); } return true; } case "DsWrite2B32": case "DsWrite2St64B32": { var st64 = instruction.Opcode == "DsWrite2St64B32"; var address = Temp("uint", RawSource(instruction, 0)); StoreLds( LdsIndex(address, EffectiveDsPairOffsetBytes(control.Offset0, st64)), RawSource(instruction, 1)); StoreLds( LdsIndex(address, EffectiveDsPairOffsetBytes(control.Offset1, st64)), RawSource(instruction, 2)); return true; } case "DsReadB32": { var address = Temp("uint", RawSource(instruction, 0)); StoreVector( instruction.Destinations[0].Value, $"sharpemu_lds[{LdsIndex(address, control.Offset0)}]"); return true; } case "DsReadB96": case "DsReadB128": { var dwordCount = instruction.Opcode == "DsReadB128" ? 4 : 3; if (instruction.Destinations.Count < dwordCount) { error = "missing LDS read operand"; return false; } var address = Temp("uint", RawSource(instruction, 0)); for (var dword = 0; dword < dwordCount; dword++) { StoreVector( instruction.Destinations[dword].Value, $"sharpemu_lds[{LdsIndex(address, control.Offset0 + (uint)(dword * sizeof(uint)))}]"); } return true; } case "DsRead2B32": case "DsRead2St64B32": { if (instruction.Destinations.Count < 2) { error = "missing LDS read2 operand"; return false; } var st64 = instruction.Opcode == "DsRead2St64B32"; var address = Temp("uint", RawSource(instruction, 0)); StoreVector( instruction.Destinations[0].Value, $"sharpemu_lds[{LdsIndex(address, EffectiveDsPairOffsetBytes(control.Offset0, st64))}]"); StoreVector( instruction.Destinations[1].Value, $"sharpemu_lds[{LdsIndex(address, EffectiveDsPairOffsetBytes(control.Offset1, st64))}]"); return true; } default: error = $"unsupported LDS opcode {instruction.Opcode}"; return false; } } private static uint EffectiveDsPairOffsetBytes(uint offset, bool st64) => offset * (st64 ? 256u : sizeof(uint)); private bool TryEmitResolvedMemoryAccess( string opcode, int bindingIndex, string byteAddress, uint vectorData, uint dwordCount, bool glc, out string error) { error = string.Empty; if (opcode is "GlobalAtomicAdd" or "BufferAtomicAdd" or "GlobalAtomicUMax" or "BufferAtomicUMax") { var function = opcode.EndsWith("Add", StringComparison.Ordinal) ? "atomic_fetch_add_explicit" : "atomic_fetch_max_explicit"; Line("if (exec)"); Line("{"); _indent++; Line($"if ({byteAddress} + 4u <= {BufferBytes(bindingIndex)} && ({byteAddress} & 3u) == 0u)"); Line("{"); _indent++; var original = Temp( "uint", $"{function}((device atomic_uint*)(b{bindingIndex} + ({byteAddress} >> 2)), v[{vectorData}], memory_order_relaxed)"); if (glc) { Line($"v[{vectorData}] = {original};"); } _indent--; Line("}"); _indent--; Line("}"); return true; } if (opcode.StartsWith("GlobalStore", StringComparison.Ordinal) || opcode.StartsWith("BufferStore", StringComparison.Ordinal)) { Line("if (exec)"); Line("{"); _indent++; if (TryGetSubdwordStoreInfo(opcode, out var storeBytes, out var sourceShift)) { var source = sourceShift == 0 ? $"v[{vectorData}]" : $"(v[{vectorData}] >> {sourceShift})"; Line($"sharpemu_store_bytes(b{bindingIndex}, {BufferBytes(bindingIndex)}, {byteAddress}, {source}, {storeBytes}u);"); } else { for (uint index = 0; index < dwordCount; index++) { Line($"sharpemu_store_bytes(b{bindingIndex}, {BufferBytes(bindingIndex)}, {byteAddress} + {index * 4}u, v[{vectorData + index}], 4u);"); } } _indent--; Line("}"); return true; } if (TryGetSubdwordLoadInfo(opcode, out var loadBytes, out var signExtend, out var d16, out var d16High)) { var loaded = Temp( "uint", $"sharpemu_load_bytes(b{bindingIndex}, {BufferBytes(bindingIndex)}, {byteAddress}, {loadBytes}u, {(signExtend ? "true" : "false")})"); if (!d16) { StoreVector(vectorData, loaded); return true; } // D16 loads merge into one half of the destination register. StoreVector( vectorData, d16High ? $"(v[{vectorData}] & 0x0000FFFFu) | (({loaded} & 0xFFFFu) << 16)" : $"(v[{vectorData}] & 0xFFFF0000u) | ({loaded} & 0xFFFFu)"); return true; } if (opcode.StartsWith("GlobalLoad", StringComparison.Ordinal) || opcode.StartsWith("BufferLoad", StringComparison.Ordinal)) { for (uint index = 0; index < dwordCount; index++) { StoreVector( vectorData + index, LoadWord(bindingIndex, $"({byteAddress} + {index * 4}u)")); } return true; } error = $"unsupported memory opcode {opcode}"; return false; } private static bool TryGetSubdwordLoadInfo( string opcode, out uint byteCount, out bool signExtend, out bool d16, out bool d16High) { byteCount = opcode.Contains("byte", StringComparison.OrdinalIgnoreCase) ? 1u : 2u; signExtend = opcode.Contains("Sbyte", StringComparison.Ordinal) || opcode.Contains("Sshort", StringComparison.Ordinal); d16 = opcode.Contains("D16", StringComparison.Ordinal); d16High = opcode.EndsWith("D16Hi", StringComparison.Ordinal); return opcode.Contains("LoadUbyte", StringComparison.Ordinal) || opcode.Contains("LoadSbyte", StringComparison.Ordinal) || opcode.Contains("LoadUshort", StringComparison.Ordinal) || opcode.Contains("LoadSshort", StringComparison.Ordinal) || opcode.Contains("LoadShortD16", StringComparison.Ordinal); } private static bool TryGetSubdwordStoreInfo( string opcode, out uint byteCount, out uint sourceShift) { byteCount = opcode.Contains("StoreByte", StringComparison.Ordinal) ? 1u : 2u; sourceShift = opcode.EndsWith("D16Hi", StringComparison.Ordinal) ? 16u : 0u; return opcode.Contains("StoreByte", StringComparison.Ordinal) || opcode.Contains("StoreShort", StringComparison.Ordinal); } private static bool IsFormatBufferLoad(string opcode) => opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal) || opcode.StartsWith("TBufferLoad", StringComparison.Ordinal); private string BufferBytes(int bindingIndex) => $"sharpemu_uniforms.buffer_bytes[{_globalBufferBase + bindingIndex}]"; private string LoadWord(int bindingIndex, string byteAddress) => $"sharpemu_load_word(b{bindingIndex}, {BufferBytes(bindingIndex)}, {byteAddress})"; private string ApplyByteBias(int bindingIndex, string byteAddress) => $"({byteAddress} + bias[{_globalBufferBase + bindingIndex}])"; // ---- binding resolution (ports the SPIR-V dominating-definition scheme) ---- private bool TryResolveDominatingBufferBinding( uint pc, uint scalarAddress, uint registerCount, out int bindingIndex) { if (_bufferBindingByPc.TryGetValue(pc, out bindingIndex)) { return true; } var candidates = _evaluation.GlobalMemoryBindings; for (var index = 0; index < candidates.Count; index++) { var binding = candidates[index]; foreach (var bindingPc in binding.InstructionPcs) { if (bindingPc == pc) { bindingIndex = index; _bufferBindingByPc.Add(pc, index); return true; } } } // No direct PC match: accept a binding only when the descriptor // registers hold the exact same definitions here as at one of the // binding's own access points — the scalar-definition dataflow the // SPIR-V translator uses for descriptors shared across sites. for (var index = 0; index < candidates.Count; index++) { var binding = candidates[index]; if (binding.ScalarAddress != scalarAddress) { continue; } foreach (var candidatePc in binding.InstructionPcs) { if (!HasSameScalarDefinitions(candidatePc, pc, scalarAddress, registerCount)) { continue; } bindingIndex = index; _bufferBindingByPc.Add(pc, index); return true; } } bindingIndex = -1; return false; } // ---- writer helpers ---- private void Line(string text) { for (var index = 0; index < _indent; index++) { _body.Append(" "); } _body.AppendLine(text); } private string Temp(string type, string expression) { var name = $"t{_nextTemp++}"; Line($"{type} {name} = {expression};"); return name; } // VCC (s106:s107) and EXEC (s126:s127) are architectural SGPRs: programs // freely use them as scratch data registers (s_buffer_load into s[106], // then v_rcp_f32 of that value is real RDNA2 code). The register file // holds their raw 32-bit values as the source of truth; the bools // vcc/exec are cached per-lane views kept in sync at every write so // control flow stays cheap. Reading them back as data returns the file. private void StoreScalar(uint register, string expression) { switch (register) { case VccLoRegister: { var value = Temp("uint", expression); Line($"s[{VccLoRegister}] = {value};"); Line($"vcc = (({value}) >> sharpemu_lane & 1u) != 0u;"); return; } case ExecLoRegister: { var value = Temp("uint", expression); Line($"s[{ExecLoRegister}] = {value};"); Line($"exec = (({value}) >> sharpemu_lane & 1u) != 0u;"); return; } case VccHiRegister: case ExecHiRegister: // Wave32: the high halves carry no lanes, but keep the data. Line($"s[{register}] = {expression};"); return; } if (register < ScalarRegisterFileCount) { Line($"s[{register}] = {expression};"); } } private void StoreVector(uint register, string expression, bool guardWithExec = true) { if (register >= VectorRegisterFileCount) { return; } if (guardWithExec) { Line($"if (exec) {{ v[{register}] = {expression}; }}"); } else { Line($"v[{register}] = {expression};"); } } private string ScalarExpression(uint register) => register < ScalarRegisterFileCount ? $"s[{register}]" : "0u"; private string SourceExpression( Gen5Operand operand, Gen5ShaderInstruction instruction) { switch (operand.Kind) { case Gen5OperandKind.ScalarRegister: return ScalarExpression(operand.Value); case Gen5OperandKind.VectorRegister: return $"v[{operand.Value}]"; case Gen5OperandKind.LiteralConstant: return FormatUInt(operand.Value); case Gen5OperandKind.EncodedConstant: // 251/252/253 read the VCCZ/EXECZ/SCC status bits as data. if (operand.Value == 251) { return $"((s[{VccLoRegister}] | s[{VccHiRegister}]) == 0u ? 1u : 0u)"; } if (operand.Value == 252) { return $"((s[{ExecLoRegister}] | s[{ExecHiRegister}]) == 0u ? 1u : 0u)"; } if (operand.Value == 253) { return "(scc ? 1u : 0u)"; } if (Gen5InlineConstants.TryDecode(operand.Value, out var constant)) { return FormatUInt(constant); } throw new NotSupportedException( $"unsupported encoded constant {operand.Value} in {instruction.Opcode}"); default: throw new NotSupportedException($"unsupported operand kind {operand.Kind}"); } } private static string FormatUInt(uint value) => value <= 9 ? $"{value}u" : $"0x{value.ToString("X", CultureInfo.InvariantCulture)}u"; private static string AsFloat(string expression) => $"as_type({expression})"; private static string AsUInt(string expression) => $"as_type({expression})"; // ---- basic blocks (ports BuildBasicBlocks from the SPIR-V translator) ---- private readonly record struct ShaderBlock( uint StartPc, int StartIndex, int EndIndex); private static IReadOnlyList BuildBasicBlocks( IReadOnlyList instructions) { if (instructions.Count == 0) { return []; } var leaders = new SortedSet { instructions[0].Pc }; for (var index = 0; index < instructions.Count; index++) { var instruction = instructions[index]; if (IsBranch(instruction.Opcode) && TryGetBranchTargetPc(instruction, out var targetPc)) { leaders.Add(targetPc); } if ((IsBranch(instruction.Opcode) || instruction.Opcode == "SEndpgm") && index + 1 < instructions.Count) { leaders.Add(instructions[index + 1].Pc); } } var starts = new List(leaders.Count); foreach (var pc in leaders) { if (FindInstructionIndex(instructions, pc) >= 0) { starts.Add(pc); } } var blocks = new List(starts.Count); for (var index = 0; index < starts.Count; index++) { var startIndex = FindInstructionIndex(instructions, starts[index]); var endIndex = index + 1 < starts.Count ? FindInstructionIndex(instructions, starts[index + 1]) : instructions.Count; if (startIndex >= 0 && endIndex > startIndex) { blocks.Add(new ShaderBlock(starts[index], startIndex, endIndex)); } } return blocks; } private static bool IsBranch(string opcode) => opcode == "SBranch" || opcode.StartsWith("SCbranch", StringComparison.Ordinal); private static bool TryGetBranchTargetPc( Gen5ShaderInstruction instruction, out uint targetPc) { targetPc = 0; if (instruction.Encoding != Gen5ShaderEncoding.Sopp || instruction.Words.Count == 0) { return false; } var offset = unchecked((short)(instruction.Words[0] & 0xFFFF)); var nextPc = (long)instruction.Pc + (instruction.Words.Count * sizeof(uint)); var target = nextPc + (offset * sizeof(uint)); if (target < 0 || target > uint.MaxValue) { return false; } targetPc = (uint)target; return true; } private static int FindInstructionIndex( IReadOnlyList instructions, uint pc) { for (var index = 0; index < instructions.Count; index++) { if (instructions[index].Pc == pc) { return index; } } return -1; } private static bool TryFindBlock( IReadOnlyList blocks, uint pc, out int block) { for (var index = 0; index < blocks.Count; index++) { if (blocks[index].StartPc == pc) { block = index; return true; } } block = -1; return false; } // ---- scalar-definition dataflow (ports BuildScalarDefinitionInfo) ---- private void BuildScalarDefinitionInfo( IReadOnlyList blocks, IReadOnlyList instructions) { var predecessors = new HashSet[blocks.Count]; for (var index = 0; index < blocks.Count; index++) { predecessors[index] = []; } void AddEdge(int source, int destination) { if (destination < 0 || destination >= blocks.Count) { return; } predecessors[destination].Add(source); } for (var blockIndex = 0; blockIndex < blocks.Count; blockIndex++) { var block = blocks[blockIndex]; var terminator = instructions[block.EndIndex - 1]; var hasFallthrough = blockIndex + 1 < blocks.Count; if (terminator.Opcode == "SEndpgm") { continue; } if (terminator.Opcode == "SBranch") { if (TryGetBranchTargetPc(terminator, out var targetPc) && TryFindBlock(blocks, targetPc, out var targetBlock)) { AddEdge(blockIndex, targetBlock); } continue; } if (terminator.Opcode.StartsWith("SCbranch", StringComparison.Ordinal)) { if (TryGetBranchTargetPc(terminator, out var targetPc) && TryFindBlock(blocks, targetPc, out var targetBlock)) { AddEdge(blockIndex, targetBlock); } if (hasFallthrough) { AddEdge(blockIndex, blockIndex + 1); } continue; } if (hasFallthrough) { AddEdge(blockIndex, blockIndex + 1); } } var blockInputs = new long[blocks.Count][]; var blockOutputs = new long[blocks.Count][]; var hasOutput = new bool[blocks.Count]; var initialDefinitions = new long[ScalarRegisterFileCount]; Array.Fill(initialDefinitions, InitialScalarDefinition); static void MergeDefinitions( long[] destination, long[] source, ref bool hasInput) { if (!hasInput) { Array.Copy(source, destination, (int)ScalarRegisterFileCount); hasInput = true; return; } for (var register = 0; register < ScalarRegisterFileCount; register++) { if (destination[register] != source[register]) { destination[register] = ConflictingScalarDefinition; } } } static void ApplyScalarDefinitions( long[] definitions, ShaderBlock block, IReadOnlyList blockInstructions) { for (var instructionIndex = block.StartIndex; instructionIndex < block.EndIndex; instructionIndex++) { var instruction = blockInstructions[instructionIndex]; foreach (var destination in instruction.Destinations) { if (destination.Kind == Gen5OperandKind.ScalarRegister && destination.Value < ScalarRegisterFileCount) { definitions[destination.Value] = instruction.Pc + 1L; } } } } var changed = true; while (changed) { changed = false; for (var blockIndex = 0; blockIndex < blocks.Count; blockIndex++) { var input = new long[ScalarRegisterFileCount]; Array.Fill(input, UnreachableScalarDefinition); var hasInput = false; if (blockIndex == 0) { MergeDefinitions(input, initialDefinitions, ref hasInput); } foreach (var predecessor in predecessors[blockIndex]) { if (hasOutput[predecessor]) { MergeDefinitions( input, blockOutputs[predecessor], ref hasInput); } } if (!hasInput) { continue; } var output = (long[])input.Clone(); ApplyScalarDefinitions(output, blocks[blockIndex], instructions); if (!hasOutput[blockIndex] || !blockInputs[blockIndex].AsSpan().SequenceEqual(input) || !blockOutputs[blockIndex].AsSpan().SequenceEqual(output)) { blockInputs[blockIndex] = input; blockOutputs[blockIndex] = output; hasOutput[blockIndex] = true; changed = true; } } } _scalarDefinitionsBeforePc.Clear(); for (var blockIndex = 0; blockIndex < blocks.Count; blockIndex++) { if (!hasOutput[blockIndex]) { continue; } var definitions = (long[])blockInputs[blockIndex].Clone(); var block = blocks[blockIndex]; for (var instructionIndex = block.StartIndex; instructionIndex < block.EndIndex; instructionIndex++) { var instruction = instructions[instructionIndex]; if (instruction.Control is Gen5ImageControl or Gen5ScalarMemoryControl or Gen5GlobalMemoryControl or Gen5BufferMemoryControl) { _scalarDefinitionsBeforePc[instruction.Pc] = (long[])definitions.Clone(); } foreach (var destination in instruction.Destinations) { if (destination.Kind == Gen5OperandKind.ScalarRegister && destination.Value < ScalarRegisterFileCount) { definitions[destination.Value] = instruction.Pc + 1L; } } } } } } }