// Copyright (C) 2026 SharpEmu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later using SharpEmu.ShaderCompiler; namespace SharpEmu.ShaderCompiler.Vulkan; public static partial class Gen5SpirvTranslator { private const uint ScalarRegisterCount = 256; private const uint VectorRegisterCount = 512; private const uint LdsDwordCount = 8192; // Graphics stages model LDS as a per-invocation Private array rather than // real workgroup-shared memory. A full 32 KB Private array per vertex/pixel // invocation is wasteful and risks Metal compile limits, and per-invocation // write-then-read correctness only needs deterministic address→slot masking, // so a smaller array is safe. private const uint PrivateLdsDwordCount = 2048; private const uint RdnaWaveLaneCount = 32; public static bool TryCompilePixelShader( Gen5ShaderState state, Gen5ShaderEvaluation evaluation, Gen5PixelOutputKind outputKind, out Gen5SpirvShader 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, ulong storageBufferOffsetAlignment = 1) => TryCompilePixelShader( state, evaluation, [new Gen5PixelOutputBinding((uint)pixelRenderTargetSlot, 0, outputKind)], out shader, out error, globalBufferBase, totalGlobalBufferCount, imageBindingBase, initialScalarBufferIndex, pixelInputEnable, pixelInputAddress, storageBufferOffsetAlignment); public static bool TryCompilePixelShader( Gen5ShaderState state, Gen5ShaderEvaluation evaluation, IReadOnlyList outputs, out Gen5SpirvShader shader, out string error, int globalBufferBase = 0, int totalGlobalBufferCount = -1, int imageBindingBase = 0, int initialScalarBufferIndex = -1, uint pixelInputEnable = 0, uint pixelInputAddress = 0, ulong storageBufferOffsetAlignment = 1) { if (outputs.Count > 8 || outputs.Any(output => output.GuestSlot > 7)) { shader = default!; error = "pixel outputs must contain at most eight guest slots in the 0..7 range"; return false; } if (outputs.Select(output => output.GuestSlot).Distinct().Count() != outputs.Count || outputs.Select(output => output.HostLocation).Distinct().Count() != outputs.Count) { shader = default!; error = "pixel output guest slots and host locations must be unique"; return false; } if (!outputs .OrderBy(output => output.HostLocation) .Select((output, index) => output.HostLocation == (uint)index) .All(isDense => isDense)) { shader = default!; error = "pixel output host locations must be dense in the 0..N-1 range"; return false; } var context = new CompilationContext( Gen5SpirvStage.Pixel, state, evaluation, outputs, 1, 1, 1, globalBufferBase, totalGlobalBufferCount, imageBindingBase, initialScalarBufferIndex, pixelInputEnable: pixelInputEnable, pixelInputAddress: pixelInputAddress, storageBufferOffsetAlignment: storageBufferOffsetAlignment); return context.TryCompile(out shader, out error); } public static bool TryCompileVertexShader( Gen5ShaderState state, Gen5ShaderEvaluation evaluation, out Gen5SpirvShader 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( Gen5SpirvStage.Vertex, state, evaluation, [], 1, 1, 1, globalBufferBase, totalGlobalBufferCount, imageBindingBase, initialScalarBufferIndex, requiredVertexOutputCount: requiredVertexOutputCount, storageBufferOffsetAlignment: storageBufferOffsetAlignment); return context.TryCompile(out shader, out error); } public static bool TryCompileComputeShader( Gen5ShaderState state, Gen5ShaderEvaluation evaluation, uint localSizeX, uint localSizeY, uint localSizeZ, out Gen5SpirvShader shader, out string error, int totalGlobalBufferCount = -1, int initialScalarBufferIndex = -1, uint waveLaneCount = 32, ulong storageBufferOffsetAlignment = 1) { var context = new CompilationContext( Gen5SpirvStage.Compute, state, evaluation, [], Math.Max(localSizeX, 1), Math.Max(localSizeY, 1), Math.Max(localSizeZ, 1), 0, totalGlobalBufferCount, 0, initialScalarBufferIndex, waveLaneCount: waveLaneCount, storageBufferOffsetAlignment: storageBufferOffsetAlignment); return context.TryCompile(out shader, out error); } internal static SpirvImageFormat DecodeStorageImageFormat( uint dataFormat, uint numberType) => CompilationContext.DecodeStorageImageFormat(dataFormat, numberType); private sealed partial class CompilationContext { private const uint ImageDescriptorDwords = 8; private const uint SamplerDescriptorDwords = 4; private const int ScalarRegisterCount = 128; private const long InitialScalarDefinition = -1; private const long ConflictingScalarDefinition = -2; private const long UnreachableScalarDefinition = -3; private readonly SpirvModuleBuilder _module = new(); private readonly Gen5SpirvStage _stage; private readonly Gen5ShaderState _state; private readonly Gen5ShaderEvaluation _evaluation; private readonly IReadOnlyList _pixelOutputBindings; private readonly uint _waveLaneCount; private readonly bool _emulateWave64; // Safety valve for the PC-dispatcher loop. Each iteration executes one // GCN basic block; a correctly-translated shader always reaches its // terminal block (pc out of range -> default -> exit) well within any // real control flow. A mistranslated shader whose loop-exit condition is // wrong would otherwise spin the dispatcher forever, hanging the single // Metal queue and freezing every later submission (black screen, no // recovery). Bounding the iteration count guarantees the invocation // terminates instead: the effect may be wrong for that shader, but the // GPU never wedges. 0 disables the guard (original unbounded behaviour). private static readonly int _maxDispatcherSteps = int.TryParse( Environment.GetEnvironmentVariable("SHARPEMU_SHADER_MAX_STEPS"), out var maxSteps) && maxSteps >= 0 ? maxSteps : 100_000; // Diagnostic coverage probe. When enabled, every selected MRT export // writes opaque magenta while preserving the shader's control flow, // EXEC mask, geometry and raster state. This separates missing // rasterization from valid fragments whose translated values are zero. private static readonly bool _forcePixelMagenta = string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_FORCE_PIXEL_MAGENTA"), "1", StringComparison.Ordinal); // Which pixel-shader MRT export target (EXP_MRT0..7 == render-target // slot) is routed to the single fragment output. The offscreen draw // path renders one bound color target per pass, so a multi-render-target // (deferred G-buffer) draw compiles one pixel variant per slot, each // selecting that slot's export here. // Vertex stage only: the fragment shader paired with this vertex shader // declares interpolated inputs for locations 0..(this-1). Metal requires // every fragment input location to be written by the vertex shader, so // the vertex stage must export at least this many param outputs (any it // does not naturally export are zero-filled) or pipeline creation fails // with "Fragment input(s) `user(locnN)` ... not written by vertex shader". private readonly int _requiredVertexOutputCount; private readonly uint _localSizeX; private readonly uint _localSizeY; private readonly uint _localSizeZ; private readonly int _globalBufferBase; private readonly int _totalGlobalBufferCount; private readonly int _imageBindingBase; private readonly int _initialScalarBufferIndex; private readonly uint _pixelInputEnable; private readonly uint _pixelInputAddress; private readonly ulong _storageBufferOffsetAlignment; private readonly List _interfaces = []; private readonly Dictionary _pixelInputs = []; private readonly Dictionary _pixelOutputs = []; private readonly Dictionary _vertexOutputs = []; private readonly Dictionary _vertexInputsByPc = []; private readonly List _imageResources = []; private readonly Dictionary _imageBindingByPc = []; private readonly Dictionary _bufferBindingByPc = []; private readonly Dictionary _scalarDefinitionsBeforePc = []; private uint _voidType; private uint _boolType; private uint _uintType; private uint _intType; private uint _longType; private uint _ulongType; private uint _floatType; private uint _vec2Type; private uint _vec3Type; private uint _vec4Type; private uint _uvec2Type; private uint _uvec3Type; private uint _uvec4Type; private uint _privateUintPointer; private uint _privateVec2Pointer; private uint _privateBoolPointer; private uint _runtimeBufferBiases; private uint _scalarRegisters; private uint _vectorRegisters; private uint _packedHalfRegisters; private uint _scc; private uint _vcc; private uint _exec; private uint _reachedPixelExport; private uint _programCounter; private uint _programActive; private uint _iterationGuard; private uint _globalBuffers; private uint _gfx10BufferFormatTable; private uint _storageBlockPointer; private uint _storageUintPointer; private uint _lds; private uint _ldsElementPointer; private uint _ldsDwordMask; private uint _positionOutput; private uint _vertexIndexInput; private uint _instanceIndexInput; private uint _fragCoordInput; private uint _localInvocationIdInput; private uint _localInvocationIndexInput; private uint _workGroupIdInput; private uint _computeDispatchLimit; private uint _pushConstantUintPointer; private uint _subgroupSizeInput; private uint _subgroupInvocationIdInput; private uint _waveMaskScratch; private uint _waveMaskScratchElementPointer; private uint _waveBroadcastScratch; private bool _waveScratchInLds; private uint _glsl; private enum ImageComponentKind { Float, Sint, Uint, } private enum VertexInputComponentKind { Float, Sint, Uint, } private readonly record struct SpirvImageResource( uint Variable, uint ImageType, uint ObjectType, uint ComponentType, uint VectorType, ImageComponentKind ComponentKind, bool IsStorage, bool Arrayed, SpirvImageDim Dimension); private readonly record struct SpirvVertexInput( uint Variable, uint Type, uint ComponentType, uint ComponentCount, VertexInputComponentKind ComponentKind); private readonly record struct SpirvPixelOutput( uint Variable, uint Type, Gen5PixelOutputKind Kind); public CompilationContext( Gen5SpirvStage stage, Gen5ShaderState state, Gen5ShaderEvaluation evaluation, IReadOnlyList pixelOutputBindings, uint localSizeX, uint localSizeY, uint localSizeZ, int globalBufferBase, int totalGlobalBufferCount, int imageBindingBase, int initialScalarBufferIndex, uint pixelInputEnable = 0, uint pixelInputAddress = 0, int requiredVertexOutputCount = 0, uint waveLaneCount = 32, ulong storageBufferOffsetAlignment = 1) { _stage = stage; _requiredVertexOutputCount = requiredVertexOutputCount; _state = state; _evaluation = evaluation; _pixelOutputBindings = pixelOutputBindings; _waveLaneCount = waveLaneCount == 64 ? 64u : 32u; _emulateWave64 = stage == Gen5SpirvStage.Compute && _waveLaneCount == 64 && (ulong)localSizeX * localSizeY * localSizeZ == 64; _localSizeX = localSizeX; _localSizeY = localSizeY; _localSizeZ = localSizeZ; _globalBufferBase = globalBufferBase; _totalGlobalBufferCount = totalGlobalBufferCount < 0 ? evaluation.GlobalMemoryBindings.Count : totalGlobalBufferCount; _imageBindingBase = imageBindingBase; _initialScalarBufferIndex = initialScalarBufferIndex; _pixelInputEnable = pixelInputEnable; _pixelInputAddress = pixelInputAddress; 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 Gen5SpirvShader shader, out string error) { shader = default!; error = string.Empty; try { if (Environment.GetEnvironmentVariable( "SHARPEMU_TRACE_TITLE_INTERFACE") == "1" && _state.Program.Address is 0x0000000500780000ul or 0x0000000500781200ul) { Console.Error.WriteLine( $"[AGC][TITLE-INTERFACE] stage={_stage} " + $"address=0x{_state.Program.Address:X16} " + $"required_vertex_outputs={_requiredVertexOutputCount} " + $"ps_ena=0x{_pixelInputEnable:X8} ps_addr=0x{_pixelInputAddress:X8}"); foreach (var instruction in _state.Program.Instructions) { if (instruction.Control is Gen5ExportControl export) { Console.Error.WriteLine( $"[AGC][TITLE-INTERFACE] pc=0x{instruction.Pc:X4} " + $"export_target={export.Target} mask=0x{export.EnableMask:X} " + $"compressed={export.Compressed} src=[" + string.Join(',', instruction.Sources) + "]"); } else if (instruction.Control is Gen5InterpolationControl interpolation) { Console.Error.WriteLine( $"[AGC][TITLE-INTERFACE] pc=0x{instruction.Pc:X4} " + $"attribute={interpolation.Attribute} " + $"channel={interpolation.Channel} dst=[" + string.Join(',', instruction.Destinations) + "]"); } else if (instruction.Control is Gen5ScalarMemoryControl scalarMemory) { var bindingIndex = -1; for (var index = 0; index < _evaluation.GlobalMemoryBindings.Count; index++) { if (_evaluation.GlobalMemoryBindings[index] .InstructionPcs.Contains(instruction.Pc)) { bindingIndex = index; break; } } var binding = bindingIndex >= 0 ? _evaluation.GlobalMemoryBindings[bindingIndex] : null; var byteOffset = scalarMemory.ImmediateOffsetBytes; var sample = binding is not null && scalarMemory.DynamicOffsetRegister is null && byteOffset >= 0 && byteOffset < binding.DataLength ? Convert.ToHexString( binding.Data.AsSpan( byteOffset, Math.Min( checked((int)scalarMemory.DestinationCount * 4), binding.DataLength - byteOffset))) : "dynamic-or-unavailable"; Console.Error.WriteLine( $"[AGC][TITLE-INTERFACE] pc=0x{instruction.Pc:X4} " + $"scalar_load={instruction.Opcode} binding={bindingIndex} " + $"scalar=s{binding?.ScalarAddress} " + $"base=0x{binding?.BaseAddress:X16} length={binding?.DataLength} " + $"offset={byteOffset} dynamic={scalarMemory.DynamicOffsetRegister} " + $"bytes={sample}"); } } } DeclareModule(); var blocks = BuildBasicBlocks(_state.Program.Instructions); if (blocks.Count == 0) { error = "shader contains no executable blocks"; return false; } BuildScalarDefinitionInfo(blocks, _state.Program.Instructions); var functionType = _module.TypeFunction(_voidType); var main = _module.BeginFunction(_voidType, functionType); _module.AddName(main, "main"); _module.AddLabel(); if (_stage == Gen5SpirvStage.Pixel && Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_TITLE_EARLY_COLOR") == "1" && _state.Program.Address == 0x0000000500781200ul) { var earlyOutput = _pixelOutputs .OrderBy(static pair => pair.Key) .Select(static pair => pair.Value) .First(); var earlyColor = earlyOutput.Kind switch { Gen5PixelOutputKind.Float => _module.AddInstruction( SpirvOp.CompositeConstruct, earlyOutput.Type, Float(1f), Float(0f), Float(1f), Float(1f)), Gen5PixelOutputKind.Sint => _module.ConstantNull(earlyOutput.Type), _ => _module.ConstantNull(earlyOutput.Type), }; Store(earlyOutput.Variable, earlyColor); _module.AddStatement(SpirvOp.Return); _module.AddLabel(); } EmitInitialState(); var loopHeader = _module.AllocateId(); var switchHeader = _module.AllocateId(); var switchMerge = _module.AllocateId(); var loopContinue = _module.AllocateId(); var loopMerge = _module.AllocateId(); var defaultLabel = _module.AllocateId(); var caseLabels = new uint[blocks.Count]; for (var index = 0; index < caseLabels.Length; index++) { caseLabels[index] = _module.AllocateId(); } _module.AddStatement(SpirvOp.Branch, loopHeader); _module.AddLabel(loopHeader); _module.AddStatement(SpirvOp.LoopMerge, loopMerge, loopContinue, 0); _module.AddStatement(SpirvOp.Branch, switchHeader); _module.AddLabel(switchHeader); var selector = Load(_uintType, _programCounter); _module.AddStatement(SpirvOp.SelectionMerge, switchMerge, 0); var switchOperands = new uint[2 + (blocks.Count * 2)]; switchOperands[0] = selector; switchOperands[1] = defaultLabel; for (var index = 0; index < blocks.Count; index++) { switchOperands[2 + (index * 2)] = (uint)index; switchOperands[3 + (index * 2)] = caseLabels[index]; } _module.AddStatement(SpirvOp.Switch, switchOperands); for (var index = 0; index < blocks.Count; index++) { _module.AddLabel(caseLabels[index]); if (!TryEmitBlock(blocks, index, out error)) { error = $"block=0x{blocks[index].StartPc:X}: {error}"; return false; } _module.AddStatement(SpirvOp.Branch, switchMerge); } _module.AddLabel(defaultLabel); Store(_programActive, _module.ConstantBool(false)); _module.AddStatement(SpirvOp.Branch, switchMerge); _module.AddLabel(switchMerge); _module.AddStatement(SpirvOp.Branch, loopContinue); _module.AddLabel(loopContinue); var active = Load(_boolType, _programActive); if (_maxDispatcherSteps > 0) { var steps = IAdd(Load(_uintType, _iterationGuard), UInt(1)); Store(_iterationGuard, steps); var withinLimit = _module.AddInstruction( SpirvOp.ULessThan, _boolType, steps, UInt((uint)_maxDispatcherSteps)); active = _module.AddInstruction( SpirvOp.LogicalAnd, _boolType, active, withinLimit); } _module.AddStatement( SpirvOp.BranchConditional, active, loopHeader, loopMerge); _module.AddLabel(loopMerge); if (_stage == Gen5SpirvStage.Pixel && Environment.GetEnvironmentVariable( "SHARPEMU_TRACE_TITLE_SHADER_STATE") == "1" && _state.Program.Address == 0x0000000500781200ul) { var stateOutput = _pixelOutputs .OrderBy(static pair => pair.Key) .Select(static pair => pair.Value) .FirstOrDefault(static output => output.Kind == Gen5PixelOutputKind.Float); if (stateOutput.Variable != 0) { uint EncodeBool(uint condition) => _module.AddInstruction( SpirvOp.Select, _floatType, condition, Float(1f), Float(0f)); Store( stateOutput.Variable, _module.AddInstruction( SpirvOp.CompositeConstruct, stateOutput.Type, EncodeBool(Load(_boolType, _exec)), EncodeBool(IsWaveMaskActive(LoadS64(52))), EncodeBool(Load(_boolType, _reachedPixelExport)), Float(1f))); } StoreS64( 126, _module.Constant64(_ulongType, 1)); } if (_stage == Gen5SpirvStage.Pixel) { // A fragment lane removed from EXEC is not a request to // write the output variable's zero initializer. It is a // killed fragment and must not participate in color, // depth, or blend operations. Keep EXEC masking during // translation, then terminate lanes that remain inactive // when the guest pixel shader exits. var returnLabel = _module.AllocateId(); var killLabel = _module.AllocateId(); // Materialize the condition before SelectionMerge: SPIR-V // requires the merge instruction to be immediately followed // by its structured branch terminator. var laneActive = Load(_boolType, _exec); _module.AddStatement( SpirvOp.SelectionMerge, returnLabel, 0); _module.AddStatement( SpirvOp.BranchConditional, laneActive, returnLabel, killLabel); _module.AddLabel(killLabel); _module.AddStatement(SpirvOp.Kill); _module.AddLabel(returnLabel); } _module.AddStatement(SpirvOp.Return); _module.EndFunction(); var model = _stage switch { Gen5SpirvStage.Vertex => SpirvExecutionModel.Vertex, Gen5SpirvStage.Pixel => SpirvExecutionModel.Fragment, _ => SpirvExecutionModel.GLCompute, }; _module.AddEntryPoint(model, main, "main", _interfaces); if (_stage == Gen5SpirvStage.Pixel) { _module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft); } else if (_stage == Gen5SpirvStage.Compute) { _module.AddExecutionMode( main, SpirvExecutionMode.LocalSize, _localSizeX, _localSizeY, _localSizeZ); } var attributeCount = _stage == Gen5SpirvStage.Vertex ? (uint)_vertexOutputs.Count : (uint)_pixelInputs.Count; shader = new Gen5SpirvShader( _module.Build(), _evaluation.GlobalMemoryBindings, _evaluation.ImageBindings, attributeCount, _stage == Gen5SpirvStage.Vertex ? _evaluation.VertexInputs ?? [] : []); return true; } catch (Exception exception) { error = exception.Message; return false; } } private void DeclareModule() { _module.AddCapability(SpirvCapability.Shader); _module.AddCapability(SpirvCapability.Int64); _module.AddCapability(SpirvCapability.ImageQuery); if (_evaluation.ImageBindings.Any( static binding => (binding.Opcode.StartsWith( "ImageSample", StringComparison.Ordinal) || binding.Opcode.StartsWith( "ImageGather4", StringComparison.Ordinal)) && binding.Opcode.EndsWith("O", StringComparison.Ordinal))) { _module.AddCapability(SpirvCapability.ImageGatherExtended); } if (UsesSubgroupOperations()) { _module.AddCapability(SpirvCapability.GroupNonUniform); if (UsesSubgroupShuffle()) { _module.AddCapability(SpirvCapability.GroupNonUniformShuffle); } if (UsesWaveControl()) { _module.AddCapability(SpirvCapability.GroupNonUniformVote); } if (UsesSubgroupBroadcast() || UsesWaveControl()) { _module.AddCapability(SpirvCapability.GroupNonUniformBallot); } } _glsl = _module.ImportExtInst("GLSL.std.450"); _voidType = _module.TypeVoid(); _boolType = _module.TypeBool(); _uintType = _module.TypeInt(32, signed: false); _intType = _module.TypeInt(32, signed: true); _longType = _module.TypeInt(64, signed: true); _ulongType = _module.TypeInt(64, signed: false); _floatType = _module.TypeFloat(32); _vec2Type = _module.TypeVector(_floatType, 2); _vec3Type = _module.TypeVector(_floatType, 3); _vec4Type = _module.TypeVector(_floatType, 4); _uvec2Type = _module.TypeVector(_uintType, 2); _uvec3Type = _module.TypeVector(_uintType, 3); _uvec4Type = _module.TypeVector(_uintType, 4); _privateUintPointer = _module.TypePointer(SpirvStorageClass.Private, _uintType); _privateVec2Pointer = _module.TypePointer(SpirvStorageClass.Private, _vec2Type); _privateBoolPointer = _module.TypePointer(SpirvStorageClass.Private, _boolType); var scalarArrayType = _module.TypeArray(_uintType, ScalarRegisterCount); var vectorArrayType = _module.TypeArray(_uintType, VectorRegisterCount); var packedHalfArrayType = _module.TypeArray(_vec2Type, VectorRegisterCount); var privateScalarArrayPointer = _module.TypePointer(SpirvStorageClass.Private, scalarArrayType); var privateVectorArrayPointer = _module.TypePointer(SpirvStorageClass.Private, vectorArrayType); var privatePackedHalfArrayPointer = _module.TypePointer(SpirvStorageClass.Private, packedHalfArrayType); _scalarRegisters = _module.AddGlobalVariable( privateScalarArrayPointer, SpirvStorageClass.Private, _module.ConstantNull(scalarArrayType)); _vectorRegisters = _module.AddGlobalVariable( privateVectorArrayPointer, SpirvStorageClass.Private, _module.ConstantNull(vectorArrayType)); _packedHalfRegisters = _module.AddGlobalVariable( privatePackedHalfArrayPointer, SpirvStorageClass.Private, _module.ConstantNull(packedHalfArrayType)); _scc = _module.AddGlobalVariable( _privateBoolPointer, SpirvStorageClass.Private, _module.ConstantBool(false)); _vcc = _module.AddGlobalVariable( _privateBoolPointer, SpirvStorageClass.Private, _module.ConstantBool(false)); _exec = _module.AddGlobalVariable( _privateBoolPointer, SpirvStorageClass.Private, _module.ConstantBool(true)); _reachedPixelExport = _module.AddGlobalVariable( _privateBoolPointer, SpirvStorageClass.Private, _module.ConstantBool(false)); _programCounter = _module.AddGlobalVariable( _privateUintPointer, SpirvStorageClass.Private, _module.Constant(_uintType, 0)); _programActive = _module.AddGlobalVariable( _privateBoolPointer, SpirvStorageClass.Private, _module.ConstantBool(true)); if (_maxDispatcherSteps > 0) { _iterationGuard = _module.AddGlobalVariable( _privateUintPointer, SpirvStorageClass.Private, _module.Constant(_uintType, 0)); _interfaces.Add(_iterationGuard); _module.AddName(_iterationGuard, "pcGuard"); } _interfaces.Add(_scalarRegisters); _interfaces.Add(_vectorRegisters); _interfaces.Add(_packedHalfRegisters); _interfaces.Add(_scc); _interfaces.Add(_vcc); _interfaces.Add(_exec); _interfaces.Add(_reachedPixelExport); _interfaces.Add(_programCounter); _interfaces.Add(_programActive); _module.AddName(_scalarRegisters, "sgpr"); _module.AddName(_vectorRegisters, "vgpr"); _module.AddName(_packedHalfRegisters, "vgprPackedHalf"); var runtimeBufferBiasCount = _globalBufferBase + _evaluation.GlobalMemoryBindings.Count; if (_initialScalarBufferIndex >= 0 && runtimeBufferBiasCount > 0) { var biasArrayType = _module.TypeArray( _uintType, (uint)runtimeBufferBiasCount); var privateBiasArrayPointer = _module.TypePointer( SpirvStorageClass.Private, biasArrayType); _runtimeBufferBiases = _module.AddGlobalVariable( privateBiasArrayPointer, SpirvStorageClass.Private, _module.ConstantNull(biasArrayType)); _module.AddName(_runtimeBufferBiases, "guestBufferByteBias"); _interfaces.Add(_runtimeBufferBiases); } DeclareBuffers(); DeclareImages(); DeclareLds(); DeclareWave64Scratch(); DeclareStageInterface(); DeclareComputeDispatchLimit(); } private void DeclareComputeDispatchLimit() { if (_stage != Gen5SpirvStage.Compute) { return; } // RDNA DISPATCH_* can express exact thread dimensions, including // a final partially populated workgroup. Vulkan dispatches whole // workgroups, so the command path supplies the exact exclusive // thread bounds through a small push-constant block and excess // invocations are disabled before any guest instruction executes. var block = _module.TypeStruct(_uvec3Type); _module.AddDecoration(block, SpirvDecoration.Block); _module.AddMemberDecoration(block, 0, SpirvDecoration.Offset, 0); var blockPointer = _module.TypePointer(SpirvStorageClass.PushConstant, block); _pushConstantUintPointer = _module.TypePointer(SpirvStorageClass.PushConstant, _uintType); _computeDispatchLimit = _module.AddGlobalVariable( blockPointer, SpirvStorageClass.PushConstant); _module.AddName(_computeDispatchLimit, "dispatchThreadLimit"); _interfaces.Add(_computeDispatchLimit); } private void DeclareWave64Scratch() { if (!_emulateWave64 || !UsesSubgroupOperations()) { return; } // Metal exposes 32 KiB of threadgroup memory on the Apple GPUs we // target. Some PS5 compute shaders legitimately request all of it, // so allocating another workgroup variable for the wave64 bridge // makes pipeline creation fail. Reuse the final three dwords of the // existing LDS allocation in that case. The translator already // bounds guest LDS accesses to this fixed allocation; keeping the // bridge inside it preserves the host limit and still provides the // cross-subgroup rendezvous needed to model one 64-lane guest wave. if (_lds != 0) { _waveScratchInLds = true; _waveMaskScratchElementPointer = _ldsElementPointer; return; } var maskArrayType = _module.TypeArray(_uintType, 2); var maskArrayPointer = _module.TypePointer(SpirvStorageClass.Workgroup, maskArrayType); _waveMaskScratchElementPointer = _module.TypePointer(SpirvStorageClass.Workgroup, _uintType); _waveMaskScratch = _module.AddGlobalVariable( maskArrayPointer, SpirvStorageClass.Workgroup); _module.AddName(_waveMaskScratch, "wave64MaskScratch"); _interfaces.Add(_waveMaskScratch); var uintPointer = _module.TypePointer(SpirvStorageClass.Workgroup, _uintType); _waveBroadcastScratch = _module.AddGlobalVariable( uintPointer, SpirvStorageClass.Workgroup); _module.AddName(_waveBroadcastScratch, "wave64BroadcastScratch"); _interfaces.Add(_waveBroadcastScratch); } private void DeclareLds() { if (!UsesLds()) { return; } // Compute shaders get genuine workgroup-shared LDS. Graphics stages // (NGG export/vertex, pixel) cannot use the Workgroup storage class // in SPIR-V, but they still emit ds_write/ds_read — typically as // per-invocation scratch/spill or as NGG staging whose cross-lane // reads don't feed this stage's exports. Model those as a // per-invocation Private array so the shader is valid SPIR-V and its // draw stops being dropped. Index masking in LdsPointer keeps the // arbitrary computed addresses inside the array. var storageClass = _stage == Gen5SpirvStage.Compute ? SpirvStorageClass.Workgroup : SpirvStorageClass.Private; var dwordCount = _stage == Gen5SpirvStage.Compute ? LdsDwordCount : PrivateLdsDwordCount; _ldsDwordMask = dwordCount - 1; var ldsArrayType = _module.TypeArray(_uintType, dwordCount); var ldsPointer = _module.TypePointer(storageClass, ldsArrayType); _ldsElementPointer = _module.TypePointer(storageClass, _uintType); _lds = storageClass == SpirvStorageClass.Workgroup ? _module.AddGlobalVariable(ldsPointer, storageClass) : _module.AddGlobalVariable( ldsPointer, storageClass, _module.ConstantNull(ldsArrayType)); _module.AddName(_lds, "lds"); _interfaces.Add(_lds); } private void DeclareBuffers() { for (var index = 0; index < _evaluation.GlobalMemoryBindings.Count; index++) { foreach (var pc in _evaluation.GlobalMemoryBindings[index].InstructionPcs) { _bufferBindingByPc.TryAdd(pc, _globalBufferBase + index); } } if (_totalGlobalBufferCount == 0) { return; } var runtimeArray = _module.TypeRuntimeArray(_uintType); _module.AddDecoration(runtimeArray, SpirvDecoration.ArrayStride, sizeof(uint)); var block = _module.TypeStruct(runtimeArray); _module.AddDecoration(block, SpirvDecoration.Block); _module.AddMemberDecoration(block, 0, SpirvDecoration.Offset, 0); var descriptors = _module.TypeArray( block, (uint)_totalGlobalBufferCount); var descriptorsPointer = _module.TypePointer(SpirvStorageClass.StorageBuffer, descriptors); _storageBlockPointer = _module.TypePointer(SpirvStorageClass.StorageBuffer, block); _storageUintPointer = _module.TypePointer(SpirvStorageClass.StorageBuffer, _uintType); _globalBuffers = _module.AddGlobalVariable( descriptorsPointer, SpirvStorageClass.StorageBuffer); _module.AddName(_globalBuffers, "guestBuffers"); _module.AddDecoration(_globalBuffers, SpirvDecoration.DescriptorSet, 0); _module.AddDecoration(_globalBuffers, SpirvDecoration.Binding, 0); _interfaces.Add(_globalBuffers); } private void DeclareImages() { for (var index = 0; index < _evaluation.ImageBindings.Count; index++) { var binding = _evaluation.ImageBindings[index]; _imageBindingByPc.TryAdd(binding.Pc, index); var isStorage = Gen5ShaderTranslator.RequiresStorageImage( binding, _evaluation.ImageBindings); var (format, componentKind) = DecodeImageFormat(binding.ResourceDescriptor); var componentType = componentKind switch { ImageComponentKind.Sint => _intType, ImageComponentKind.Uint => _uintType, _ => _floatType, }; if (isStorage && format == SpirvImageFormat.Unknown) { _module.AddCapability( SpirvCapability.StorageImageReadWithoutFormat); _module.AddCapability( SpirvCapability.StorageImageWriteWithoutFormat); } else if (isStorage && RequiresExtendedStorageImageFormat(format)) { _module.AddCapability( SpirvCapability.StorageImageExtendedFormats); } var dimension = binding.Control.Dimension == 2 ? SpirvImageDim.Dim3D : SpirvImageDim.Dim2D; var isArrayed = dimension != SpirvImageDim.Dim3D && !isStorage && Gen5ShaderTranslator.IsArrayedImageBinding(binding); var imageType = _module.TypeImage( componentType, dimension, depth: false, arrayed: isArrayed, multisampled: false, sampled: isStorage ? 2u : 1u, isStorage ? format : SpirvImageFormat.Unknown); var objectType = isStorage ? imageType : _module.TypeSampledImage(imageType); var pointer = _module.TypePointer( SpirvStorageClass.UniformConstant, objectType); var variable = _module.AddGlobalVariable( pointer, SpirvStorageClass.UniformConstant); _module.AddName(variable, isStorage ? $"image{index}" : $"tex{index}"); _module.AddDecoration(variable, SpirvDecoration.DescriptorSet, 0); _module.AddDecoration( variable, SpirvDecoration.Binding, (uint)(_imageBindingBase + index + 1)); _imageResources.Add( new SpirvImageResource( variable, imageType, objectType, componentType, _module.TypeVector(componentType, 4), componentKind, isStorage, isArrayed, dimension)); _interfaces.Add(variable); } } private static bool RequiresExtendedStorageImageFormat( SpirvImageFormat format) => format is not SpirvImageFormat.Unknown and not SpirvImageFormat.Rgba32f and not SpirvImageFormat.Rgba32i and not SpirvImageFormat.Rgba32ui; private static (SpirvImageFormat Format, ImageComponentKind Kind) DecodeImageFormat(IReadOnlyList descriptor) { if (descriptor.Count < 2) { return (SpirvImageFormat.Unknown, ImageComponentKind.Float); } var unifiedFormat = (descriptor[1] >> 20) & 0x1FFu; if (!Gfx10UnifiedFormat.TryDecode( unifiedFormat, out var dataFormat, out var numberType)) { return (SpirvImageFormat.Unknown, ImageComponentKind.Float); } var kind = numberType switch { 4 => ImageComponentKind.Uint, 5 => ImageComponentKind.Sint, _ => ImageComponentKind.Float, }; return (DecodeStorageImageFormat(dataFormat, numberType), kind); } internal static SpirvImageFormat DecodeStorageImageFormat( uint dataFormat, uint numberType) => (dataFormat, numberType) switch { (1, 0 or 9) => SpirvImageFormat.R8, (1, 1) => SpirvImageFormat.R8Snorm, (1, 4) => SpirvImageFormat.R8ui, (1, 5) => SpirvImageFormat.R8i, (2, 0) => SpirvImageFormat.R16, (2, 1) => SpirvImageFormat.R16Snorm, (2, 4) => SpirvImageFormat.R16ui, (2, 5) => SpirvImageFormat.R16i, (2, 7) => SpirvImageFormat.R16f, (3, 0 or 9) => SpirvImageFormat.Rg8, (3, 1) => SpirvImageFormat.Rg8Snorm, (3, 4) => SpirvImageFormat.Rg8ui, (3, 5) => SpirvImageFormat.Rg8i, (4, 4) => SpirvImageFormat.R32ui, (4, 5) => SpirvImageFormat.R32i, (4, 7) => SpirvImageFormat.R32f, (5, 0) => SpirvImageFormat.Rg16, (5, 1) => SpirvImageFormat.Rg16Snorm, (5, 4) => SpirvImageFormat.Rg16ui, (5, 5) => SpirvImageFormat.Rg16i, (5, 7) => SpirvImageFormat.Rg16f, (6 or 7, 7) => SpirvImageFormat.R11fG11fB10f, (8 or 9, 0) => SpirvImageFormat.Rgb10A2, (8 or 9, 4) => SpirvImageFormat.Rgb10A2ui, (10, 0 or 9) => SpirvImageFormat.Rgba8, (10, 1) => SpirvImageFormat.Rgba8Snorm, (10, 4) => SpirvImageFormat.Rgba8ui, (10, 5) => SpirvImageFormat.Rgba8i, (11, 4) => SpirvImageFormat.Rg32ui, (11, 5) => SpirvImageFormat.Rg32i, (11, 7) => SpirvImageFormat.Rg32f, (12, 0) => SpirvImageFormat.Rgba16, (12, 1) => SpirvImageFormat.Rgba16Snorm, (12, 4) => SpirvImageFormat.Rgba16ui, (12, 5) => SpirvImageFormat.Rgba16i, (12, 7) => SpirvImageFormat.Rgba16f, (13 or 14, 4) => SpirvImageFormat.Rgba32ui, (13 or 14, 5) => SpirvImageFormat.Rgba32i, (13 or 14, 7) => SpirvImageFormat.Rgba32f, _ => SpirvImageFormat.Unknown, }; private void DeclareStageInterface() { if (UsesSubgroupOperations()) { var subgroupPointer = _module.TypePointer(SpirvStorageClass.Input, _uintType); _subgroupInvocationIdInput = _module.AddGlobalVariable( subgroupPointer, SpirvStorageClass.Input); _module.AddDecoration( _subgroupInvocationIdInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.SubgroupLocalInvocationId); _interfaces.Add(_subgroupInvocationIdInput); if (_emulateWave64) { _subgroupSizeInput = _module.AddGlobalVariable( subgroupPointer, SpirvStorageClass.Input); _module.AddDecoration( _subgroupSizeInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.SubgroupSize); _interfaces.Add(_subgroupSizeInput); } if (_waveLaneCount == 64) { _localInvocationIndexInput = _module.AddGlobalVariable( subgroupPointer, SpirvStorageClass.Input); _module.AddDecoration( _localInvocationIndexInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.LocalInvocationIndex); _interfaces.Add(_localInvocationIndexInput); } } if (_stage == Gen5SpirvStage.Vertex) { DeclareVertexInputs(); var inputPointer = _module.TypePointer(SpirvStorageClass.Input, _uintType); _vertexIndexInput = _module.AddGlobalVariable( inputPointer, SpirvStorageClass.Input); _module.AddDecoration( _vertexIndexInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.VertexIndex); _interfaces.Add(_vertexIndexInput); _instanceIndexInput = _module.AddGlobalVariable( inputPointer, SpirvStorageClass.Input); _module.AddDecoration( _instanceIndexInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.InstanceIndex); _interfaces.Add(_instanceIndexInput); var outputPointer = _module.TypePointer(SpirvStorageClass.Output, _vec4Type); _positionOutput = _module.AddGlobalVariable( outputPointer, SpirvStorageClass.Output); _module.AddDecoration( _positionOutput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position); _interfaces.Add(_positionOutput); var parameters = _state.Program.Instructions .Select(instruction => instruction.Control) .OfType() .Where(export => export.Target is >= 32 and < 64) .Select(export => export.Target - 32) // 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 are // zero-filled in EmitInitialState. .Concat(Enumerable .Range(0, Math.Max(_requiredVertexOutputCount, 0)) .Select(location => (uint)location)) .Distinct() .Order() .ToArray(); foreach (var parameter in parameters) { var variable = _module.AddGlobalVariable( outputPointer, SpirvStorageClass.Output); _module.AddDecoration(variable, SpirvDecoration.Location, parameter); _vertexOutputs.Add(parameter, variable); _interfaces.Add(variable); } } else if (_stage == Gen5SpirvStage.Pixel) { var inputVec4Pointer = _module.TypePointer(SpirvStorageClass.Input, _vec4Type); var attributes = _state.Program.Instructions .Select(instruction => instruction.Control) .OfType() .Select(control => control.Attribute) .Distinct() .Order() .ToArray(); foreach (var attribute in attributes) { var variable = _module.AddGlobalVariable( inputVec4Pointer, SpirvStorageClass.Input); _module.AddDecoration(variable, SpirvDecoration.Location, attribute); _pixelInputs.Add(attribute, variable); _interfaces.Add(variable); } _fragCoordInput = _module.AddGlobalVariable( inputVec4Pointer, SpirvStorageClass.Input); _module.AddDecoration( _fragCoordInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.FragCoord); _interfaces.Add(_fragCoordInput); var declaredPixelOutputs = Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_TITLE_SINGLE_MRT") == "1" && _state.Program.Address == 0x0000000500781200ul ? _pixelOutputBindings.Take(1) : _pixelOutputBindings; foreach (var binding in declaredPixelOutputs) { var outputType = GetPixelOutputType(binding.Kind); var outputPointer = _module.TypePointer(SpirvStorageClass.Output, outputType); var variable = _module.AddGlobalVariable( outputPointer, SpirvStorageClass.Output); _module.AddName(variable, $"mrt{binding.GuestSlot}"); _module.AddDecoration( variable, SpirvDecoration.Location, binding.HostLocation); _pixelOutputs.Add( binding.GuestSlot, new SpirvPixelOutput(variable, outputType, binding.Kind)); _interfaces.Add(variable); } } else { var inputPointer = _module.TypePointer(SpirvStorageClass.Input, _uvec3Type); _localInvocationIdInput = _module.AddGlobalVariable( inputPointer, SpirvStorageClass.Input); _module.AddDecoration( _localInvocationIdInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.LocalInvocationId); _workGroupIdInput = _module.AddGlobalVariable( inputPointer, SpirvStorageClass.Input); _module.AddDecoration( _workGroupIdInput, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.WorkgroupId); _interfaces.Add(_localInvocationIdInput); _interfaces.Add(_workGroupIdInput); } } private void DeclareVertexInputs() { foreach (var input in _evaluation.VertexInputs ?? []) { var componentKind = input.NumberFormat switch { 4 => VertexInputComponentKind.Uint, 5 => VertexInputComponentKind.Sint, _ => VertexInputComponentKind.Float, }; var componentType = componentKind switch { VertexInputComponentKind.Uint => _uintType, VertexInputComponentKind.Sint => _intType, _ => _floatType, }; var type = input.ComponentCount switch { 1u => componentType, >= 2u and <= 4u => _module.TypeVector(componentType, input.ComponentCount), _ => 0u, }; if (type == 0) { continue; } var pointer = _module.TypePointer(SpirvStorageClass.Input, type); var variable = _module.AddGlobalVariable( pointer, SpirvStorageClass.Input); _module.AddName(variable, $"attr{input.Location}"); _module.AddDecoration( variable, SpirvDecoration.Location, input.Location); _vertexInputsByPc.TryAdd( input.Pc, new SpirvVertexInput( variable, type, componentType, input.ComponentCount, componentKind)); _interfaces.Add(variable); } } private void EmitInitialState() { if (_initialScalarBufferIndex >= 0) { // Initial scalar registers arrive in a per-draw buffer instead // of being baked as constants, so animated user data (colors, // scroll offsets) reuses one translation and pipeline. Only // registers the program can observe need loading. var consumed = Gen5ShaderTranslator.ComputeConsumedScalarMask(_state.Program); for (uint index = 0; index < _evaluation.InitialScalarRegisters.Count && index < ScalarRegisterCount; index++) { if (Gen5ShaderTranslator.IsScalarConsumed(consumed, index)) { StoreS( index, LoadBufferWord(_initialScalarBufferIndex, UInt(index))); } } var runtimeBufferBiasCount = _globalBufferBase + _evaluation.GlobalMemoryBindings.Count; for (var binding = 0; binding < runtimeBufferBiasCount; binding++) { Store( RuntimeBufferBiasPointer(binding), LoadBufferWord( _initialScalarBufferIndex, UInt(checked(256u + (uint)binding)))); } } else { for (uint index = 0; index < _evaluation.InitialScalarRegisters.Count && index < ScalarRegisterCount; index++) { var value = _evaluation.InitialScalarRegisters[(int)index]; if (value != 0) { StoreS(index, UInt(value)); } } } Store(_scc, _module.ConstantBool(false)); Store(_reachedPixelExport, _module.ConstantBool(false)); if (_subgroupInvocationIdInput != 0) { StoreWaveMask(106, _module.ConstantBool(false)); StoreWaveMask(126, _module.ConstantBool(true)); } else { // Graphics stages emulate one logical wave lane. Keep the // guest-visible VCC/EXEC scalar pairs synchronized with the // internal booleans: shaders commonly save EXEC from s126:s127 // and restore it after divergent work. Initializing only _exec // left those registers at zero, so the first restore disabled // every fragment before its color export. StoreS64(106, _module.Constant64(_ulongType, 0)); StoreS64(126, _module.Constant64(_ulongType, 1)); } Store(_programCounter, UInt(0)); Store(_programActive, _module.ConstantBool(true)); if (_stage == Gen5SpirvStage.Vertex) { StoreV(5, Load(_uintType, _vertexIndexInput), guardWithExec: false); StoreV(8, Load(_uintType, _instanceIndexInput), guardWithExec: false); // Give every declared param output a defined starting value. // Outputs the program actually exports overwrite this; the // extras that only exist to satisfy the fragment interface stay // zero. The explicit store also keeps SPIRV-Cross from pruning // an unexported output (which would re-break the interface). foreach (var output in _vertexOutputs.Values) { Store(output, _module.ConstantNull(_vec4Type)); } } else if (_stage == Gen5SpirvStage.Pixel) { var fragCoord = Load(_vec4Type, _fragCoordInput); EmitPixelInputState(fragCoord); foreach (var output in _pixelOutputs.Values) { Store(output.Variable, _module.ConstantNull(output.Type)); } } else { var localId = Load(_uvec3Type, _localInvocationIdInput); var workGroupId = Load(_uvec3Type, _workGroupIdInput); var invocationInBounds = _module.ConstantBool(true); for (uint component = 0; component < 3; component++) { var localComponent = _module.AddInstruction( SpirvOp.CompositeExtract, _uintType, localId, component); StoreV(component, localComponent, guardWithExec: false); var groupComponent = _module.AddInstruction( SpirvOp.CompositeExtract, _uintType, workGroupId, component); var localSize = component switch { 0 => _localSizeX, 1 => _localSizeY, _ => _localSizeZ, }; var globalComponent = IAdd( _module.AddInstruction( SpirvOp.IMul, _uintType, groupComponent, UInt(localSize)), localComponent); var limitPointer = _module.AddInstruction( SpirvOp.AccessChain, _pushConstantUintPointer, _computeDispatchLimit, UInt(0), UInt(component)); var componentInBounds = _module.AddInstruction( SpirvOp.ULessThan, _boolType, globalComponent, Load(_uintType, limitPointer)); invocationInBounds = _module.AddInstruction( SpirvOp.LogicalAnd, _boolType, invocationInBounds, componentInBounds); } Store(_programActive, invocationInBounds); if (_state.ComputeSystemRegisters is { } registers) { StoreComputeSystemRegister( registers.WorkGroupXRegister, workGroupId, 0); StoreComputeSystemRegister( registers.WorkGroupYRegister, workGroupId, 1); StoreComputeSystemRegister( registers.WorkGroupZRegister, workGroupId, 2); if (registers.ThreadGroupSizeRegister is { } sizeRegister) { StoreS( sizeRegister, UInt(checked(_localSizeX * _localSizeY * _localSizeZ))); } } } } private void EmitPixelInputState(uint fragCoord) { uint vgpr = 0; // Pixel input VGPRs are compacted in SPI_PS_INPUT_ADDR order. The // interpolation inputs occupy register slots even though V_INTERP // is lowered directly from SPIR-V interpolants; position inputs // following them must still land in the hardware-selected VGPRs. AdvancePixelInput(0, 2, ref vgpr); // PERSP_SAMPLE AdvancePixelInput(1, 2, ref vgpr); // PERSP_CENTER AdvancePixelInput(2, 2, ref vgpr); // PERSP_CENTROID AdvancePixelInput(3, 3, ref vgpr); // PERSP_PULL_MODEL AdvancePixelInput(4, 2, ref vgpr); // LINEAR_SAMPLE AdvancePixelInput(5, 2, ref vgpr); // LINEAR_CENTER AdvancePixelInput(6, 2, ref vgpr); // LINEAR_CENTROID AdvancePixelInput(7, 1, ref vgpr); // LINE_STIPPLE EmitPixelPositionInput(8, 0, fragCoord, ref vgpr); // POS_X_FLOAT EmitPixelPositionInput(9, 1, fragCoord, ref vgpr); // POS_Y_FLOAT EmitPixelPositionInput(10, 2, fragCoord, ref vgpr); // POS_Z_FLOAT EmitPixelPositionInput(11, 3, fragCoord, ref vgpr); // POS_W_FLOAT // FRONT_FACE, ANCILLARY, SAMPLE_COVERAGE and POS_FIXED_PT follow // position inputs. Reserve their compact slots until their SPIR-V // builtins are needed by a guest shader. AdvancePixelInput(12, 1, ref vgpr); AdvancePixelInput(13, 1, ref vgpr); AdvancePixelInput(14, 1, ref vgpr); AdvancePixelInput(15, 1, ref vgpr); } private void AdvancePixelInput(int bit, uint dwordCount, ref uint vgpr) { if ((_pixelInputAddress & (1u << bit)) != 0) { vgpr += dwordCount; } } private void EmitPixelPositionInput( int bit, uint component, uint fragCoord, ref uint vgpr) { var mask = 1u << bit; if ((_pixelInputAddress & mask) == 0) { return; } if ((_pixelInputEnable & mask) != 0) { var value = _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, fragCoord, component); StoreV(vgpr, Bitcast(_uintType, value), guardWithExec: false); } vgpr++; } private void StoreComputeSystemRegister( uint? register, uint workGroupId, uint component) { if (register is null) { return; } var value = _module.AddInstruction( SpirvOp.CompositeExtract, _uintType, workGroupId, component); StoreS(register.Value, value); } private bool TryEmitBlock( IReadOnlyList blocks, int blockIndex, out string error) { error = string.Empty; var block = blocks[blockIndex]; for (var index = block.StartIndex; index < block.EndIndex; index++) { var instruction = _state.Program.Instructions[index]; if (IsBranch(instruction.Opcode) || instruction.Opcode == "SEndpgm") { continue; } if (!TryEmitInstruction(instruction, out error)) { error = $"pc=0x{instruction.Pc:X} {instruction.Opcode}: {error}"; return false; } CapturePixelVgprs(instruction); CapturePixelVgprPoints(instruction); MarkPixelPath(instruction); CapturePixelExec(instruction); } var terminator = _state.Program.Instructions[block.EndIndex - 1]; if (terminator.Opcode == "SEndpgm") { Store(_programActive, _module.ConstantBool(false)); return true; } var fallthrough = blockIndex + 1 < blocks.Count ? (uint)(blockIndex + 1) : uint.MaxValue; if (terminator.Opcode == "SBranch") { if (!TryGetBranchTargetPc(terminator, out var targetPc)) { error = "invalid scalar branch target"; return false; } if (IsExitBranchTarget(_state.Program.Instructions, targetPc)) { Store(_programActive, _module.ConstantBool(false)); return true; } if (!TryFindBlock(blocks, targetPc, out var targetBlock)) { error = $"invalid scalar branch target pc=0x{terminator.Pc:X} target=0x{targetPc:X} blocks={FormatBlockStarts(blocks)}"; return false; } Store(_programCounter, UInt((uint)targetBlock)); return true; } if (terminator.Opcode.StartsWith("SCbranch", StringComparison.Ordinal)) { var hasTarget = TryGetBranchTargetPc(terminator, out var targetPc); var targetBlock = -1; var hasTargetBlock = hasTarget && TryFindBlock(blocks, targetPc, out targetBlock); var targetExits = hasTarget && IsExitBranchTarget(_state.Program.Instructions, targetPc); var hasCondition = TryGetBranchCondition(terminator.Opcode, out var condition); if (!hasTarget || (!hasTargetBlock && !targetExits) || !hasCondition) { error = $"invalid conditional scalar branch opcode={terminator.Opcode} " + $"pc=0x{terminator.Pc:X} " + $"target={(hasTarget ? $"0x{targetPc:X}" : "invalid")} " + $"target_block={(hasTargetBlock ? targetBlock.ToString() : targetExits ? "exit" : "missing")} " + $"fallthrough={(fallthrough == uint.MaxValue ? "end" : fallthrough.ToString())} " + $"condition={hasCondition} " + $"blocks={FormatBlockStarts(blocks)}"; return false; } var takenBlock = targetExits ? uint.MaxValue : (uint)targetBlock; var selected = _module.AddInstruction( SpirvOp.Select, _uintType, condition, UInt(takenBlock), UInt(fallthrough)); Store(_programCounter, selected); return true; } if (fallthrough == uint.MaxValue) { Store(_programActive, _module.ConstantBool(false)); } else { Store(_programCounter, UInt(fallthrough)); } return true; } private static string FormatBlockStarts(IReadOnlyList blocks) { const int maxBlocks = 32; var count = Math.Min(blocks.Count, maxBlocks); var starts = new string[count]; for (var index = 0; index < count; index++) { starts[index] = $"0x{blocks[index].StartPc:X}"; } return blocks.Count <= maxBlocks ? string.Join(",", starts) : string.Join(",", starts) + $",...({blocks.Count})"; } private static bool IsExitBranchTarget( IReadOnlyList instructions, uint targetPc) { if (instructions.Count == 0) { return false; } var last = instructions[^1]; var lastEndPc = last.Pc + (uint)(last.Words.Count * sizeof(uint)); return targetPc >= lastEndPc; } private bool TryGetBranchCondition(string opcode, out uint condition) { condition = opcode switch { "SCbranchScc0" => LogicalNot(Load(_boolType, _scc)), "SCbranchScc1" => Load(_boolType, _scc), "SCbranchVccz" => LogicalNot(SubgroupAny(Load(_boolType, _vcc))), "SCbranchVccnz" => SubgroupAny(Load(_boolType, _vcc)), "SCbranchExecz" => LogicalNot(SubgroupAny(Load(_boolType, _exec))), "SCbranchExecnz" => SubgroupAny(Load(_boolType, _exec)), _ => 0, }; return condition != 0; } private bool TryEmitInstruction( Gen5ShaderInstruction instruction, out string error) { error = string.Empty; if (instruction.Opcode is "SNop" or "SWaitcnt" or "SInstPrefetch" or "STtraceData" or // 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. "SSendmsg" or "VInterpMovF32") { return true; } if (instruction.Opcode == "SBarrier") { var workgroup = UInt(2); var semantics = UInt(0x108); _module.AddStatement( SpirvOp.ControlBarrier, workgroup, workgroup, semantics); return true; } if (instruction.Control is Gen5ScalarMemoryControl scalarMemory) { return TryEmitScalarMemory(instruction, scalarMemory, out error); } if (instruction.Control is Gen5InterpolationControl interpolation) { return TryEmitInterpolation(instruction, interpolation, out error); } if (instruction.Control is Gen5ImageControl image) { return TryEmitImage(instruction, image, 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.Control is Gen5ExportControl export) { return TryEmitExport(instruction, export, out error); } if (instruction.Control is Gen5DataShareControl) { return TryEmitDataShare(instruction, out error); } if (instruction.Encoding is Gen5ShaderEncoding.Sop1 or Gen5ShaderEncoding.Sop2 or Gen5ShaderEncoding.Sopc or Gen5ShaderEncoding.Sopk) { return TryEmitScalarAlu(instruction, out error); } if (instruction.Encoding is Gen5ShaderEncoding.Sopp or Gen5ShaderEncoding.Smrd or Gen5ShaderEncoding.Smem) { return true; } return TryEmitVectorAlu(instruction, out error); } private bool TryEmitDataShare( Gen5ShaderInstruction instruction, out string error) { error = string.Empty; if (_lds == 0 || _ldsElementPointer == 0 || instruction.Control is not Gen5DataShareControl control) { error = "invalid LDS instruction"; return false; } if (control.Gds) { error = "GDS data share is not implemented"; return false; } switch (instruction.Opcode) { case "DsWriteB32": { if (instruction.Sources.Count < 2) { error = "missing LDS write source"; return false; } var address = GetRawSource(instruction, 0); StoreLds( LdsPointer(address, control.Offset0), GetRawSource(instruction, 1)); return true; } case "DsWriteB64": { if (instruction.Sources.Count < 3) { error = "missing LDS write64 source"; return false; } var address = GetRawSource(instruction, 0); var offset = control.Offset0; StoreLds(LdsPointer(address, offset), GetRawSource(instruction, 1)); StoreLds( LdsPointer(address, offset + sizeof(uint)), GetRawSource(instruction, 2)); return true; } case "DsWriteB96": case "DsWriteB128": { // ds_write_b96 stores 3 consecutive dwords, ds_write_b128 // stores 4, from data0..data0+N at the address's offset. var dwordCount = instruction.Opcode == "DsWriteB128" ? 4 : 3; if (instruction.Sources.Count < 1 + dwordCount) { error = "missing LDS write128 source"; return false; } var address = GetRawSource(instruction, 0); var offset = control.Offset0; for (var dword = 0; dword < dwordCount; dword++) { StoreLds( LdsPointer(address, offset + (uint)(dword * sizeof(uint))), GetRawSource(instruction, 1 + dword)); } return true; } case "DsWrite2B32": case "DsWrite2St64B32": { if (instruction.Sources.Count < 3) { error = "missing LDS write2 source"; return false; } var st64 = instruction.Opcode == "DsWrite2St64B32"; var address = GetRawSource(instruction, 0); StoreLds( LdsPointer( address, EffectiveDsPairOffsetBytes(control.Offset0, st64)), GetRawSource(instruction, 1)); StoreLds( LdsPointer( address, EffectiveDsPairOffsetBytes(control.Offset1, st64)), GetRawSource(instruction, 2)); return true; } case "DsReadB32": { if (instruction.Destinations.Count < 1 || instruction.Sources.Count < 1) { error = "missing LDS read operand"; return false; } var address = GetRawSource(instruction, 0); var value = Load( _uintType, LdsPointer(address, control.Offset0)); StoreV(instruction.Destinations[0].Value, value); return true; } case "DsReadB96": case "DsReadB128": { // ds_read_b96 loads 3 consecutive dwords, ds_read_b128 loads // 4, into dest..dest+N from the address's offset. var dwordCount = instruction.Opcode == "DsReadB128" ? 4 : 3; if (instruction.Destinations.Count < dwordCount || instruction.Sources.Count < 1) { error = "missing LDS read128 operand"; return false; } var address = GetRawSource(instruction, 0); var offset = control.Offset0; for (var dword = 0; dword < dwordCount; dword++) { var value = Load( _uintType, LdsPointer(address, offset + (uint)(dword * sizeof(uint)))); StoreV(instruction.Destinations[dword].Value, value); } return true; } case "DsRead2B32": case "DsRead2St64B32": { if (instruction.Destinations.Count < 2 || instruction.Sources.Count < 1) { error = "missing LDS read2 operand"; return false; } var st64 = instruction.Opcode == "DsRead2St64B32"; var address = GetRawSource(instruction, 0); var first = Load( _uintType, LdsPointer( address, EffectiveDsPairOffsetBytes(control.Offset0, st64))); var second = Load( _uintType, LdsPointer( address, EffectiveDsPairOffsetBytes(control.Offset1, st64))); StoreV(instruction.Destinations[0].Value, first); StoreV(instruction.Destinations[1].Value, second); return true; } default: if (Gen5ShaderTranslator.IsDataShareAtomic(instruction.Opcode)) { return TryEmitDataShareAtomic(instruction, control, out error); } error = $"unsupported LDS opcode {instruction.Opcode}"; return false; } } private static uint EffectiveDsPairOffsetBytes(uint offset, bool st64 = false) => offset * (st64 ? 256u : sizeof(uint)); private uint LdsPointer(uint address, uint offsetBytes) { var addressWithOffset = offsetBytes == 0 ? address : IAdd(address, UInt(offsetBytes)); // Mask the dword index into the array bounds. LDS is a power-of-two // dword count, so this is a no-op for in-range compute addresses but // prevents out-of-bounds access when a graphics-stage scratch write // uses an arbitrary computed byte address. var index = BitwiseAnd( ShiftRightLogical(addressWithOffset, UInt(2)), UInt(_ldsDwordMask)); return _module.AddInstruction( SpirvOp.AccessChain, _ldsElementPointer, _lds, index); } private void StoreLds(uint pointer, uint value) { var active = Load(_boolType, _exec); var oldValue = Load(_uintType, pointer); var selected = _module.AddInstruction( SpirvOp.Select, _uintType, active, value, oldValue); Store(pointer, selected); } private bool TryEmitDataShareAtomic( Gen5ShaderInstruction instruction, Gen5DataShareControl control, out string error) { error = string.Empty; var atomicOp = instruction.Opcode switch { "DsAddU32" or "DsAddRtnU32" => SpirvOp.AtomicIAdd, "DsSubU32" or "DsSubRtnU32" => SpirvOp.AtomicISub, "DsIncU32" or "DsIncRtnU32" => SpirvOp.AtomicIIncrement, "DsDecU32" or "DsDecRtnU32" => SpirvOp.AtomicIDecrement, "DsMinI32" or "DsMinRtnI32" => SpirvOp.AtomicSMin, "DsMaxI32" or "DsMaxRtnI32" => SpirvOp.AtomicSMax, "DsMinU32" or "DsMinRtnU32" => SpirvOp.AtomicUMin, "DsMaxU32" or "DsMaxRtnU32" => SpirvOp.AtomicUMax, "DsAndB32" or "DsAndRtnB32" => SpirvOp.AtomicAnd, "DsOrB32" or "DsOrRtnB32" => SpirvOp.AtomicOr, "DsXorB32" or "DsXorRtnB32" => SpirvOp.AtomicXor, "DsWrxchgRtnB32" => SpirvOp.AtomicExchange, "DsCmpstB32" or "DsCmpstRtnB32" => SpirvOp.AtomicCompareExchange, _ => SpirvOp.Nop, }; if (atomicOp == SpirvOp.Nop) { error = $"unsupported LDS opcode {instruction.Opcode}"; return false; } var address = GetRawSource(instruction, 0); var pointer = LdsPointer(address, control.Offset0); EmitExecConditional(() => { var original = EmitAtomic( atomicOp, _uintType, pointer, scope: 2, semantics: 0x108, // DS_CMPST sources: DATA0 is the comparator, DATA1 the new value. value: () => GetRawSource( instruction, atomicOp == SpirvOp.AtomicCompareExchange ? 2 : 1), comparator: () => GetRawSource(instruction, 1)); if (instruction.Destinations.Count > 0) { StoreV(instruction.Destinations[0].Value, original); } }); return true; } // Maps the AMD atomic-op name suffix shared by buffer/image atomics to a SPIR-V opcode. // Inc/Dec approximate the AMD wrap-clamp semantics (MEM = tmp >= DATA ? 0 : tmp + 1), // which is exact for the common 0xFFFFFFFF clamp operand. private static bool TryGetAtomicOp(string name, out SpirvOp op) { op = name switch { "Swap" => SpirvOp.AtomicExchange, "Cmpswap" => SpirvOp.AtomicCompareExchange, "Add" => SpirvOp.AtomicIAdd, "Sub" => SpirvOp.AtomicISub, "Smin" => SpirvOp.AtomicSMin, "Umin" => SpirvOp.AtomicUMin, "Smax" => SpirvOp.AtomicSMax, "Umax" => SpirvOp.AtomicUMax, "And" => SpirvOp.AtomicAnd, "Or" => SpirvOp.AtomicOr, "Xor" => SpirvOp.AtomicXor, "Inc" => SpirvOp.AtomicIIncrement, "Dec" => SpirvOp.AtomicIDecrement, _ => SpirvOp.Nop, }; return op != SpirvOp.Nop; } private uint EmitAtomic( SpirvOp op, uint type, uint pointer, uint scope, uint semantics, Func value, Func comparator) { if (op is SpirvOp.AtomicIIncrement or SpirvOp.AtomicIDecrement) { return _module.AddInstruction( op, type, pointer, UInt(scope), UInt(semantics)); } if (op == SpirvOp.AtomicCompareExchange) { // The unequal semantics must not contain Release; downgrade it to Acquire. return _module.AddInstruction( op, type, pointer, UInt(scope), UInt(semantics), UInt((semantics & ~0x8u) | 0x2u), value(), comparator()); } return _module.AddInstruction( op, type, pointer, UInt(scope), UInt(semantics), value()); } private bool TryEmitInterpolation( Gen5ShaderInstruction instruction, Gen5InterpolationControl interpolation, out string error) { error = string.Empty; if (_stage != Gen5SpirvStage.Pixel || !_pixelInputs.TryGetValue(interpolation.Attribute, out var input) || !TryGetVectorDestination(instruction, out var destination)) { error = "invalid interpolated attribute"; return false; } var vector = Load(_vec4Type, input); var component = _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, vector, interpolation.Channel); StoreV(destination, Bitcast(_uintType, component)); return true; } 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) { StoreS(destination.Value, UInt(0)); } } return true; } var dynamicOffset = control.DynamicOffsetRegister is { } register ? LoadS(register) : UInt(0); var byteAddress = IAdd( dynamicOffset, UInt(unchecked((uint)control.ImmediateOffsetBytes))); byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress); var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); 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; } var address = index == 0 ? dwordAddress : IAdd(dwordAddress, UInt((uint)index)); StoreS(destination.Value, LoadBufferWord(bindingIndex, address)); } 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 memoryOpcode = control.UsesFlatAddress ? "Global" + instruction.Opcode["Flat".Length..] : instruction.Opcode; var vectorByteAddress = LoadV(control.VectorAddress); if (control.UsesFlatAddress) { // FLAT instructions carry the complete 64-bit guest address // in a VGPR pair. The scalar evaluator captures the buffer // rooted at the inferred SGPR pair, so convert the low address // dword back to a byte offset inside that binding. Subtraction // modulo 2^32 also handles an address addition that carried // into the high dword, while the bounded binding prevents an // unrelated pointer from escaping into host storage. vectorByteAddress = _module.AddInstruction( SpirvOp.ISub, _uintType, vectorByteAddress, LoadS(control.ScalarAddress)); } var byteAddress = IAdd( vectorByteAddress, UInt(unchecked((uint)control.OffsetBytes))); byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress); var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); if (memoryOpcode is "GlobalAtomicAdd" or "GlobalAtomicUMax") { EmitExecConditional(() => { EmitConditional(IsBufferWordInRange(bindingIndex, dwordAddress), () => { var original = _module.AddInstruction( memoryOpcode == "GlobalAtomicAdd" ? SpirvOp.AtomicIAdd : SpirvOp.AtomicUMax, _uintType, BufferWordPointer(bindingIndex, dwordAddress), UInt(1), UInt(0x48), LoadV(control.VectorData)); if (control.Glc) { StoreV(control.VectorData, original); } }); }); return true; } if (memoryOpcode.StartsWith("GlobalStore", StringComparison.Ordinal)) { EmitExecConditional(() => { if (TryGetSubdwordStoreInfo( memoryOpcode, out var byteCount, out var sourceShift)) { StoreBufferBytes( bindingIndex, byteAddress, LoadV(control.VectorData), byteCount, sourceShift); return; } for (uint index = 0; index < control.DwordCount; index++) { var address = index == 0 ? byteAddress : IAdd(byteAddress, UInt(index * sizeof(uint))); StoreBufferBytes( bindingIndex, address, LoadV(control.VectorData + index), sizeof(uint), 0); } }); return true; } if (TryGetSubdwordLoadInfo( memoryOpcode, out var loadByteCount, out var signExtend, out var d16, out var d16High)) { StoreV( control.VectorData, LoadSubdwordBufferValue( bindingIndex, byteAddress, LoadV(control.VectorData), loadByteCount, signExtend, d16, d16High)); return true; } for (uint index = 0; index < control.DwordCount; index++) { var address = index == 0 ? byteAddress : IAdd(byteAddress, UInt(index * sizeof(uint))); StoreV( control.VectorData + index, LoadUnalignedBufferWord(bindingIndex, address)); } return true; } private bool TryEmitBufferMemory( Gen5ShaderInstruction instruction, Gen5BufferMemoryControl control, out string error) { error = string.Empty; if (_stage == Gen5SpirvStage.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 ? GetRawSource(instruction, 2) : UInt(0); var stride = ShiftRightLogical(LoadS(control.ScalarResource + 1), UInt(16)); stride = BitwiseAnd(stride, UInt(0x3FFF)); var vectorIndex = control.IndexEnabled ? LoadV(control.VectorAddress) : UInt(0); var vectorOffset = control.OffsetEnabled ? LoadV(control.VectorAddress + (control.IndexEnabled ? 1u : 0u)) : UInt(0); var byteAddress = IAdd( UInt(unchecked((uint)control.OffsetBytes)), scalarOffset); byteAddress = IAdd(byteAddress, vectorOffset); byteAddress = IAdd( byteAddress, _module.AddInstruction(SpirvOp.IMul, _uintType, vectorIndex, stride)); byteAddress = ApplyGuestBufferByteBias(bindingIndex, byteAddress); var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal)) { if (!TryGetAtomicOp(instruction.Opcode["BufferAtomic".Length..], out var atomicOp)) { error = $"unsupported buffer opcode {instruction.Opcode}"; return false; } EmitExecConditional(() => { var inRange = IsBufferWordInRange(bindingIndex, dwordAddress); EmitConditional(inRange, () => { var original = EmitAtomic( atomicOp, _uintType, BufferWordPointer(bindingIndex, dwordAddress), scope: 1, semantics: 0x48, value: () => LoadV(control.VectorData), comparator: () => LoadV(control.VectorData + 1)); if (control.Glc) { StoreV(control.VectorData, original); } }); }); return true; } if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal) || instruction.Opcode.StartsWith("BufferStoreFormat", StringComparison.Ordinal) || instruction.Opcode.StartsWith("BufferStoreByte", StringComparison.Ordinal) || instruction.Opcode.StartsWith("BufferStoreShort", StringComparison.Ordinal)) { EmitExecConditional(() => { if (TryGetSubdwordStoreInfo( instruction.Opcode, out var byteCount, out var sourceShift)) { StoreBufferBytes( bindingIndex, byteAddress, LoadV(control.VectorData), byteCount, sourceShift); return; } for (uint index = 0; index < control.DwordCount; index++) { var address = index == 0 ? byteAddress : IAdd(byteAddress, UInt(index * sizeof(uint))); StoreBufferBytes( bindingIndex, address, LoadV(control.VectorData + index), sizeof(uint), 0); } }); return true; } if (TryGetSubdwordLoadInfo( instruction.Opcode, out var loadByteCount, out var signExtend, out var d16, out var d16High)) { StoreV( control.VectorData, LoadSubdwordBufferValue( bindingIndex, byteAddress, LoadV(control.VectorData), loadByteCount, signExtend, d16, d16High)); return true; } if (!instruction.Opcode.StartsWith("BufferLoad", StringComparison.Ordinal) && !instruction.Opcode.StartsWith("TBufferLoad", StringComparison.Ordinal)) { error = $"unsupported buffer opcode {instruction.Opcode}"; return false; } // MUBUF format loads take their element format and destination // swizzle from the GFX10 buffer descriptor. Keep raw dword loads // on the byte-address >> 2 path below: unlike typed loads they do // not perform component conversion or dst_sel processing. // Vertex shaders normally expose indexed format loads as Vulkan // attributes. Loads with an additional per-lane byte offset cannot // be represented by a fixed attribute description, so the scalar // evaluator captures their descriptor as storage instead. Preserve // typed conversion for both MUBUF and MTBUF in that fallback path. if (IsFormatBufferLoad(instruction.Opcode)) { EmitBufferFormatLoad( bindingIndex, byteAddress, control.ScalarResource, control.VectorData, control.DwordCount); return true; } for (uint index = 0; index < control.DwordCount; index++) { var address = index == 0 ? byteAddress : IAdd(byteAddress, UInt(index * sizeof(uint))); StoreV( control.VectorData + index, LoadUnalignedBufferWord(bindingIndex, address)); } return true; } private void EmitBufferFormatLoad( int bindingIndex, uint byteAddress, uint scalarResource, uint vectorData, uint componentCount) { var descriptorWord3 = LoadS(scalarResource + 3); var unifiedFormat = BitwiseAnd( ShiftRightLogical(descriptorWord3, UInt(12)), UInt(0x7F)); var (dataFormat, numberFormat) = DecodeGfx10BufferFormat(unifiedFormat); var canonical = new uint[4]; for (var component = 0; component < canonical.Length; component++) { canonical[component] = LoadGfx10BufferFormatComponent( bindingIndex, byteAddress, dataFormat, numberFormat, component); } var one = Gfx10FormatOne(numberFormat); for (uint destination = 0; destination < componentCount; destination++) { var selector = BitwiseAnd( ShiftRightLogical(descriptorWord3, UInt(destination * 3)), UInt(7)); var value = UInt(0); value = SelectUInt(selector, 1, one, value); value = SelectUInt(selector, 4, canonical[0], value); value = SelectUInt(selector, 5, canonical[1], value); value = SelectUInt(selector, 6, canonical[2], value); value = SelectUInt(selector, 7, canonical[3], value); StoreV(vectorData + destination, value); } } private (uint DataFormat, uint NumberFormat) DecodeGfx10BufferFormat( uint unifiedFormat) { // The descriptor is loaded at execution time, so format decoding // must remain dynamic too. Generate one module-level lookup table // from the same authoritative decoder used by descriptor // evaluation rather than specializing the shader to the SRD seen // at compile time (compiled compute shaders may be reused with new // SRDs). A table also avoids emitting 77 compares at every format // load site, which matters in buffer-heavy compute kernels. if (_gfx10BufferFormatTable == 0) { const uint formatCount = 128; var entries = new uint[formatCount]; for (uint format = 0; format < formatCount; format++) { Gfx10UnifiedFormat.TryDecode( format, out var decodedDataFormat, out var decodedNumberFormat); entries[format] = UInt( decodedDataFormat | (decodedNumberFormat << 8)); } var tableType = _module.TypeArray(_uintType, formatCount); var tablePointer = _module.TypePointer( SpirvStorageClass.Private, tableType); _gfx10BufferFormatTable = _module.AddGlobalVariable( tablePointer, SpirvStorageClass.Private, _module.ConstantComposite(tableType, entries)); _module.AddName(_gfx10BufferFormatTable, "gfx10BufferFormats"); _interfaces.Add(_gfx10BufferFormatTable); } var entryPointer = _module.AddInstruction( SpirvOp.AccessChain, _privateUintPointer, _gfx10BufferFormatTable, unifiedFormat); var entry = Load(_uintType, entryPointer); return ( BitwiseAnd(entry, UInt(0xFF)), BitwiseAnd( ShiftRightLogical(entry, UInt(8)), UInt(0xFF))); } private uint LoadGfx10BufferFormatComponent( int bindingIndex, uint elementAddress, uint dataFormat, uint numberFormat, int component) { var byteOffset = UInt(0); var bitOffset = UInt(0); var bitCount = UInt(0); void SetLayout(uint format, uint bytes, uint bits, uint count) { var matches = _module.AddInstruction( SpirvOp.IEqual, _boolType, dataFormat, UInt(format)); byteOffset = _module.AddInstruction( SpirvOp.Select, _uintType, matches, UInt(bytes), byteOffset); bitOffset = _module.AddInstruction( SpirvOp.Select, _uintType, matches, UInt(bits), bitOffset); bitCount = _module.AddInstruction( SpirvOp.Select, _uintType, matches, UInt(count), bitCount); } // Legacy DATA_FORMAT layouts selected by the GFX10 unified format. // Packed formats keep their bit offset in the first dword; byte // offsets are used for naturally aligned vector components. switch (component) { case 0: SetLayout(1, 0, 0, 8); // 8 SetLayout(2, 0, 0, 16); // 16 SetLayout(3, 0, 0, 8); // 8_8 SetLayout(4, 0, 0, 32); // 32 SetLayout(5, 0, 0, 16); // 16_16 SetLayout(6, 0, 0, 10); // 10_11_11 SetLayout(7, 0, 0, 11); // 11_11_10 SetLayout(8, 0, 0, 10); // 10_10_10_2 SetLayout(9, 0, 0, 2); // 2_10_10_10 SetLayout(10, 0, 0, 8); // 8_8_8_8 SetLayout(11, 0, 0, 32); // 32_32 SetLayout(12, 0, 0, 16); // 16_16_16_16 SetLayout(13, 0, 0, 32); // 32_32_32 SetLayout(14, 0, 0, 32); // 32_32_32_32 break; case 1: SetLayout(3, 1, 0, 8); SetLayout(5, 2, 0, 16); SetLayout(6, 0, 10, 11); SetLayout(7, 0, 11, 11); SetLayout(8, 0, 10, 10); SetLayout(9, 0, 2, 10); SetLayout(10, 1, 0, 8); SetLayout(11, 4, 0, 32); SetLayout(12, 2, 0, 16); SetLayout(13, 4, 0, 32); SetLayout(14, 4, 0, 32); break; case 2: SetLayout(6, 0, 21, 11); SetLayout(7, 0, 22, 10); SetLayout(8, 0, 20, 10); SetLayout(9, 0, 12, 10); SetLayout(10, 2, 0, 8); SetLayout(12, 4, 0, 16); SetLayout(13, 8, 0, 32); SetLayout(14, 8, 0, 32); break; case 3: SetLayout(8, 0, 30, 2); SetLayout(9, 0, 22, 10); SetLayout(10, 3, 0, 8); SetLayout(12, 6, 0, 16); SetLayout(14, 12, 0, 32); break; } var packed = LoadUnalignedBufferWord( bindingIndex, IAdd(elementAddress, byteOffset)); var raw = _module.AddInstruction( SpirvOp.BitFieldUExtract, _uintType, packed, bitOffset, bitCount); var converted = ConvertGfx10BufferComponent( raw, bitCount, numberFormat, dataFormat); var valid = _module.AddInstruction( SpirvOp.INotEqual, _boolType, bitCount, UInt(0)); return _module.AddInstruction( SpirvOp.Select, _uintType, valid, converted, component == 3 ? Gfx10FormatOne(numberFormat) : UInt(0)); } private uint ConvertGfx10BufferComponent( uint raw, uint bitCount, uint numberFormat, uint dataFormat) { var widthIs32 = _module.AddInstruction( SpirvOp.IEqual, _boolType, bitCount, UInt(32)); var lowMask = _module.AddInstruction( SpirvOp.ISub, _uintType, ShiftLeftLogical(UInt(1), bitCount), UInt(1)); lowMask = _module.AddInstruction( SpirvOp.Select, _uintType, widthIs32, UInt(uint.MaxValue), lowMask); var signedRaw = _module.AddInstruction( SpirvOp.BitFieldSExtract, _intType, Bitcast(_intType, raw), UInt(0), bitCount); var signedBits = Bitcast(_uintType, signedRaw); var unsignedFloat = _module.AddInstruction( SpirvOp.ConvertUToF, _floatType, raw); var signedFloat = _module.AddInstruction( SpirvOp.ConvertSToF, _floatType, signedRaw); var unorm = Bitcast( _uintType, _module.AddInstruction( SpirvOp.FDiv, _floatType, unsignedFloat, _module.AddInstruction( SpirvOp.ConvertUToF, _floatType, lowMask))); var signedMaximum = ShiftRightLogical(lowMask, UInt(1)); var snormFloat = _module.AddInstruction( SpirvOp.FDiv, _floatType, signedFloat, _module.AddInstruction( SpirvOp.ConvertUToF, _floatType, signedMaximum)); snormFloat = _module.AddInstruction( SpirvOp.Select, _floatType, _module.AddInstruction( SpirvOp.FOrdLessThan, _boolType, snormFloat, Float(-1f)), Float(-1f), snormFloat); var snorm = Bitcast(_uintType, snormFloat); var uscaled = Bitcast(_uintType, unsignedFloat); var sscaled = Bitcast(_uintType, signedFloat); var unpackedHalf = Ext(62, _vec2Type, BitwiseAnd(raw, UInt(0xFFFF))); var half = Bitcast( _uintType, _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, unpackedHalf, 0)); var floating = _module.AddInstruction( SpirvOp.Select, _uintType, _module.AddInstruction( SpirvOp.IEqual, _boolType, bitCount, UInt(16)), half, raw); // DATA_FORMAT 10_11_11 and 11_11_10 use unsigned mini-floats // when NUM_FORMAT is FLOAT, not ordinary integer bit patterns. var isPackedFloat = _module.AddInstruction( SpirvOp.LogicalOr, _boolType, _module.AddInstruction( SpirvOp.IEqual, _boolType, dataFormat, UInt(6)), _module.AddInstruction( SpirvOp.IEqual, _boolType, dataFormat, UInt(7))); floating = _module.AddInstruction( SpirvOp.Select, _uintType, isPackedFloat, DecodeUnsignedMiniFloat(raw, bitCount), floating); var result = raw; result = SelectUInt(numberFormat, 0, unorm, result); result = SelectUInt(numberFormat, 1, snorm, result); result = SelectUInt(numberFormat, 2, uscaled, result); result = SelectUInt(numberFormat, 3, sscaled, result); result = SelectUInt(numberFormat, 4, raw, result); result = SelectUInt(numberFormat, 5, signedBits, result); result = SelectUInt(numberFormat, 7, floating, result); return result; } private uint DecodeUnsignedMiniFloat(uint raw, uint bitCount) { var mantissaBits = _module.AddInstruction( SpirvOp.ISub, _uintType, bitCount, UInt(5)); var mantissaMask = _module.AddInstruction( SpirvOp.ISub, _uintType, ShiftLeftLogical(UInt(1), mantissaBits), UInt(1)); var mantissa = BitwiseAnd(raw, mantissaMask); var exponent = BitwiseAnd( ShiftRightLogical(raw, mantissaBits), UInt(0x1F)); var mantissaShift = _module.AddInstruction( SpirvOp.ISub, _uintType, UInt(23), mantissaBits); var normalBits = BitwiseOr( ShiftLeftLogical(IAdd(exponent, UInt(112)), UInt(23)), ShiftLeftLogical(mantissa, mantissaShift)); var subnormal = Bitcast( _uintType, _module.AddInstruction( SpirvOp.FMul, _floatType, _module.AddInstruction( SpirvOp.ConvertUToF, _floatType, mantissa), _module.AddInstruction( SpirvOp.Select, _floatType, _module.AddInstruction( SpirvOp.IEqual, _boolType, mantissaBits, UInt(6)), Float(1f / 1_048_576f), // 2^-20 for 11-bit UFLOAT Float(1f / 524_288f)))); // 2^-19 for 10-bit UFLOAT var special = BitwiseOr( UInt(0x7F800000), ShiftLeftLogical(mantissa, mantissaShift)); var result = _module.AddInstruction( SpirvOp.Select, _uintType, _module.AddInstruction( SpirvOp.IEqual, _boolType, exponent, UInt(0)), subnormal, normalBits); return _module.AddInstruction( SpirvOp.Select, _uintType, _module.AddInstruction( SpirvOp.IEqual, _boolType, exponent, UInt(31)), special, result); } private uint Gfx10FormatOne(uint numberFormat) { var isUint = _module.AddInstruction( SpirvOp.IEqual, _boolType, numberFormat, UInt(4)); var isSint = _module.AddInstruction( SpirvOp.IEqual, _boolType, numberFormat, UInt(5)); return _module.AddInstruction( SpirvOp.Select, _uintType, _module.AddInstruction( SpirvOp.LogicalOr, _boolType, isUint, isSint), UInt(1), UInt(0x3F800000)); } private uint SelectUInt( uint selector, uint expected, uint whenTrue, uint whenFalse) => _module.AddInstruction( SpirvOp.Select, _uintType, _module.AddInstruction( SpirvOp.IEqual, _boolType, selector, UInt(expected)), whenTrue, whenFalse); private uint LoadUnalignedBufferWord(int bindingIndex, uint byteAddress) { var result = UInt(0); for (uint index = 0; index < 4; index++) { var address = index == 0 ? byteAddress : IAdd(byteAddress, UInt(index)); var dwordAddress = ShiftRightLogical(address, UInt(2)); var bitOffset = ShiftLeftLogical(BitwiseAnd(address, UInt(3)), UInt(3)); var value = BitwiseAnd( ShiftRightLogical(LoadBufferWord(bindingIndex, dwordAddress), bitOffset), UInt(0xFF)); result = BitwiseOr(result, ShiftLeftLogical(value, UInt(index * 8))); } return result; } private uint LoadSubdwordBufferValue( int bindingIndex, uint byteAddress, uint previous, uint byteCount, bool signExtend, bool d16, bool d16High) { var width = byteCount * 8; var raw = BitwiseAnd( LoadUnalignedBufferWord(bindingIndex, byteAddress), UInt(byteCount == 1 ? 0xFFu : 0xFFFFu)); if (signExtend) { raw = Bitcast( _uintType, _module.AddInstruction( SpirvOp.BitFieldSExtract, _intType, Bitcast(_intType, raw), UInt(0), UInt(width))); } if (!d16) { return raw; } var half = BitwiseAnd(raw, UInt(0xFFFF)); return d16High ? BitwiseOr( BitwiseAnd(previous, UInt(0x0000_FFFF)), ShiftLeftLogical(half, UInt(16))) : BitwiseOr( BitwiseAnd(previous, UInt(0xFFFF_0000)), half); } private void StoreBufferBytes( int bindingIndex, uint byteAddress, uint value, uint byteCount, uint sourceShift) { value = ShiftRightLogical(value, UInt(sourceShift)); for (uint index = 0; index < byteCount; index++) { var address = index == 0 ? byteAddress : IAdd(byteAddress, UInt(index)); var dwordAddress = ShiftRightLogical(address, UInt(2)); var shift = ShiftLeftLogical(BitwiseAnd(address, UInt(3)), UInt(3)); var oldValue = LoadBufferWord(bindingIndex, dwordAddress); var byteMask = ShiftLeftLogical(UInt(0xFF), shift); var sourceByte = BitwiseAnd( ShiftRightLogical(value, UInt(index * 8)), UInt(0xFF)); var updated = BitwiseOr( BitwiseAnd( oldValue, _module.AddInstruction(SpirvOp.Not, _uintType, byteMask)), ShiftLeftLogical(sourceByte, shift)); StoreBufferWord(bindingIndex, dwordAddress, updated); } } 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("TBufferLoadFormat", StringComparison.Ordinal); private static bool UsesSampler(string opcode) => opcode.StartsWith("ImageSample", StringComparison.Ordinal) || opcode.StartsWith("ImageGather", StringComparison.Ordinal); private bool TryResolveDominatingBufferBinding( uint pc, uint scalarRegister, uint registerCount, out int bindingIndex) { if (_bufferBindingByPc.TryGetValue(pc, out bindingIndex)) { return true; } for (var index = 0; index < _evaluation.GlobalMemoryBindings.Count; index++) { var binding = _evaluation.GlobalMemoryBindings[index]; if (binding.ScalarAddress != scalarRegister) { continue; } foreach (var candidatePc in binding.InstructionPcs) { if (!HasSameScalarDefinitions( candidatePc, pc, scalarRegister, registerCount)) { continue; } bindingIndex = _globalBufferBase + index; _bufferBindingByPc.Add(pc, bindingIndex); return true; } } bindingIndex = -1; return false; } private bool TryResolveDominatingImageBinding( Gen5ShaderInstruction instruction, Gen5ImageControl control, out int bindingIndex) { if (_imageBindingByPc.TryGetValue(instruction.Pc, out bindingIndex) && bindingIndex < _imageResources.Count) { return true; } var imageLoad = Gen5ShaderTranslator.IsImageLoadOperation(instruction.Opcode); var storage = Gen5ShaderTranslator.IsStorageImageOperation(instruction.Opcode); for (var index = 0; index < _evaluation.ImageBindings.Count; index++) { var candidate = _evaluation.ImageBindings[index]; if (candidate.Control.ScalarResource != control.ScalarResource || candidate.Control.ScalarSampler != control.ScalarSampler || Gen5ShaderTranslator.IsImageLoadOperation(candidate.Opcode) != imageLoad || Gen5ShaderTranslator.IsStorageImageOperation(candidate.Opcode) != storage || !HasSameScalarDefinitions( candidate.Pc, instruction.Pc, control.ScalarResource, ImageDescriptorDwords) || UsesSampler(instruction.Opcode) && !HasSameScalarDefinitions( candidate.Pc, instruction.Pc, control.ScalarSampler, SamplerDescriptorDwords)) { continue; } bindingIndex = index; _imageBindingByPc.Add(instruction.Pc, index); return true; } bindingIndex = -1; return false; } private bool HasSameScalarDefinitions( uint candidatePc, uint targetPc, uint firstRegister, uint registerCount) { if (firstRegister + registerCount > ScalarRegisterCount || !_scalarDefinitionsBeforePc.TryGetValue(candidatePc, out var candidate) || !_scalarDefinitionsBeforePc.TryGetValue(targetPc, out var target)) { return false; } for (var register = firstRegister; register < firstRegister + registerCount; register++) { var definition = candidate[register]; if (definition is ConflictingScalarDefinition or UnreachableScalarDefinition || target[register] != definition) { return false; } } return true; } private bool TryEmitVertexInputFetch( Gen5BufferMemoryControl control, SpirvVertexInput input, out string error) { error = string.Empty; if (control.DwordCount == 0 || control.DwordCount > input.ComponentCount) { error = $"invalid vertex input fetch components={control.DwordCount} " + $"input={input.ComponentCount}"; return false; } var loaded = Load(input.Type, input.Variable); for (uint component = 0; component < control.DwordCount; component++) { var value = input.ComponentCount == 1 ? loaded : _module.AddInstruction( SpirvOp.CompositeExtract, input.ComponentType, loaded, component); var raw = input.ComponentKind == VertexInputComponentKind.Uint ? value : Bitcast(_uintType, value); StoreV(control.VectorData + component, raw); } return true; } private bool TryEmitImage( Gen5ShaderInstruction instruction, Gen5ImageControl image, out string error) { error = string.Empty; if (!TryResolveDominatingImageBinding(instruction, image, out var bindingIndex)) { var candidates = _evaluation.ImageBindings .Where(binding => binding.Control.ScalarResource == image.ScalarResource && binding.Control.ScalarSampler == image.ScalarSampler) .Take(16) .Select(binding => $"{binding.Opcode}@0x{binding.Pc:X}" + $"/r={HasSameScalarDefinitions(binding.Pc, instruction.Pc, image.ScalarResource, ImageDescriptorDwords)}" + $"/s={!UsesSampler(instruction.Opcode) || HasSameScalarDefinitions(binding.Pc, instruction.Pc, image.ScalarSampler, SamplerDescriptorDwords)}"); error = $"unresolved image binding t=s{image.ScalarResource} " + $"s=s{image.ScalarSampler} " + $"candidates=[{string.Join(',', candidates)}]"; return false; } var resource = _imageResources[bindingIndex]; var imageObject = Load(resource.ObjectType, resource.Variable); if (instruction.Opcode == "ImageGetResinfo") { var sizeComponentCount = ImageCoordinateComponentCount(resource); var queryImage = resource.IsStorage ? imageObject : _module.AddInstruction( SpirvOp.Image, resource.ImageType, imageObject); var size = _module.AddInstruction( resource.IsStorage ? SpirvOp.ImageQuerySize : SpirvOp.ImageQuerySizeLod, _module.TypeVector(_intType, sizeComponentCount), resource.IsStorage ? [queryImage] : [queryImage, UInt(0)]); uint outputIndex = 0; for (uint component = 0; component < 4; component++) { if ((image.Dmask & (1u << (int)component)) == 0) { continue; } uint value; if (component < sizeComponentCount) { var signedValue = _module.AddInstruction( SpirvOp.CompositeExtract, _intType, size, component); value = Bitcast(_uintType, signedValue); } else { value = UInt(1); } StoreV(image.VectorData + outputIndex++, value); } return true; } if (instruction.Opcode is "ImageStore" or "ImageStoreMip") { if (!resource.IsStorage) { error = "image store is not bound as storage"; return false; } var coordinateComponentCount = ImageCoordinateComponentCount(resource); var coordinates = BuildIntegerCoordinates( image, 0, coordinateComponentCount); var components = new uint[4]; uint sourceIndex = 0; for (var component = 0; component < components.Length; component++) { if ((image.Dmask & (1u << component)) != 0) { var raw = LoadImageStoreComponent( image, resource, sourceIndex++); components[component] = resource.ComponentKind switch { ImageComponentKind.Sint => Bitcast(_intType, raw), ImageComponentKind.Uint => raw, _ => Bitcast(_floatType, raw), }; } else { components[component] = resource.ComponentKind switch { ImageComponentKind.Sint => _module.Constant(_intType, 0), ImageComponentKind.Uint => UInt(0), _ => Float(0), }; } } var texel = _module.AddInstruction( SpirvOp.CompositeConstruct, resource.VectorType, components); var imageSize = _module.AddInstruction( SpirvOp.ImageQuerySize, _module.TypeVector(_intType, coordinateComponentCount), imageObject); EmitBoundsCheckedImageWrite( coordinates, imageSize, imageObject, texel, coordinateComponentCount); return true; } if (instruction.Opcode.StartsWith("ImageAtomic", StringComparison.Ordinal)) { if (!resource.IsStorage) { error = "image atomic is not bound as storage"; return false; } if (resource.ComponentKind == ImageComponentKind.Float || !TryGetAtomicOp(instruction.Opcode["ImageAtomic".Length..], out var atomicOp)) { error = $"unsupported storage image opcode {instruction.Opcode}"; return false; } var signed = resource.ComponentKind == ImageComponentKind.Sint; var coordinateComponentCount = ImageCoordinateComponentCount(resource); var atomicImageSize = _module.AddInstruction( SpirvOp.ImageQuerySize, _module.TypeVector(_intType, coordinateComponentCount), imageObject); var coordinates = BuildClampedIntegerCoordinates( image, 0, atomicImageSize, coordinateComponentCount); EmitExecConditional(() => { var pointer = _module.AddInstruction( SpirvOp.ImageTexelPointer, _module.TypePointer(SpirvStorageClass.Image, resource.ComponentType), resource.Variable, coordinates, UInt(0)); uint LoadData(uint register) => signed ? Bitcast(_intType, LoadV(register)) : LoadV(register); var original = EmitAtomic( atomicOp, resource.ComponentType, pointer, scope: 1, semantics: 0x808, value: () => LoadData(image.VectorData), comparator: () => LoadData(image.VectorData + 1)); if (image.Glc) { StoreV( image.VectorData, signed ? Bitcast(_uintType, original) : original); } }); return true; } if (resource.IsStorage && instruction.Opcode is not ("ImageLoad" or "ImageLoadMip")) { error = $"unsupported storage image opcode {instruction.Opcode}"; return false; } uint sampled; var writeAllComponents = false; if (instruction.Opcode is "ImageLoad" or "ImageLoadMip") { if (resource.IsStorage) { var coordinateComponentCount = ImageCoordinateComponentCount(resource); var imageSize = _module.AddInstruction( SpirvOp.ImageQuerySize, _module.TypeVector(_intType, coordinateComponentCount), imageObject); var coordinates = BuildClampedIntegerCoordinates( image, 0, imageSize, coordinateComponentCount); sampled = _module.AddInstruction( SpirvOp.ImageRead, resource.VectorType, imageObject, coordinates); } else { var mipLevel = _evaluation.ImageBindings[bindingIndex].MipLevel ?? 0; var fetchedImage = _module.AddInstruction( SpirvOp.Image, resource.ImageType, imageObject); var coordinateComponentCount = ImageCoordinateComponentCount(resource); var imageSize = _module.AddInstruction( SpirvOp.ImageQuerySizeLod, _module.TypeVector(_intType, coordinateComponentCount), fetchedImage, UInt(mipLevel)); var coordinates = BuildClampedIntegerCoordinates( image, 0, imageSize, coordinateComponentCount); sampled = _module.AddInstruction( SpirvOp.ImageFetch, resource.VectorType, fetchedImage, coordinates, 2, UInt(mipLevel)); } } else if (instruction.Opcode.StartsWith( "ImageSample", StringComparison.Ordinal)) { var hasOffset = instruction.Opcode.EndsWith("O", StringComparison.Ordinal); var hasCompare = instruction.Opcode.Contains("SampleC", StringComparison.Ordinal); var hasGradients = instruction.Opcode.Contains("SampleD", StringComparison.Ordinal); var hasZeroLod = instruction.Opcode.Contains("Lz", StringComparison.Ordinal); var hasLod = !hasZeroLod && instruction.Opcode.Contains("SampleL", StringComparison.Ordinal); var hasBias = instruction.Opcode.Contains("SampleB", StringComparison.Ordinal); // RDNA MIMG address operands are ordered as // {offset}{bias/lod}{z-compare}{derivatives}{body}. The old // lowering treated SAMPLE_D as body-first and consequently // sampled gradients as coordinates in every captured // derivative operation. var spatialComponentCount = ImageSpatialComponentCount(resource); var coordinateComponentCount = ImageCoordinateComponentCount(resource); var addressCursor = 0; var offset = 0u; if (hasOffset) { addressCursor = AlignFullImageAddress(image, addressCursor); offset = BuildImageOffset( image, addressCursor, spatialComponentCount); addressCursor += ImageFullAddressSlots(image); } // SAMPLE_B prefixes the body with a bias. SAMPLE_L instead // carries LOD as the final body component (x, y, lod for 2D), // per the RDNA image-address table. var lodOrBias = hasBias ? LoadImageFloatAddress(image, addressCursor++) : 0u; var reference = 0u; if (hasCompare) { // PCF references remain full-width even when A16 packs the // ordinary address components two per VGPR. addressCursor = AlignFullImageAddress(image, addressCursor); reference = Bitcast( _floatType, LoadV(image.GetAddressRegister( ImageAddressRegister(image, addressCursor)))); addressCursor += ImageFullAddressSlots(image); } var gradientX = hasGradients ? BuildFloatCoordinates( image, addressCursor, spatialComponentCount) : 0u; var gradientY = hasGradients ? BuildFloatCoordinates( image, addressCursor + (int)spatialComponentCount, spatialComponentCount) : 0u; if (hasGradients) { addressCursor += checked((int)(spatialComponentCount * 2)); } var coordinates = BuildFloatCoordinates( image, addressCursor, coordinateComponentCount); var explicitLod = hasGradients || hasZeroLod || hasLod; var lod = hasZeroLod ? Float(0) : hasLod ? LoadImageFloatAddress( image, addressCursor + (int)coordinateComponentCount) : lodOrBias; if (hasOffset) { // Vulkan before maintenance8 forbids the dynamic Offset // image operand on non-gather sampling operations. RDNA // offsets are per-lane VGPR values, so ConstOffset is not // equivalent. Fold the texel offset into normalized sample // coordinates using the queried mip extent instead. var offsetLod = explicitLod && !hasGradients ? lod : Float(0); coordinates = ApplyDynamicSampleOffset( resource, imageObject, coordinates, offset, offsetLod); } var imageOperands = hasGradients ? 4u : explicitLod ? 2u : hasBias ? 1u : 0u; var operands = new List { imageObject, coordinates, }; if (imageOperands != 0) { operands.Add(imageOperands); if (hasGradients) { operands.Add(gradientX); operands.Add(gradientY); } else if (explicitLod) { operands.Add(lod); } else if (hasBias) { operands.Add(lodOrBias); } } sampled = _module.AddInstruction( explicitLod ? SpirvOp.ImageSampleExplicitLod : SpirvOp.ImageSampleImplicitLod, resource.VectorType, [.. operands]); if (hasCompare) { sampled = EmitManualDepthCompare(resource, sampled, reference); } } else if (instruction.Opcode.StartsWith( "ImageGather4", StringComparison.Ordinal)) { var hasOffset = instruction.Opcode.EndsWith("O", StringComparison.Ordinal); var hasCompare = instruction.Opcode.Contains("Gather4C", StringComparison.Ordinal); var spatialComponentCount = ImageSpatialComponentCount(resource); var coordinateComponentCount = ImageCoordinateComponentCount(resource); var addressCursor = 0; var offset = 0u; if (hasOffset) { offset = BuildImageOffset( image, addressCursor, spatialComponentCount); addressCursor += ImageFullAddressSlots(image); } var reference = 0u; if (hasCompare) { addressCursor = AlignFullImageAddress(image, addressCursor); reference = Bitcast( _floatType, LoadV(image.GetAddressRegister( ImageAddressRegister(image, addressCursor)))); addressCursor += ImageFullAddressSlots(image); } var coordinates = BuildFloatCoordinates( image, addressCursor, coordinateComponentCount); var operands = new List { imageObject, coordinates, }; if (hasCompare) { operands.Add(UInt(0)); } else { uint component = 0; while (component < 3 && (image.Dmask & (1u << (int)component)) == 0) { component++; } operands.Add(UInt(component)); } if (hasOffset) { operands.Add(0x10u); operands.Add(offset); } sampled = _module.AddInstruction( SpirvOp.ImageGather, resource.VectorType, [.. operands]); if (hasCompare) { var compared = new uint[4]; for (var component = 0u; component < 4; component++) { var texel = _module.AddInstruction( SpirvOp.CompositeExtract, resource.ComponentType, sampled, component); compared[component] = EmitDepthCompareScalar(resource, texel, reference); } sampled = _module.AddInstruction( SpirvOp.CompositeConstruct, resource.VectorType, compared); } writeAllComponents = true; } else { error = $"unsupported image opcode {instruction.Opcode}"; return false; } var outputValues = new List(4); for (uint component = 0; component < 4; component++) { if (!writeAllComponents && (image.Dmask & (1u << (int)component)) == 0) { continue; } var value = _module.AddInstruction( SpirvOp.CompositeExtract, resource.ComponentType, sampled, component); var raw = resource.ComponentKind switch { ImageComponentKind.Uint => value, _ => Bitcast(_uintType, value), }; outputValues.Add(raw); } if (_stage == Gen5SpirvStage.Pixel && PixelImageCaptureAddressMatches() && uint.TryParse( Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_IMAGE_PC"), out var captureImagePc) && instruction.Pc == captureImagePc) { var captureBase = 248u; if (uint.TryParse( Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_IMAGE_VGPR_BASE"), out var requestedCaptureBase)) { captureBase = requestedCaptureBase; } captureBase = captureBase <= 252 ? captureBase : 248u; for (var component = 0; component < 4; component++) { StoreV( captureBase + (uint)component, component < outputValues.Count ? outputValues[component] : Bitcast(_uintType, Float(1))); } } if (image.D16) { for (var index = 0; index < outputValues.Count; index += 2) { var low = outputValues[index]; var high = index + 1 < outputValues.Count ? outputValues[index + 1] : UInt(0); StoreV( image.VectorData + (uint)(index / 2), PackImageD16(resource, low, high)); } } else { for (var index = 0; index < outputValues.Count; index++) { StoreV(image.VectorData + (uint)index, outputValues[index]); } } return true; } private uint EmitDepthCompareScalar( SpirvImageResource resource, uint texel, uint reference) { var texelAsFloat = resource.ComponentKind switch { ImageComponentKind.Uint => _module.AddInstruction( SpirvOp.ConvertUToF, _floatType, texel), ImageComponentKind.Sint => _module.AddInstruction( SpirvOp.ConvertSToF, _floatType, texel), _ => texel, }; var passes = _module.AddInstruction( SpirvOp.FOrdLessThanEqual, _boolType, reference, texelAsFloat); return _module.AddInstruction( SpirvOp.Select, resource.ComponentType, passes, resource.ComponentKind switch { ImageComponentKind.Uint => UInt(1), ImageComponentKind.Sint => _module.Constant(_intType, 1), _ => Float(1), }, resource.ComponentKind switch { ImageComponentKind.Uint => UInt(0), ImageComponentKind.Sint => _module.Constant(_intType, 0), _ => Float(0), }); } private uint EmitManualDepthCompare( SpirvImageResource resource, uint sampledVector, uint reference) { var texel = _module.AddInstruction( SpirvOp.CompositeExtract, resource.ComponentType, sampledVector, 0u); var scalar = EmitDepthCompareScalar(resource, texel, reference); return _module.AddInstruction( SpirvOp.CompositeConstruct, resource.VectorType, scalar, scalar, scalar, resource.ComponentKind switch { ImageComponentKind.Uint => UInt(1), ImageComponentKind.Sint => _module.Constant(_intType, 1), _ => Float(1), }); } private static uint ImageSpatialComponentCount( SpirvImageResource resource) => resource.Dimension == SpirvImageDim.Dim3D ? 3u : 2u; private static uint ImageCoordinateComponentCount( SpirvImageResource resource) => resource.Arrayed ? 3u : ImageSpatialComponentCount(resource); private uint BuildFloatCoordinates( Gen5ImageControl image, int start, uint componentCount) { var components = new uint[checked((int)componentCount)]; for (var component = 0; component < components.Length; component++) { components[component] = LoadImageFloatAddress( image, start + component); } return _module.AddInstruction( SpirvOp.CompositeConstruct, _module.TypeVector(_floatType, componentCount), components); } private static int ImageAddressRegister( Gen5ImageControl image, int component) => image.A16 ? component / 2 : component; private static int ImageFullAddressSlots(Gen5ImageControl image) => image.A16 ? 2 : 1; private static int AlignFullImageAddress( Gen5ImageControl image, int component) => image.A16 ? (component + 1) & ~1 : component; private uint LoadImageFloatAddress(Gen5ImageControl image, int component) { var raw = LoadV(image.GetAddressRegister( ImageAddressRegister(image, component))); if (!image.A16) { return Bitcast(_floatType, raw); } var unpacked = Ext(62, _vec2Type, raw); return _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, unpacked, (uint)(component & 1)); } private uint LoadImageIntegerAddress(Gen5ImageControl image, int component) { var raw = LoadV(image.GetAddressRegister( ImageAddressRegister(image, component))); if (!image.A16) { return raw; } return BitwiseAnd( ShiftRightLogical(raw, UInt((uint)((component & 1) * 16))), UInt(0xFFFF)); } private uint LoadImageStoreComponent( Gen5ImageControl image, SpirvImageResource resource, uint component) { if (!image.D16) { return LoadV(image.VectorData + component); } var packed = LoadV(image.VectorData + component / 2); if (resource.ComponentKind == ImageComponentKind.Float) { var unpacked = Ext(62, _vec2Type, packed); return Bitcast( _uintType, _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, unpacked, component & 1)); } var shifted = ShiftRightLogical(packed, UInt((component & 1) * 16)); var low = BitwiseAnd(shifted, UInt(0xFFFF)); if (resource.ComponentKind != ImageComponentKind.Sint) { return low; } return Bitcast( _uintType, _module.AddInstruction( SpirvOp.BitFieldSExtract, _intType, Bitcast(_intType, low), UInt(0), UInt(16))); } private uint PackImageD16( SpirvImageResource resource, uint low, uint high) { if (resource.ComponentKind == ImageComponentKind.Float) { var pair = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec2Type, Bitcast(_floatType, low), Bitcast(_floatType, high)); return Ext(58, _uintType, pair); } return BitwiseOr( BitwiseAnd(low, UInt(0xFFFF)), ShiftLeftLogical(BitwiseAnd(high, UInt(0xFFFF)), UInt(16))); } private uint BuildIntegerCoordinates( Gen5ImageControl image, int start, uint componentCount) { var components = new uint[checked((int)componentCount)]; for (var component = 0; component < components.Length; component++) { components[component] = Bitcast( _intType, LoadImageIntegerAddress(image, start + component)); } return _module.AddInstruction( SpirvOp.CompositeConstruct, _module.TypeVector(_intType, componentCount), components); } private uint BuildClampedIntegerCoordinates( Gen5ImageControl image, int start, uint imageSize, uint componentCount) { var components = new uint[checked((int)componentCount)]; for (var component = 0; component < components.Length; component++) { components[component] = ClampSignedCoordinate( Bitcast( _intType, LoadImageIntegerAddress(image, start + component)), _module.AddInstruction( SpirvOp.CompositeExtract, _intType, imageSize, (uint)component)); } return _module.AddInstruction( SpirvOp.CompositeConstruct, _module.TypeVector(_intType, componentCount), components); } private uint ClampSignedCoordinate(uint value, uint extent) { var zero = _module.Constant(_intType, 0); var max = _module.AddInstruction( SpirvOp.ISub, _intType, extent, _module.Constant(_intType, 1)); var belowZero = _module.AddInstruction( SpirvOp.SLessThan, _boolType, value, zero); var atLeastZero = _module.AddInstruction( SpirvOp.Select, _intType, belowZero, zero, value); var aboveMax = _module.AddInstruction( SpirvOp.SGreaterThan, _boolType, atLeastZero, max); return _module.AddInstruction( SpirvOp.Select, _intType, aboveMax, max, atLeastZero); } private void EmitBoundsCheckedImageWrite( uint coordinates, uint imageSize, uint imageObject, uint texel, uint coordinateComponentCount) { var zero = _module.Constant(_intType, 0); var inRange = Load(_boolType, _exec); for (uint component = 0; component < coordinateComponentCount; component++) { var coordinate = _module.AddInstruction( SpirvOp.CompositeExtract, _intType, coordinates, component); var extent = _module.AddInstruction( SpirvOp.CompositeExtract, _intType, imageSize, component); var nonNegative = _module.AddInstruction( SpirvOp.SGreaterThanEqual, _boolType, coordinate, zero); var belowExtent = _module.AddInstruction( SpirvOp.SLessThan, _boolType, coordinate, extent); var componentInRange = _module.AddInstruction( SpirvOp.LogicalAnd, _boolType, nonNegative, belowExtent); inRange = _module.AddInstruction( SpirvOp.LogicalAnd, _boolType, inRange, componentInRange); } var writeLabel = _module.AllocateId(); var mergeLabel = _module.AllocateId(); _module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0); _module.AddStatement( SpirvOp.BranchConditional, inRange, writeLabel, mergeLabel); _module.AddLabel(writeLabel); _module.AddStatement( SpirvOp.ImageWrite, imageObject, coordinates, texel); _module.AddStatement(SpirvOp.Branch, mergeLabel); _module.AddLabel(mergeLabel); } private uint BuildImageOffset( Gen5ImageControl image, int component, uint componentCount) { var packed = Bitcast( _intType, LoadV(image.GetAddressRegister( ImageAddressRegister(image, component)))); var components = new uint[checked((int)componentCount)]; for (var index = 0; index < components.Length; index++) { components[index] = _module.AddInstruction( SpirvOp.BitFieldSExtract, _intType, packed, UInt((uint)(index * 8)), UInt(6)); } return _module.AddInstruction( SpirvOp.CompositeConstruct, _module.TypeVector(_intType, componentCount), components); } private uint ApplyDynamicSampleOffset( SpirvImageResource resource, uint sampledImage, uint coordinates, uint texelOffset, uint lod) { var spatialComponentCount = ImageSpatialComponentCount(resource); var coordinateComponentCount = ImageCoordinateComponentCount(resource); var spatialIntegerType = _module.TypeVector(_intType, spatialComponentCount); var spatialFloatType = _module.TypeVector(_floatType, spatialComponentCount); var queryComponentCount = resource.Arrayed ? coordinateComponentCount : spatialComponentCount; var image = _module.AddInstruction( SpirvOp.Image, resource.ImageType, sampledImage); var signedLod = _module.AddInstruction( SpirvOp.ConvertFToS, _intType, lod); var lodIsNegative = _module.AddInstruction( SpirvOp.SLessThan, _boolType, signedLod, _module.Constant(_intType, 0)); var clampedLod = _module.AddInstruction( SpirvOp.Select, _intType, lodIsNegative, _module.Constant(_intType, 0), signedLod); var size = _module.AddInstruction( SpirvOp.ImageQuerySizeLod, _module.TypeVector(_intType, queryComponentCount), image, clampedLod); if (resource.Arrayed) { size = _module.AddInstruction( SpirvOp.VectorShuffle, spatialIntegerType, size, size, 0u, 1u); } var sizeFloat = _module.AddInstruction( SpirvOp.ConvertSToF, spatialFloatType, size); var offsetFloat = _module.AddInstruction( SpirvOp.ConvertSToF, spatialFloatType, texelOffset); var normalizedOffset = _module.AddInstruction( SpirvOp.FDiv, spatialFloatType, offsetFloat, sizeFloat); if (!resource.Arrayed) { return _module.AddInstruction( SpirvOp.FAdd, spatialFloatType, coordinates, normalizedOffset); } var arrayOffset = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec3Type, _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, normalizedOffset, 0u), _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, normalizedOffset, 1u), Float(0)); return _module.AddInstruction( SpirvOp.FAdd, _vec3Type, coordinates, arrayOffset); } private bool TryEmitExport( Gen5ShaderInstruction instruction, Gen5ExportControl export, out string error) { error = string.Empty; if (instruction.Sources.Count < 4) { error = "missing export sources"; return false; } if (_stage == Gen5SpirvStage.Pixel) { if (!_pixelOutputs.TryGetValue(export.Target, out var output)) { return true; } Store(_reachedPixelExport, _module.ConstantBool(true)); var values = new uint[4]; for (var component = 0; component < 4; component++) { var enabled = (export.EnableMask & (1u << component)) != 0; if (!enabled) { values[component] = _module.AddInstruction( SpirvOp.CompositeExtract, output.Kind switch { Gen5PixelOutputKind.Uint => _uintType, Gen5PixelOutputKind.Sint => _intType, _ => _floatType, }, Load(output.Type, output.Variable), (uint)component); continue; } if (export.Compressed) { var value = LoadCompressedExportComponent( instruction, component); values[component] = output.Kind switch { Gen5PixelOutputKind.Uint => _module.AddInstruction( SpirvOp.ConvertFToU, _uintType, value), Gen5PixelOutputKind.Sint => _module.AddInstruction( SpirvOp.ConvertFToS, _intType, value), _ => value, }; continue; } var raw = LoadV(instruction.Sources[component].Value); values[component] = output.Kind switch { Gen5PixelOutputKind.Uint => raw, Gen5PixelOutputKind.Sint => Bitcast(_intType, raw), _ => Bitcast(_floatType, raw), }; } var vector = _module.AddInstruction( SpirvOp.CompositeConstruct, output.Type, values); if (output.Kind == Gen5PixelOutputKind.Float && PixelExportVgprAddressMatches() && uint.TryParse( Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_PIXEL_EXPORT_VGPR_BASE"), out var debugVgprBase)) { var registerBase = debugVgprBase + export.Target * 4; vector = _module.AddInstruction( SpirvOp.CompositeConstruct, output.Type, Bitcast(_floatType, LoadV(registerBase)), Bitcast(_floatType, LoadV(registerBase + 1)), Bitcast(_floatType, LoadV(registerBase + 2)), Bitcast(_floatType, LoadV(registerBase + 3))); } if (output.Kind == Gen5PixelOutputKind.Float && PixelExportVgprAddressMatches() && uint.TryParse( Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_PIXEL_EXPORT_PACK_VGPR_BASE"), out var debugPackVgprBase)) { var registerBase = debugPackVgprBase + export.Target * 4; var lowPair = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec2Type, TruncateFloat32ForPack(Bitcast(_floatType, LoadV(registerBase))), TruncateFloat32ForPack(Bitcast(_floatType, LoadV(registerBase + 1)))); var highPair = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec2Type, TruncateFloat32ForPack(Bitcast(_floatType, LoadV(registerBase + 2))), TruncateFloat32ForPack(Bitcast(_floatType, LoadV(registerBase + 3)))); var unpackedLow = Ext(62, _vec2Type, Ext(58, _uintType, lowPair)); var unpackedHigh = Ext(62, _vec2Type, Ext(58, _uintType, highPair)); vector = _module.AddInstruction( SpirvOp.CompositeConstruct, output.Type, _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, unpackedLow, 0), _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, unpackedLow, 1), _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, unpackedHigh, 0), _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, unpackedHigh, 1)); } if (_forcePixelMagenta && PixelExportDebugAddressMatches()) { vector = output.Kind switch { Gen5PixelOutputKind.Float => _module.AddInstruction( SpirvOp.CompositeConstruct, output.Type, Float(1f), Float(0f), Float(1f), Float(1f)), Gen5PixelOutputKind.Sint => _module.AddInstruction( SpirvOp.CompositeConstruct, output.Type, Bitcast(_intType, UInt(1)), Bitcast(_intType, UInt(0)), Bitcast(_intType, UInt(1)), Bitcast(_intType, UInt(1))), _ => _module.AddInstruction( SpirvOp.CompositeConstruct, output.Type, UInt(1), UInt(0), UInt(1), UInt(1)), }; } if (Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_TITLE_EXPORT_EXEC") == "1" && _state.Program.Address == 0x0000000500781200ul) { Store(_exec, _module.ConstantBool(true)); StoreS64( 126, _module.Constant64(_ulongType, 1)); } vector = _module.AddInstruction( SpirvOp.Select, output.Type, Load(_boolType, _exec), vector, Load(output.Type, output.Variable)); Store(output.Variable, vector); return true; } if (_stage != Gen5SpirvStage.Vertex) { return true; } uint outputVariable; if (export.Target is >= 12 and < 16) { if (export.Target != 12) { return true; } outputVariable = _positionOutput; } else if (export.Target is >= 32 and < 64 && _vertexOutputs.TryGetValue(export.Target - 32, out var parameter)) { outputVariable = parameter; } else { return true; } var components = new uint[4]; for (var component = 0; component < 4; component++) { components[component] = (export.EnableMask & (1u << component)) != 0 ? export.Compressed ? LoadCompressedExportComponent(instruction, component) : Bitcast( _floatType, LoadV(instruction.Sources[component].Value)) : Float(component == 3 ? 1f : 0f); } var outputValue = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec4Type, components); if (_state.Program.Address == 0x0000000500780000ul && export.Target is >= 32 and < 36 && Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_TITLE_VERTEX_OUTPUTS_ONE") == "1") { outputValue = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec4Type, Float(1f), Float(1f), Float(1f), Float(1f)); } outputValue = _module.AddInstruction( SpirvOp.Select, _vec4Type, Load(_boolType, _exec), outputValue, Load(_vec4Type, outputVariable)); Store(outputVariable, outputValue); return true; } private bool PixelExportDebugAddressMatches() { var addressFilter = Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS"); if (string.IsNullOrWhiteSpace(addressFilter)) { return true; } var span = addressFilter.AsSpan(); if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { span = span[2..]; } return ulong.TryParse( span, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var address) && _state.Program.Address == address; } private bool PixelImageCaptureAddressMatches() { var addressFilter = Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_IMAGE_ADDRESS"); if (string.IsNullOrWhiteSpace(addressFilter)) { return false; } var span = addressFilter.AsSpan(); if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { span = span[2..]; } return ulong.TryParse( span, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var address) && _state.Program.Address == address; } private void CapturePixelVgprs(Gen5ShaderInstruction instruction) { if (_stage != Gen5SpirvStage.Pixel || !PixelVgprCaptureAddressMatches() || !uint.TryParse( Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_VGPR_PC"), out var capturePc) || instruction.Pc != capturePc) { return; } var sourceText = Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_VGPR_SOURCES"); if (string.IsNullOrWhiteSpace(sourceText)) { return; } var destinationBase = 248u; if (uint.TryParse( Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_VGPR_DEST_BASE"), out var requestedDestinationBase)) { destinationBase = requestedDestinationBase; } var sources = sourceText.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); if (sources.Length is 0 or > 4 || destinationBase > 252 || destinationBase + (uint)sources.Length > 256) { return; } for (var index = 0; index < sources.Length; index++) { if (!uint.TryParse(sources[index], out var source) || source >= 256) { return; } } for (var index = 0; index < sources.Length; index++) { _ = uint.TryParse(sources[index], out var source); StoreV( destinationBase + (uint)index, LoadV(source), guardWithExec: Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_VGPR_IGNORE_EXEC") != "1"); } } private bool PixelVgprCaptureAddressMatches() { var addressFilter = Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS"); if (string.IsNullOrWhiteSpace(addressFilter)) { return false; } var span = addressFilter.AsSpan(); if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { span = span[2..]; } return ulong.TryParse( span, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var address) && _state.Program.Address == address; } private void CapturePixelVgprPoints(Gen5ShaderInstruction instruction) { if (_stage != Gen5SpirvStage.Pixel || !PixelVgprCaptureAddressMatches()) { return; } var captureText = Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_VGPR_POINTS"); if (string.IsNullOrWhiteSpace(captureText)) { return; } foreach (var capture in captureText.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { var fields = capture.Split(':'); if (fields.Length != 3 || !uint.TryParse(fields[0], out var pc) || !uint.TryParse(fields[1], out var source) || !uint.TryParse(fields[2], out var destination) || pc != instruction.Pc || source >= 256 || destination >= 256) { continue; } StoreV( destination, LoadV(source), guardWithExec: Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_VGPR_IGNORE_EXEC") != "1"); } } private void MarkPixelPath(Gen5ShaderInstruction instruction) { if (_stage != Gen5SpirvStage.Pixel || !PixelVgprCaptureAddressMatches()) { return; } var markerText = Environment.GetEnvironmentVariable( "SHARPEMU_MARK_PIXEL_PCS"); if (string.IsNullOrWhiteSpace(markerText)) { return; } foreach (var marker in markerText.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { var separator = marker.IndexOf(':'); if (separator <= 0 || separator == marker.Length - 1 || !uint.TryParse(marker.AsSpan(0, separator), out var pc) || !uint.TryParse(marker.AsSpan(separator + 1), out var register) || pc != instruction.Pc || register >= 256) { continue; } StoreV( register, Bitcast(_uintType, Float(1)), guardWithExec: false); } } private void CapturePixelExec(Gen5ShaderInstruction instruction) { if (_stage != Gen5SpirvStage.Pixel || !PixelVgprCaptureAddressMatches()) { return; } var captureText = Environment.GetEnvironmentVariable( "SHARPEMU_CAPTURE_PIXEL_EXEC_PCS"); if (string.IsNullOrWhiteSpace(captureText)) { return; } foreach (var capture in captureText.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { var separator = capture.IndexOf(':'); if (separator <= 0 || separator == capture.Length - 1 || !uint.TryParse(capture.AsSpan(0, separator), out var pc) || !uint.TryParse(capture.AsSpan(separator + 1), out var register) || pc != instruction.Pc || register >= 256) { continue; } var value = _module.AddInstruction( SpirvOp.Select, _floatType, Load(_boolType, _exec), Float(1), Float(0)); StoreV( register, Bitcast(_uintType, value), guardWithExec: false); } } private bool PixelExportVgprAddressMatches() { var addressFilter = Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_PIXEL_EXPORT_VGPR_ADDRESS"); if (string.IsNullOrWhiteSpace(addressFilter)) { return PixelExportDebugAddressMatches(); } var span = addressFilter.AsSpan(); if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) { span = span[2..]; } return ulong.TryParse( span, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var address) && _state.Program.Address == address; } private uint LoadCompressedExportComponent( Gen5ShaderInstruction instruction, int component) { if (TryLoadPackedHalfExportComponent( instruction, component, out var shadowValue)) { return shadowValue; } var packed = LoadV(instruction.Sources[component >> 1].Value); var unpacked = Ext(62, _vec2Type, packed); return _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, unpacked, (uint)(component & 1)); } private bool TryLoadPackedHalfExportComponent( Gen5ShaderInstruction exportInstruction, int component, out uint value) { value = 0; var packedSource = exportInstruction.Sources[component >> 1]; var tracePackedExport = Environment.GetEnvironmentVariable( "SHARPEMU_TRACE_PACKED_EXPORT") == "1" && _state.Program.Address == 0x0000000500781200ul; if (tracePackedExport) { Console.Error.WriteLine( $"[AGC][PACKED-EXPORT] exp_pc=0x{exportInstruction.Pc:X} " + $"component={component} source={packedSource.Kind}:" + $"{packedSource.Value}"); if (component == 0 && exportInstruction.Pc == 0x630) { foreach (var decoded in _state.Program.Instructions.Where( static decoded => decoded.Pc <= 0x640)) { Console.Error.WriteLine( $"[AGC][TITLE-IR] 0x{decoded.Pc:X4} " + $"{decoded.Opcode} dst=[" + string.Join(',', decoded.Destinations) + "] src=[" + string.Join(',', decoded.Sources) + "] words=[" + string.Join(',', decoded.Words.Select(static word => $"{word:X8}")) + "] ctrl=" + decoded.Control); } } } if (packedSource.Kind != Gen5OperandKind.VectorRegister) { if (tracePackedExport) { Console.Error.WriteLine( "[AGC][PACKED-EXPORT] rejected: source is not a VGPR"); } return false; } for (var index = _state.Program.Instructions.Count - 1; index >= 0; index--) { var candidate = _state.Program.Instructions[index]; if (candidate.Pc >= exportInstruction.Pc) { continue; } if (exportInstruction.Pc - candidate.Pc > 128) { break; } if (!candidate.Destinations.Any(destination => destination.Kind == Gen5OperandKind.VectorRegister && destination.Value == packedSource.Value)) { continue; } if (tracePackedExport) { Console.Error.WriteLine( $"[AGC][PACKED-EXPORT] nearest_pc=0x{candidate.Pc:X} " + $"opcode={candidate.Opcode} distance=" + $"{exportInstruction.Pc - candidate.Pc}"); } if (candidate.Opcode != "VCvtPkrtzF16F32" || candidate.Sources.Count < 2) { if (tracePackedExport) { Console.Error.WriteLine( "[AGC][PACKED-EXPORT] rejected: nearest writer is " + candidate.Opcode); } return false; } var packedPointer = PackedHalfPointer(packedSource.Value); if (Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_PACKED_EXPORT_STORE_ONE") == "1" && _state.Program.Address == 0x0000000500781200ul) { Store( packedPointer, _module.AddInstruction( SpirvOp.CompositeConstruct, _vec2Type, Float(1f), Float(1f))); } var packedPair = Load( _vec2Type, packedPointer); value = _module.AddInstruction( SpirvOp.CompositeExtract, _floatType, packedPair, (uint)(component & 1)); if (Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_PACKED_EXPORT_ONE") == "1") { value = Float(1f); } if (tracePackedExport) { Console.Error.WriteLine( "[AGC][PACKED-EXPORT] selected shadow pair"); } return true; } if (tracePackedExport) { Console.Error.WriteLine( "[AGC][PACKED-EXPORT] rejected: no nearby writer"); } return false; } private uint GetPixelOutputType(Gen5PixelOutputKind kind) => kind switch { Gen5PixelOutputKind.Uint => _uvec4Type, Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4), _ => _vec4Type, }; private uint LoadBufferWord(int binding, uint dwordAddress) { var inRange = IsBufferWordInRange(binding, dwordAddress); var safeAddress = _module.AddInstruction( SpirvOp.Select, _uintType, inRange, dwordAddress, UInt(0)); var value = Load(_uintType, BufferWordPointer(binding, safeAddress)); return _module.AddInstruction( SpirvOp.Select, _uintType, inRange, value, UInt(0)); } private uint ApplyGuestBufferByteBias(int binding, uint byteAddress) { var evaluationBinding = binding - _globalBufferBase; if ((uint)evaluationBinding >= (uint)_evaluation.GlobalMemoryBindings.Count) { // Runtime SGPR blocks and other synthetic descriptors do not // alias guest virtual memory and are always bound at offset 0. return byteAddress; } if (_initialScalarBufferIndex >= 0) { // Descriptor offsets must satisfy Vulkan's storage-buffer // alignment. The presenter therefore rounds the shared guest // allocation offset down and packs the discarded low address // bits after the 256 initial SGPRs in the per-dispatch runtime // block. Keeping this value runtime-stable prevents rotating // guest allocations from producing a new multi-megabyte SPIR-V // module and Metal pipeline while preserving exact byte access. var runtimeByteBias = Load( _uintType, RuntimeBufferBiasPointer(binding)); return IAdd(byteAddress, runtimeByteBias); } // The presenter binds the shared allocation at the largest aligned // offset not greater than this guest resource's offset. Because the // allocation base is aligned to the same power of two, the bytes // discarded from the descriptor offset are exactly the low address // bits below. Adding them here keeps scalar, MUBUF and GLOBAL paths // byte-exact, including atomics and resources that overlap another // descriptor at an unaligned guest address. var byteBias = _evaluation.GlobalMemoryBindings[evaluationBinding].BaseAddress & (_storageBufferOffsetAlignment - 1); return byteBias == 0 ? byteAddress : IAdd(byteAddress, UInt(checked((uint)byteBias))); } private void StoreBufferWord(int binding, uint dwordAddress, uint value) { EmitConditional( IsBufferWordInRange(binding, dwordAddress), () => Store(BufferWordPointer(binding, dwordAddress), value)); } private uint IsBufferWordInRange(int binding, uint dwordAddress) { var buffer = _module.AddInstruction( SpirvOp.AccessChain, _storageBlockPointer, _globalBuffers, UInt((uint)binding)); var length = _module.AddInstruction( SpirvOp.ArrayLength, _uintType, buffer, 0); return _module.AddInstruction( SpirvOp.ULessThan, _boolType, dwordAddress, length); } private uint BufferWordPointer(int binding, uint dwordAddress) => _module.AddInstruction( SpirvOp.AccessChain, _storageUintPointer, _globalBuffers, UInt((uint)binding), UInt(0), dwordAddress); private uint ScalarPointer(uint register) => _module.AddInstruction( SpirvOp.AccessChain, _privateUintPointer, _scalarRegisters, UInt(register)); private uint RuntimeBufferBiasPointer(int binding) => _module.AddInstruction( SpirvOp.AccessChain, _privateUintPointer, _runtimeBufferBiases, UInt(checked((uint)binding))); private uint VectorPointer(uint register) => _module.AddInstruction( SpirvOp.AccessChain, _privateUintPointer, _vectorRegisters, UInt(register)); private uint PackedHalfPointer(uint register) => _module.AddInstruction( SpirvOp.AccessChain, _privateVec2Pointer, _packedHalfRegisters, UInt(register)); private uint LoadS(uint register) => Load(_uintType, ScalarPointer(register)); private uint LoadV(uint register) => Load(_uintType, VectorPointer(register)); private void StoreS(uint register, uint value) { Store(ScalarPointer(register), value); if (register is 106 or 107) { Store(_vcc, IsWaveMaskActive(LoadS64(106))); } else if (register is 126 or 127) { Store(_exec, IsWaveMaskActive(LoadS64(126))); } } private void StoreV(uint register, uint value, bool guardWithExec = true) { if (guardWithExec) { var active = Load(_boolType, _exec); var oldValue = LoadV(register); value = _module.AddInstruction( SpirvOp.Select, _uintType, active, value, oldValue); } Store(VectorPointer(register), value); } private void StorePackedHalf(uint register, uint value) { var active = Load(_boolType, _exec); if (Environment.GetEnvironmentVariable( "SHARPEMU_FORCE_PACKED_STORE_EXEC_VALUES") == "1" && _state.Program.Address == 0x0000000500781200ul) { var activePair = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec2Type, Float(1f), Float(1f)); var inactivePair = _module.AddInstruction( SpirvOp.CompositeConstruct, _vec2Type, Float(0.5f), Float(0.5f)); value = _module.AddInstruction( SpirvOp.Select, _vec2Type, active, activePair, inactivePair); Store(PackedHalfPointer(register), value); return; } value = _module.AddInstruction( SpirvOp.Select, _vec2Type, active, value, Load(_vec2Type, PackedHalfPointer(register))); Store(PackedHalfPointer(register), value); } private uint Load(uint type, uint pointer) { if (pointer == 0) { throw new InvalidOperationException( "SPIR-V generator attempted OpLoad from id 0."); } return _module.AddInstruction(SpirvOp.Load, type, pointer); } private void Store(uint pointer, uint value) => _module.AddStatement(SpirvOp.Store, pointer, value); private uint UInt(uint value) => _module.Constant(_uintType, value); private uint Float(float value) => _module.ConstantFloat(_floatType, value); private uint Bitcast(uint type, uint value) => _module.AddInstruction(SpirvOp.Bitcast, type, value); private uint IAdd(uint left, uint right) => _module.AddInstruction(SpirvOp.IAdd, _uintType, left, right); private uint ShiftLeftLogical(uint left, uint right) => _module.AddInstruction( SpirvOp.ShiftLeftLogical, _uintType, left, BitwiseAnd(right, UInt(31))); private uint ShiftRightLogical(uint left, uint right) => _module.AddInstruction( SpirvOp.ShiftRightLogical, _uintType, left, BitwiseAnd(right, UInt(31))); private uint ShiftRightArithmetic(uint left, uint right) => Bitcast( _uintType, _module.AddInstruction( SpirvOp.ShiftRightArithmetic, _intType, Bitcast(_intType, left), BitwiseAnd(right, UInt(31)))); private uint ShiftLeftLogical64(uint left, uint right) => _module.AddInstruction( SpirvOp.ShiftLeftLogical, _ulongType, left, BitwiseAnd64(right, _module.Constant64(_ulongType, 63))); private uint ShiftRightLogical64(uint left, uint right) => _module.AddInstruction( SpirvOp.ShiftRightLogical, _ulongType, left, BitwiseAnd64(right, _module.Constant64(_ulongType, 63))); private uint BitwiseAnd(uint left, uint right) => _module.AddInstruction(SpirvOp.BitwiseAnd, _uintType, left, right); private uint BitwiseAnd64(uint left, uint right) => _module.AddInstruction(SpirvOp.BitwiseAnd, _ulongType, left, right); private uint BitwiseOr64(uint left, uint right) => _module.AddInstruction(SpirvOp.BitwiseOr, _ulongType, left, right); private uint BitwiseOr(uint left, uint right) => _module.AddInstruction(SpirvOp.BitwiseOr, _uintType, left, right); private uint BitwiseXor(uint left, uint right) => _module.AddInstruction(SpirvOp.BitwiseXor, _uintType, left, right); private uint LogicalNot(uint value) => _module.AddInstruction(SpirvOp.LogicalNot, _boolType, value); private uint SubgroupAny(uint condition) => _subgroupInvocationIdInput == 0 ? condition : _emulateWave64 ? IsNotZero64(BooleanToWaveMask(condition)) : _module.AddInstruction( SpirvOp.GroupNonUniformAny, _boolType, UInt(3), condition); private uint GuestWaveLane() { if (_waveLaneCount == 64 && _localInvocationIndexInput != 0) { return BitwiseAnd( Load(_uintType, _localInvocationIndexInput), UInt(63)); } if (_subgroupInvocationIdInput != 0) { return BitwiseAnd( Load(_uintType, _subgroupInvocationIdInput), UInt(31)); } // Graphics stages without subgroup support have one logical lane; // they must not emit OpLoad for absent SPIR-V input ID zero. return UInt(0); } private uint CurrentLaneBit() { if (_subgroupInvocationIdInput == 0) { return _module.Constant64(_ulongType, 1); } var maskedLane = GuestWaveLane(); var shifted = ShiftLeftLogical64( _module.Constant64(_ulongType, 1), _module.AddInstruction( SpirvOp.UConvert, _ulongType, maskedLane)); return _emulateWave64 ? shifted : _module.AddInstruction( SpirvOp.Select, _ulongType, IsCurrentLaneInRdnaWave(), shifted, _module.Constant64(_ulongType, 0)); } private uint IsCurrentLaneInRdnaWave() => _module.AddInstruction( SpirvOp.ULessThan, _boolType, Load(_uintType, _subgroupInvocationIdInput), UInt(32)); private uint BooleanToLaneMask(uint condition) => _module.AddInstruction( SpirvOp.Select, _ulongType, condition, CurrentLaneBit(), _module.Constant64(_ulongType, 0)); private uint BooleanToWaveMask(uint condition) { if (_subgroupInvocationIdInput == 0) { return BooleanToLaneMask(condition); } var ballot = _module.AddInstruction( SpirvOp.GroupNonUniformBallot, _uvec4Type, UInt(3), condition); var low = _module.AddInstruction( SpirvOp.CompositeExtract, _uintType, ballot, 0); if (_emulateWave64) { var high = _module.AddInstruction( SpirvOp.CompositeExtract, _uintType, ballot, 1); var subgroupLane = Load(_uintType, _subgroupInvocationIdInput); var firstLane = _module.AddInstruction( SpirvOp.IEqual, _boolType, subgroupLane, UInt(0)); var half = ShiftRightLogical(GuestWaveLane(), UInt(5)); EmitConditional(firstLane, () => { Store(WaveMaskScratchPointer(half), low); var nativeWave64 = _module.AddInstruction( SpirvOp.UGreaterThanEqual, _boolType, Load(_uintType, _subgroupSizeInput), UInt(64)); EmitConditional(nativeWave64, () => { Store(WaveMaskScratchPointer(UInt(1)), high); }); }); EmitWave64Barrier(); var lowMask = Load( _uintType, WaveMaskScratchPointer(UInt(0))); var highMask = Load( _uintType, WaveMaskScratchPointer(UInt(1))); var combined = BitwiseOr64( _module.AddInstruction( SpirvOp.UConvert, _ulongType, lowMask), ShiftLeftLogical64( _module.AddInstruction( SpirvOp.UConvert, _ulongType, highMask), _module.Constant64(_ulongType, 32))); EmitWave64Barrier(); return combined; } var widened = _module.AddInstruction(SpirvOp.UConvert, _ulongType, low); if (_waveLaneCount != 64) { return widened; } return _module.AddInstruction( SpirvOp.Select, _ulongType, _module.AddInstruction( SpirvOp.UGreaterThanEqual, _boolType, GuestWaveLane(), UInt(32)), ShiftLeftLogical64( widened, _module.Constant64(_ulongType, 32)), widened); } private uint WaveMaskScratchPointer(uint index) => _module.AddInstruction( SpirvOp.AccessChain, _waveMaskScratchElementPointer, _waveScratchInLds ? _lds : _waveMaskScratch, _waveScratchInLds ? IAdd(UInt(LdsDwordCount - 3), index) : index); private uint WaveBroadcastScratchPointer() => _waveScratchInLds ? _module.AddInstruction( SpirvOp.AccessChain, _ldsElementPointer, _lds, UInt(LdsDwordCount - 1)) : _waveBroadcastScratch; private void EmitWave64Barrier() { var workgroup = UInt(2); _module.AddStatement( SpirvOp.ControlBarrier, workgroup, workgroup, UInt(0x108)); } // A wave-mask SGPR (VCC/EXEC) consumed as a per-lane predicate — the // condition of VCndmask, a VCC/EXEC branch, or the derived _vcc/_exec // bool — must be tested at the CURRENT lane's bit, exactly as the // hardware does, not as "the 64-bit value is non-zero". The two coincide // for comparison results (only the lane's own bit is ever set), so the // single-lane path historically used a cheaper whole-word non-zero test. // But bitwise-complement wave-mask idioms (S_NOT/S_ORN2/S_ANDN2/S_NAND/ // S_NOR on a 64-bit mask) set the unused upper 63 bits; a whole-word test // then reports "lane active" even when this lane's bit is clear. Unity's // PostProcessing NaN killer does exactly this (`anyNaN | ~allFinite`), // which made every valid pixel read as NaN and get replaced with 0 — // zeroing the whole scene before tonemap. Extract the lane bit always. private uint IsWaveMaskActive(uint mask) => IsCurrentLaneSet(mask); private uint IsCurrentLaneSet(uint mask) => IsNotZero64( _module.AddInstruction( SpirvOp.BitwiseAnd, _ulongType, mask, CurrentLaneBit())); private void StoreWaveMask(uint register, uint condition) => StoreS64(register, BooleanToWaveMask(condition)); private void EmitExecConditional(Action emit) { var active = Load(_boolType, _exec); EmitConditional(active, emit); } private void EmitConditional(uint condition, Action emit) { var activeLabel = _module.AllocateId(); var mergeLabel = _module.AllocateId(); _module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0); _module.AddStatement( SpirvOp.BranchConditional, condition, activeLabel, mergeLabel); _module.AddLabel(activeLabel); emit(); _module.AddStatement(SpirvOp.Branch, mergeLabel); _module.AddLabel(mergeLabel); } private bool UsesLds() => _state.Program.Instructions.Any(instruction => instruction.Control is Gen5DataShareControl); private bool UsesSubgroupShuffle() => _state.Program.Instructions.Any(instruction => instruction.Control is Gen5DppControl or Gen5Dpp8Control || instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32" or "VReadlaneB32"); private bool UsesSubgroupBroadcast() => _state.Program.Instructions.Any(instruction => instruction.Opcode == "VReadfirstlaneB32"); private bool UsesWaveControl() => _state.Program.Instructions.Any(instruction => instruction.Opcode.Contains("Saveexec", StringComparison.Ordinal) || instruction.Opcode.StartsWith("SCbranchExec", StringComparison.Ordinal) || instruction.Opcode.StartsWith("SCbranchVcc", StringComparison.Ordinal) || instruction.Opcode.StartsWith("VCmpx", StringComparison.Ordinal) || instruction.Sources.Any(IsWaveMaskOperand) || instruction.Destinations.Any(IsWaveMaskOperand)); private bool UsesSubgroupOperations() => _stage == Gen5SpirvStage.Compute && (UsesSubgroupShuffle() || UsesSubgroupBroadcast() || UsesWaveControl() || _state.Program.Instructions.Any(static instruction => instruction.Opcode is "VMbcntLoU32B32" or "VMbcntHiU32B32")); private static bool IsWaveMaskOperand(Gen5Operand operand) => operand.Kind == Gen5OperandKind.ScalarRegister && operand.Value is 106 or 107 or 126 or 127; private static bool TryGetVectorDestination( Gen5ShaderInstruction instruction, out uint destination) { if (instruction.Destinations.Count != 0 && instruction.Destinations[0].Kind == Gen5OperandKind.VectorRegister) { destination = instruction.Destinations[0].Value; return true; } destination = 0; return false; } 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 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 = leaders .Where(pc => instructions.Any(instruction => instruction.Pc == pc)) .ToArray(); var blocks = new List(starts.Length); for (var index = 0; index < starts.Length; index++) { var startIndex = FindInstructionIndex(instructions, starts[index]); var endIndex = index + 1 < starts.Length ? FindInstructionIndex(instructions, starts[index + 1]) : instructions.Count; if (startIndex >= 0 && endIndex > startIndex) { blocks.Add(new ShaderBlock(starts[index], startIndex, endIndex)); } } return blocks; } 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 = Enumerable.Repeat( InitialScalarDefinition, ScalarRegisterCount).ToArray(); static void MergeDefinitions( long[] destination, long[] source, ref bool hasInput) { if (!hasInput) { Array.Copy(source, destination, ScalarRegisterCount); hasInput = true; return; } for (var register = 0; register < ScalarRegisterCount; 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 < ScalarRegisterCount) { definitions[destination.Value] = instruction.Pc + 1L; } } } } var changed = true; while (changed) { changed = false; for (var blockIndex = 0; blockIndex < blocks.Count; blockIndex++) { var input = Enumerable.Repeat( UnreachableScalarDefinition, ScalarRegisterCount).ToArray(); 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 < ScalarRegisterCount) { definitions[destination.Value] = instruction.Pc + 1L; } } } } } 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; } private readonly record struct ShaderBlock( uint StartPc, int StartIndex, int EndIndex); } }