sharpemu/src/SharpEmu.ShaderCompiler.Vulkan/Gen5SpirvTranslator.cs

8488 lines
344 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
namespace SharpEmu.ShaderCompiler.Vulkan;
public static partial class Gen5SpirvTranslator
{
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;
private const uint Wave64ExchangeSlotCount = 4;
private const uint Wave64ExchangeDwordCount = Wave64ExchangeSlotCount * 2;
internal static SpirvImageFormat DecodeStorageImageFormat(
uint dataFormat,
uint numberType) =>
CompilationContext.DecodeStorageImageFormat(dataFormat, numberType);
// Vulkan only guarantees maxComputeWorkGroupSize >= (1024, 1024, 64) and
// maxComputeWorkGroupInvocations >= 1024, but a GNM NUM_THREAD layout can put its
// largest axis on Z (real PS5 hardware has no such asymmetry between axes). Remap
// logical GNM axes onto physical SPIR-V/dispatch axes so the largest logical size
// always lands on the physical axis with the most headroom (X), and the smallest
// lands on Z, which is the only axis with a tight guaranteed limit. This is a pure
// function of the three thread-group sizes so the host dispatch path (which permutes
// vkCmdDispatch's group counts the same way) can recompute it independently without
// any shader-compiler metadata plumbing.
//
// Returns physicalAxisOfLogical: physicalAxisOfLogical[logical 0=X/1=Y/2=Z] is the
// physical axis (0=X/1=Y/2=Z) that logical axis is remapped onto.
public static int[] ComputeWorkgroupAxisOrder(uint sizeX, uint sizeY, uint sizeZ)
{
if (sizeZ <= 64)
{
return [0, 1, 2];
}
var sizes = new[] { sizeX, sizeY, sizeZ };
var byDescendingSize = new[] { 0, 1, 2 };
Array.Sort(byDescendingSize, (a, b) => sizes[b].CompareTo(sizes[a]));
var physicalAxisOfLogical = new int[3];
for (var physical = 0; physical < 3; physical++)
{
physicalAxisOfLogical[byDescendingSize[physical]] = physical;
}
return physicalAxisOfLogical;
}
private sealed partial class CompilationContext
{
private const int ScalarRegisterCount = 128;
// M0. Used as the runtime index added to the register numbers encoded in
// the V_MOVREL* instructions, and as the LDS/GDS base elsewhere.
private const uint M0ScalarRegister = 124;
private readonly SpirvModuleBuilder _module = new();
private readonly Gen5SpirvStage _stage;
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
private readonly bool _usesPixelValidMask;
private readonly bool _enableGraphicsSubgroupOperations;
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;
// Vulkan only guarantees maxComputeWorkGroupSize >= (1024, 1024, 64), but a GNM
// NUM_THREAD layout can put its largest axis on Z (real hardware has no such asymmetry).
// physicalAxisOfLogical[logical 0=X/1=Y/2=Z] gives which physical SPIR-V axis (and which
// vkCmdDispatch group-count slot) that logical axis is remapped onto, so the largest size
// always lands on the physical axis with the most headroom.
private readonly int[] _physicalAxisOfLogical;
private readonly uint _pixelInputEnable;
private readonly uint _pixelInputAddress;
private readonly uint[] _pixelInputCntl;
private readonly List<uint> _interfaces = [];
private readonly Dictionary<uint, uint> _pixelInputs = [];
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
private readonly Dictionary<uint, uint> _vertexOutputs = [];
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
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 readonly HashSet<uint> _flagVariables = [];
private uint _runtimeBufferBiases;
// One Private variable per register the program touches, declared on first use. A whole
// register-file array is kept only for the VGPRs of a program that indexes them at run time
// (V_MOVREL*): MoltenVK lowers a 512-entry private array to thread memory, and every
// register access of every invocation then spills through it.
private readonly Dictionary<uint, uint> _scalarRegisterVariables = new();
private readonly Dictionary<uint, uint> _vectorRegisterVariables = new();
private uint _vectorRegisters;
private (uint First, uint Last) _dynamicVectorRange;
private uint _packedHalfRegisters;
private uint _scc;
private uint _vcc;
private uint _exec;
private uint _reachedPixelExport;
private uint _pixelValidMaskActive;
private uint _programCounter;
private uint _programActive;
private uint _iterationGuard;
// One-lane waves only have lane 0 of each VGPR. Other lanes that the program spills
// SGPRs into with V_WRITELANE and reads back with V_READLANE live here, keyed by
// (VGPR, lane).
private readonly Dictionary<(uint Register, uint Lane), uint> _laneSpillSlots = new();
private uint _globalBuffers;
private uint _gfx10BufferFormatTable;
private uint _storageBlockPointer;
private uint _storageUintPointer;
private uint _lds;
private uint _ldsElementPointer;
private uint _lds64ElementPointer;
private uint _ldsDwordMask;
private uint _scratch;
private uint _scratchElementPointer;
private uint _scratchDwordCount;
private uint _positionOutput;
private uint _pointSizeOutput;
private uint _clipDistanceOutput;
private uint _cullDistanceOutput;
private uint _layerOutput;
private uint _viewportIndexOutput;
private uint _clipDistanceCount;
private uint _invalidPositionClipDistance = uint.MaxValue;
private uint _cullDistanceCount;
private uint _vertexIndexInput;
private uint _instanceIndexInput;
private uint _fragCoordInput;
private uint _localInvocationIdInput;
private uint _localInvocationIndexInput;
private uint _workGroupIdInput;
private uint _pushConstantUintPointer;
private uint _subgroupInvocationIdInput;
private uint _wave64Exchange;
private uint _wave64ExchangeElementPointer;
private uint _wave64ExchangeOffset;
private uint _wave64ExchangeParity;
private Ir.Gen5Wave64HalfMaskPlan? _halfMaskPlan;
private uint? _emittingPc;
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,
bool Cube = false,
bool Multisampled = false,
SpirvImageDim Dimension = SpirvImageDim.Dim2D,
uint ConversionFormat = 0,
uint ShaderSwizzle = 0,
int EmulatedCompareFunction = -1);
private readonly record struct SpirvVertexInput(
uint Variable,
uint Type,
uint ComponentType,
uint ComponentCount,
VertexInputComponentKind ComponentKind,
uint NumberFormat,
uint DestinationSelect);
private readonly record struct SpirvPixelOutput(
uint Variable,
uint Type,
Gen5PixelOutputKind Kind,
Gen5ColorComponentMapping ComponentMapping);
public bool TryCompile(out Gen5SpirvShader shader, out string error)
{
shader = default!;
error = string.Empty;
try
{
if (Environment.GetEnvironmentVariable(
"SHARPEMU_TRACE_TITLE_INTERFACE") == "1" &&
_request.Program.Address is 0x0000000500780000ul or
0x0000000500781200ul)
{
Console.Error.WriteLine(
$"[AGC][TITLE-INTERFACE] stage={_stage} " +
$"address=0x{_request.Program.Address:X16} " +
$"required_vertex_outputs={_requiredVertexOutputCount} " +
$"ps_ena=0x{_pixelInputEnable:X8} ps_addr=0x{_pixelInputAddress:X8}");
foreach (var instruction in _request.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) + "]");
}
}
}
var blocks = BuildBasicBlocks(_request.Program.Instructions);
// The fallback when the full structurer declines: blocks in program order behind a
// next-block guard, with natural loops as structured loops.
var structuredForward = StructuredForwardBlocks && blocks.Count != 0 && TryBuildLoopRegions(blocks, out _loopLatchByHeader);
DeclareModule();
if (blocks.Count == 0)
{
error = "shader contains no executable blocks";
return false;
}
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" &&
_request.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();
if (StructuredControlFlow && blocks.Count > 0 && TryStructureRange(blocks, 0, blocks.Count, blocks.Count, emit: false, out _))
{
// As the dispatcher does before its first block: an out-of-bounds invocation runs nothing.
var runLabel = _module.AllocateId();
var doneLabel = _module.AllocateId();
var isActive = Load(_boolType, _programActive);
_module.AddStatement(SpirvOp.SelectionMerge, doneLabel, 0);
_module.AddStatement(SpirvOp.BranchConditional, isActive, runLabel, doneLabel);
_module.AddLabel(runLabel);
if (!TryStructureRange(blocks, 0, blocks.Count, blocks.Count, emit: true, out error))
{
return false;
}
_module.AddStatement(SpirvOp.Branch, doneLabel);
_module.AddLabel(doneLabel);
}
else
{
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();
}
if (structuredForward)
{
// Each invocation visits its blocks in program order, except for the back
// edges of natural loops. Emitting the blocks in order, each guarded by "this
// is the next block", and every loop as a structured SPIR-V loop runs exactly
// what the dispatcher loop would. The driver then sees ifs and loops and can
// keep only the live guest registers in hardware registers, where the
// dispatcher keeps nearly all of them live and pays a switch per block.
if (!TryEmitStructuredRange(blocks, 0, blocks.Count - 1, -1, out error))
{
return false;
}
_module.AddStatement(SpirvOp.Branch, loopMerge);
}
else
{
_module.AddStatement(SpirvOp.Branch, loopHeader);
_module.AddLabel(loopHeader);
// Check before the first block can write from an inactive invocation.
var programIsActive = Load(_boolType, _programActive);
_module.AddStatement(SpirvOp.LoopMerge, loopMerge, loopContinue, 0);
_module.AddStatement(SpirvOp.BranchConditional, programIsActive, switchHeader, loopMerge);
_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" &&
_request.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)
{
// EXP.VM publishes EXEC as the pixel-valid mask. EXEC may
// subsequently be restored for control-flow convergence,
// so the final EXEC value is not the fragment-validity
// decision. Programs without VM retain the old EXEC
// fallback so malformed shaders still fail conservatively.
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,
_usesPixelValidMask ? _pixelValidMaskActive : _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)
{
var logicalSizes = new[] { _localSizeX, _localSizeY, _localSizeZ };
var physicalSizes = new uint[3];
for (var logical = 0; logical < 3; logical++)
{
physicalSizes[_physicalAxisOfLogical[logical]] = logicalSizes[logical];
}
_module.AddExecutionMode(
main,
SpirvExecutionMode.LocalSize,
physicalSizes[0],
physicalSizes[1],
physicalSizes[2]);
}
var attributeCount = _stage == Gen5SpirvStage.Vertex
? (uint)_vertexOutputs.Count
: (uint)_pixelInputs.Count;
shader = new Gen5SpirvShader(_module.Build(), attributeCount);
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 (UsesSubgroupOperations())
{
_module.AddCapability(SpirvCapability.GroupNonUniform);
_module.AddCapability(SpirvCapability.GroupNonUniformBallot);
if (UsesSubgroupShuffle())
{
_module.AddCapability(SpirvCapability.GroupNonUniformShuffle);
}
if (UsesWaveControl())
{
_module.AddCapability(SpirvCapability.GroupNonUniformVote);
}
}
_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);
if (_request.Program.Instructions.Any(static instruction => instruction.Opcode.StartsWith("VMovrel", StringComparison.Ordinal)))
{
// A dynamically indexed private array cannot live in registers: Metal spills the
// whole 2 KiB file to thread memory and every VGPR access becomes a memory access
// (Octopath's instanced vertex shader: 112 ms per draw). Keep one variable per
// register and resolve V_MOVREL* with a select over the registers the program uses.
_dynamicVectorRange = DynamicVectorRange(_request.Program);
}
// Keep scalar state in uint variables, matching upstream's flag load/store conversion.
uint Flag(bool initialValue)
{
var variable = _module.AddGlobalVariable(_privateUintPointer, SpirvStorageClass.Private, UInt(initialValue ? 1u : 0u));
_flagVariables.Add(variable);
return variable;
}
_scc = Flag(false);
_vcc = Flag(false);
_exec = Flag(true);
_reachedPixelExport = Flag(false);
if (_usesPixelValidMask)
{
_pixelValidMaskActive = Flag(true);
}
_programCounter = _module.AddGlobalVariable(
_privateUintPointer,
SpirvStorageClass.Private,
_module.Constant(_uintType, 0));
_programActive = Flag(true);
if (UsesWave64Exchange())
{
_wave64ExchangeParity = _module.AddGlobalVariable(_privateUintPointer, SpirvStorageClass.Private, UInt(0));
_interfaces.Add(_wave64ExchangeParity);
_module.AddName(_wave64ExchangeParity, "wave64ExchangeParity");
}
if (_maxDispatcherSteps > 0)
{
_iterationGuard = _module.AddGlobalVariable(
_privateUintPointer,
SpirvStorageClass.Private,
_module.Constant(_uintType, 0));
_interfaces.Add(_iterationGuard);
_module.AddName(_iterationGuard, "pcGuard");
}
foreach (var slot in FindLaneSpillSlots())
{
var variable = _module.AddGlobalVariable(_privateUintPointer, SpirvStorageClass.Private, _module.Constant(_uintType, 0));
_interfaces.Add(variable);
_module.AddName(variable, $"v{slot.Register}_lane{slot.Lane}");
_laneSpillSlots.Add(slot, variable);
}
if (_vectorRegisters != 0)
{
_interfaces.Add(_vectorRegisters);
_module.AddName(_vectorRegisters, "vgpr");
}
_interfaces.Add(_scc);
_interfaces.Add(_vcc);
_interfaces.Add(_exec);
_interfaces.Add(_reachedPixelExport);
if (_pixelValidMaskActive != 0)
{
_interfaces.Add(_pixelValidMaskActive);
_module.AddName(_pixelValidMaskActive, "pixelValidMaskActive");
}
_interfaces.Add(_programCounter);
_interfaces.Add(_programActive);
{
DeclareLayoutBindings();
DeclareScratch();
DeclareLds();
DeclareWave64Scratch();
DeclareStageInterface();
return;
}
}
private void DeclareScratch()
{
if (!_request.Program.Instructions.Any(static instruction =>
instruction.Opcode.StartsWith("Scratch", StringComparison.Ordinal)) &&
!_request.Memory.Entries.Any(static memory =>
memory.AddressSpace is FlatAddressSpace.Private or FlatAddressSpace.SharedOrPrivate))
{
return;
}
_scratchDwordCount = Math.Max(_request.ScratchDwords, 1u);
var arrayType = _module.TypeArray(_uintType, _scratchDwordCount);
var arrayPointer = _module.TypePointer(SpirvStorageClass.Private, arrayType);
_scratchElementPointer = _module.TypePointer(SpirvStorageClass.Private, _uintType);
_scratch = _module.AddGlobalVariable(
arrayPointer,
SpirvStorageClass.Private,
_module.ConstantNull(arrayType));
_interfaces.Add(_scratch);
_module.AddName(_scratch, "scratch");
}
private void DeclareWave64Scratch()
{
if (!UsesWave64Exchange())
{
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 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)
{
_wave64Exchange = _lds;
_wave64ExchangeElementPointer = _ldsElementPointer;
_wave64ExchangeOffset = _ldsDwordMask + 1 < LdsDwordCount
? _ldsDwordMask + 1
: LdsDwordCount - Wave64ExchangeDwordCount;
return;
}
var exchangeArrayType = _module.TypeArray(_uintType, Wave64ExchangeDwordCount);
_wave64ExchangeElementPointer = _module.TypePointer(SpirvStorageClass.Workgroup, _uintType);
_wave64Exchange = _module.AddGlobalVariable(
_module.TypePointer(SpirvStorageClass.Workgroup, exchangeArrayType),
SpirvStorageClass.Workgroup);
_wave64ExchangeOffset = 0;
_module.AddName(_wave64Exchange, "wave64Exchange");
_interfaces.Add(_wave64Exchange);
}
private bool UsesWave64Exchange() => _emulateWave64 && UsesSubgroupOperations();
private uint ComputeLdsGuestDwordCount()
{
var declared = _request.LocalDataShareDwords;
return declared == 0 || declared > LdsDwordCount
? LdsDwordCount
: System.Numerics.BitOperations.RoundUpToPowerOf2(declared);
}
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
? ComputeLdsGuestDwordCount()
: PrivateLdsDwordCount;
_ldsDwordMask = dwordCount - 1;
var arrayDwordCount = UsesWave64Exchange() && dwordCount < LdsDwordCount
? dwordCount + Wave64ExchangeDwordCount
: dwordCount;
var ldsArrayType = _module.TypeArray(_uintType, arrayDwordCount);
var ldsPointer = _module.TypePointer(storageClass, ldsArrayType);
_ldsElementPointer = _module.TypePointer(storageClass, _uintType);
_lds64ElementPointer = _module.TypePointer(storageClass, _ulongType);
_lds = storageClass == SpirvStorageClass.Workgroup
? _module.AddGlobalVariable(ldsPointer, storageClass)
: _module.AddGlobalVariable(
ldsPointer,
storageClass,
_module.ConstantNull(ldsArrayType));
_module.AddName(_lds, "lds");
_interfaces.Add(_lds);
}
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);
if (_stage == Gen5SpirvStage.Pixel)
{
// Vulkan requires integer fragment inputs, including subgroup
// built-ins, to use flat interpolation.
_module.AddDecoration(
_subgroupInvocationIdInput,
SpirvDecoration.Flat);
}
_interfaces.Add(_subgroupInvocationIdInput);
// LocalInvocationIndex is a compute-stage built-in. Graphics
// stages can still use native subgroup operations, but they
// cannot combine two subgroup32 halves through a workgroup.
if (_stage == Gen5SpirvStage.Compute && _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);
DeclareAuxPositionOutputs();
var parameters = _request.Program.Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5ExportControl>()
.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 = _request.Program.Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5InterpolationControl>()
.Select(control => control.Attribute)
.Distinct()
.Order()
.ToArray();
DeclareInterpolationParameters();
// Several PS input slots may read one VS parameter (the guest compiler emits a
// slot per use). Relocating a duplicate left it on a location the vertex program
// never writes, so it read zero: Astro Bot's save-slot card normals decoded to NaN
// and a machine's albedo came out black. Slots of one kind share one host input;
// when a V_INTERP_MOV slot is among them, the others read the per-vertex values
// (interpolated with the barycentrics, or vertex 0 for a flat slot).
var shared = new Dictionary<uint, uint>();
bool IsFlatSlot(uint attribute) =>
((attribute < (uint)_pixelInputCntl.Length ? _pixelInputCntl[attribute] : 0u) & 0x400u) != 0 ||
_flatParameterAttributes.Contains(attribute);
foreach (var group in attributes.GroupBy(attribute =>
(attribute < (uint)_pixelInputCntl.Length ? _pixelInputCntl[attribute] : attribute) & 0x1Fu))
{
uint? perVertexOwner = null;
foreach (var attribute in group)
{
if (_perVertexAttributes.Contains(attribute))
{
perVertexOwner = attribute;
break;
}
}
var owners = new Dictionary<bool, uint>();
foreach (var attribute in group)
{
if (perVertexOwner is { } perVertex)
{
if (attribute != perVertex)
{
shared.Add(attribute, perVertex);
if (!_perVertexAttributes.Contains(attribute))
{
_perVertexSourcedAttributes.Add(attribute, IsFlatSlot(attribute));
}
}
continue;
}
var flat = IsFlatSlot(attribute);
if (owners.TryGetValue(flat, out var owner))
{
shared.Add(attribute, owner);
}
else
{
owners.Add(flat, attribute);
}
}
}
if (_perVertexSourcedAttributes.ContainsValue(false))
{
DeclarePerspectiveBarycentric();
}
attributes = attributes.Where(attribute => !shared.ContainsKey(attribute)).ToArray();
var locations = Gen5PixelInputMapping.ResolveLocations(
_pixelInputCntl,
attributes);
for (var index = 0; index < attributes.Length; index++)
{
var attribute = attributes[index];
// V_INTERP_MOV reads one vertex of the primitive, so its attribute is a
// per-vertex array rather than an interpolated value.
var variable = _module.AddGlobalVariable(
_perVertexAttributes.Contains(attribute)
? _module.TypePointer(SpirvStorageClass.Input, _module.TypeArray(_vec4Type, 3))
: inputVec4Pointer,
SpirvStorageClass.Input);
// VINTRP ATTR selects the PS input slot. SPI_PS_INPUT_CNTL
// maps that slot to a VS parameter export location.
var cntl = attribute < (uint)_pixelInputCntl.Length
? _pixelInputCntl[attribute]
: attribute;
_module.AddDecoration(
variable,
SpirvDecoration.Location,
locations[index]);
if (_perVertexAttributes.Contains(attribute))
{
_module.AddDecoration(variable, SpirvDecoration.PerVertexKhr);
}
else if ((cntl & 0x400u) != 0 || _flatParameterAttributes.Contains(attribute))
{
_module.AddDecoration(variable, SpirvDecoration.Flat);
}
_pixelInputs.Add(attribute, variable);
_interfaces.Add(variable);
}
foreach (var (attribute, owner) in shared)
{
_pixelInputs.Add(attribute, _pixelInputs[owner]);
}
_fragCoordInput = _module.AddGlobalVariable(
inputVec4Pointer,
SpirvStorageClass.Input);
_module.AddDecoration(
_fragCoordInput,
SpirvDecoration.BuiltIn,
(uint)SpirvBuiltIn.FragCoord);
_interfaces.Add(_fragCoordInput);
DeclarePixelSystemInputs();
var declaredPixelOutputs =
Environment.GetEnvironmentVariable(
"SHARPEMU_FORCE_TITLE_SINGLE_MRT") == "1" &&
_request.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.ExportTarget,
new SpirvPixelOutput(
variable,
outputType,
binding.Kind,
binding.ComponentMapping));
_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 _request.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);
var vertexInput = new SpirvVertexInput(
variable,
type,
componentType,
input.ComponentCount,
componentKind,
input.NumberFormat,
input.DestinationSelect);
_vertexInputsByPc.TryAdd(input.Pc, vertexInput);
foreach (var aliasPc in input.AliasPcs ?? [])
{
_vertexInputsByPc.TryAdd(aliasPc, vertexInput);
}
_interfaces.Add(variable);
}
}
private void EmitInitialState()
{
{
EmitLayoutInitialState();
}
Store(_scc, _module.ConstantBool(false));
Store(_reachedPixelExport, _module.ConstantBool(false));
if (_pixelValidMaskActive != 0)
{
Store(_pixelValidMaskActive, _module.ConstantBool(true));
}
if (_subgroupInvocationIdInput != 0 && _emulateWave64)
{
StoreS64(106, BothHalvesOfOwnBallot(_module.ConstantBool(false)));
StoreS64(126, BothHalvesOfOwnBallot(_module.ConstantBool(true)));
}
else 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));
}
// Primitive (NGG) vertex programs rebuild EXEC from the merged wave info in s3:
// vertex count in bits 0-7, primitive count in bits 8-15. Left at zero, EXEC
// becomes all 64 lanes, and a waterfall loop then waits forever for lanes that
// do not exist to clear their bits (Astro Bot intro_next froze the GPU this way).
if (_stage == Gen5SpirvStage.Vertex && _request.UserDataBase > 3)
{
uint laneCount;
if (_subgroupInvocationIdInput == 0)
{
laneCount = UInt(1);
}
else
{
var lanes = BooleanToWaveMask(_module.ConstantBool(true));
laneCount = IAdd(
_module.AddInstruction(SpirvOp.BitCount, _uintType, Narrow(lanes)),
_module.AddInstruction(SpirvOp.BitCount, _uintType, Narrow(ShiftRightLogical64(lanes, _module.Constant64(_ulongType, 32)))));
}
StoreS(3, BitwiseOr(laneCount, ShiftLeftLogical(laneCount, UInt(8))));
}
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));
}
if (_pointSizeOutput != 0)
{
Store(_pointSizeOutput, Float(0f));
}
if (_layerOutput != 0)
{
Store(_layerOutput, UInt(0));
}
if (_viewportIndexOutput != 0)
{
Store(_viewportIndexOutput, UInt(0));
}
InitializeDistanceOutput(_clipDistanceOutput, _clipDistanceCount);
InitializeDistanceOutput(_cullDistanceOutput, _cullDistanceCount);
}
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++)
{
// gl_LocalInvocationId/gl_WorkGroupId are indexed by the physical SPIR-V
// axis, which can differ from the logical GNM axis when the workgroup was
// remapped to respect the device's tighter Z-axis limit (see
// ComputeWorkgroupAxisOrder).
var physicalComponent = (uint)_physicalAxisOfLogical[component];
var localComponent = _module.AddInstruction(
SpirvOp.CompositeExtract,
_uintType,
localId,
physicalComponent);
StoreV(component, localComponent, guardWithExec: false);
var groupComponent = _module.AddInstruction(
SpirvOp.CompositeExtract,
_uintType,
workGroupId,
physicalComponent);
var localSize = component switch
{
0 => _localSizeX,
1 => _localSizeY,
_ => _localSizeZ,
};
var globalComponent = IAdd(
_module.AddInstruction(
SpirvOp.IMul,
_uintType,
groupComponent,
UInt(localSize)),
localComponent);
// Keep inactive invocations masked when the final workgroup is partial.
var limit = ComputeThreadLimit(component);
var componentInBounds = _module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
globalComponent,
limit);
invocationInBounds = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
invocationInBounds,
componentInBounds);
}
Store(_programActive, invocationInBounds);
if (_request.ComputeSystemRegisters is { } registers)
{
StoreComputeSystemRegister(
registers.WorkGroupXRegister,
workGroupId,
(uint)_physicalAxisOfLogical[0]);
StoreComputeSystemRegister(
registers.WorkGroupYRegister,
workGroupId,
(uint)_physicalAxisOfLogical[1]);
StoreComputeSystemRegister(
registers.WorkGroupZRegister,
workGroupId,
(uint)_physicalAxisOfLogical[2]);
if (registers.ThreadGroupSizeRegister is { } sizeRegister)
{
StoreS(
sizeRegister,
UInt(checked(_localSizeX * _localSizeY * _localSizeZ)));
}
}
}
}
private void EmitPixelInputState(uint fragCoord)
{
uint vgpr = 0;
// Keep input registers in SPI_PS_INPUT_ADDR order, including slots
// whose values are read through the interpolated input interface.
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.
EmitPixelSystemInput(12, _frontFacingInput == 0 ? 0 : LoadFrontFaceInput(), ref vgpr);
EmitPixelSystemInput(13, _ancillaryLayerInput == 0 ? 0 : LoadAncillaryInput(), ref vgpr);
EmitPixelSystemInput(14, _sampleMaskInput == 0 ? 0 : LoadSampleCoverageInput(), ref vgpr);
EmitPixelSystemInput(15, (_pixelInputAddress & _pixelInputEnable & 0x8000u) == 0 ? 0 : LoadFixedPointPositionInput(fragCoord), ref vgpr);
}
private void AdvancePixelInput(int bit, uint dwordCount, ref uint vgpr)
{
if ((_pixelInputAddress & (1u << bit)) != 0)
{
// Shaders that interpolate by hand (P1/P2 on per-vertex attributes) read
// the I/J barycentrics from these registers.
if (_barycentricInputs.TryGetValue(bit, out var barycentricInput))
{
var coordinates = LoadBarycentricCoordinates(bit, barycentricInput);
for (uint component = 0; component < 2; component++)
{
var coordinate = _module.AddInstruction(
SpirvOp.CompositeExtract, _floatType, coordinates, component + 1);
StoreV(vgpr + component, Bitcast(_uintType, coordinate), guardWithExec: false);
}
}
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);
if (component == 3)
{
value = _module.AddInstruction(SpirvOp.FDiv, _floatType, Float(1f), value);
}
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 enum SharedMemoryPhase { None, Read, Write }
private bool TryEmitBlock(
IReadOnlyList<ShaderBlock> blocks,
int blockIndex,
out string error)
{
if (!TryEmitBlockBody(blocks, blockIndex, out error))
{
return false;
}
return TryEmitBlockTerminator(blocks, blockIndex, out error);
}
// Every instruction of a block except its terminating branch or s_endpgm.
private bool TryEmitBlockBody(
IReadOnlyList<ShaderBlock> blocks,
int blockIndex,
out string error)
{
error = string.Empty;
var block = blocks[blockIndex];
var halfMaskPlan = HalfMaskPlan();
// One guest wave can span two host subgroups. Keep its shared-memory phases ordered.
// The half-mask plan inserts its own barriers; other wave64 programs also
// need LDS phase ordering when reads and writes span basic blocks.
var synchronizeSharedMemory = _emulateWave64 && halfMaskPlan is null;
var sharedMemoryPhase = SharedMemoryPhase.None;
for (var index = block.StartIndex; index < block.EndIndex; index++)
{
var instruction = _request.Program.Instructions[index];
if (halfMaskPlan is not null)
{
if (halfMaskPlan.ExactPairsBefore.TryGetValue(instruction.Pc, out var exactPairs))
{
EmitExactWaveMasks(exactPairs);
}
if (halfMaskPlan.SharedMemoryBarriersBefore.Contains(instruction.Pc))
{
EmitWave64Barrier();
}
}
if (IsBranch(instruction.Opcode) || instruction.Opcode == "SEndpgm")
{
continue;
}
if (synchronizeSharedMemory)
{
var nextPhase = instruction.Control is Gen5DataShareControl { Gds: false }
? instruction.Opcode.StartsWith("DsRead", StringComparison.Ordinal) ? SharedMemoryPhase.Read
: instruction.Opcode.StartsWith("DsWrite", StringComparison.Ordinal) ? SharedMemoryPhase.Write : SharedMemoryPhase.None
: SharedMemoryPhase.None;
if (instruction.Opcode == "SBarrier") sharedMemoryPhase = SharedMemoryPhase.None;
if (nextPhase != SharedMemoryPhase.None)
{
if (sharedMemoryPhase != SharedMemoryPhase.None && sharedMemoryPhase != nextPhase) EmitWave64Barrier();
sharedMemoryPhase = nextPhase;
}
}
_execKnownFull = IsExecKnownFull(instruction.Pc);
_emittingPc = instruction.Pc;
var emitted = TryEmitInstruction(instruction, out error);
_emittingPc = null;
_execKnownFull = false;
if (!emitted)
{
error = $"pc=0x{instruction.Pc:X} {instruction.Opcode}: {error}";
return false;
}
CapturePixelVgprs(instruction);
CapturePixelVgprPoints(instruction);
MarkPixelPath(instruction);
CapturePixelExec(instruction);
}
if (synchronizeSharedMemory && sharedMemoryPhase != SharedMemoryPhase.None) EmitWave64Barrier();
return true;
}
// Stores the next dispatcher block (or ends the program) for a block's terminator.
private bool TryEmitBlockTerminator(
IReadOnlyList<ShaderBlock> blocks,
int blockIndex,
out string error)
{
error = string.Empty;
var block = blocks[blockIndex];
var terminator = _request.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(_request.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(_request.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;
}
// Forward-only, properly nested control flow is emitted as structured selections instead of
// the PC-dispatch loop: a loop around a switch keeps every register live across iterations,
// blocks cannot be optimized together, and the Metal compiler spills. SHARPEMU_STRUCTURED_CF=0
// keeps the dispatcher for every program.
private static readonly bool StructuredControlFlow =
Environment.GetEnvironmentVariable("SHARPEMU_STRUCTURED_CF") != "0";
private static bool IsStructuredCondition(string opcode) => opcode is
"SCbranchScc0" or "SCbranchScc1" or "SCbranchVccz" or "SCbranchVccnz" or
"SCbranchExecz" or "SCbranchExecnz" or
"SCbranchCdbgsys" or "SCbranchCdbguser" or "SCbranchCdbgsysOrUser" or "SCbranchCdbgsysAndUser";
// The block a branch continues at, or blocks.Count for a branch past the last instruction.
private bool TryResolveBranchBlock(IReadOnlyList<ShaderBlock> blocks, Gen5ShaderInstruction branch, out int target)
{
target = -1;
if (!TryGetBranchTargetPc(branch, out var targetPc))
{
return false;
}
if (IsExitBranchTarget(_request.Program.Instructions, targetPc))
{
target = blocks.Count;
return true;
}
return TryFindBlock(blocks, targetPc, out target);
}
// Emits (or, without emit, only checks) blocks [begin, end). A branch from the range's last
// block to leave falls through to the range end. Inside a loop body, latch is the block
// whose backward branch the caller emits as the loop's continue, and a branch to end would
// leave the loop, which a structured body cannot express.
private bool TryStructureRange(
IReadOnlyList<ShaderBlock> blocks,
int begin,
int end,
int leave,
bool emit,
out string error,
int latch = -1)
{
error = string.Empty;
var instructions = _request.Program.Instructions;
var index = begin;
while (index < end)
{
// A loop body starts at its own header, which must not open the loop again.
if (!(latch >= 0 && index == begin) && index != latch &&
TryFindLoopLatch(blocks, index, end, out var loopLatch))
{
if (!TryStructureLoop(blocks, index, loopLatch, emit, out error))
{
return false;
}
index = loopLatch + 1;
continue;
}
if (emit && !TryEmitBlockBody(blocks, index, out error))
{
error = $"block=0x{blocks[index].StartPc:X}: {error}";
return false;
}
if (index == latch)
{
return true;
}
var terminator = instructions[blocks[index].EndIndex - 1];
var next = index + 1;
if (terminator.Opcode == "SEndpgm")
{
if (next != blocks.Count)
{
error = "s_endpgm before the last block";
return false;
}
if (emit)
{
Store(_programActive, _module.ConstantBool(false));
}
index = next;
continue;
}
if (terminator.Opcode == "SBranch")
{
if (!TryResolveBranchBlock(blocks, terminator, out var branchTarget) ||
!(branchTarget == next || (next == end && branchTarget == leave)))
{
error = $"unstructured s_branch at 0x{terminator.Pc:X}";
return false;
}
index = next;
continue;
}
if (!terminator.Opcode.StartsWith("SCbranch", StringComparison.Ordinal))
{
index = next;
continue;
}
if (!IsStructuredCondition(terminator.Opcode) ||
!TryResolveBranchBlock(blocks, terminator, out var target) || target <= index)
{
error = $"unstructured {terminator.Opcode} at 0x{terminator.Pc:X}";
return false;
}
// if (taken) break; -- only out of the innermost loop, to the block after its latch.
if (_structuredLoopExit >= 0 && target == _structuredLoopExit)
{
if (emit)
{
TryGetBranchCondition(terminator.Opcode, out var leaveLoop);
var breakLabel = _module.AllocateId();
var stayLabel = _module.AllocateId();
_module.AddStatement(SpirvOp.SelectionMerge, stayLabel, 0);
_module.AddStatement(SpirvOp.BranchConditional, leaveLoop, breakLabel, stayLabel);
_module.AddLabel(breakLabel);
_module.AddStatement(SpirvOp.Branch, _structuredLoopMerge);
_module.AddLabel(stayLabel);
}
index = next;
continue;
}
if (target > end || (latch >= 0 && target == end))
{
error = $"unstructured {terminator.Opcode} at 0x{terminator.Pc:X}";
return false;
}
if (target == next)
{
index = next;
continue;
}
// if/else: the skipped range ends with an s_branch over the taken range.
var thenLast = instructions[blocks[target - 1].EndIndex - 1];
if (target < blocks.Count && target - 1 > index && thenLast.Opcode == "SBranch" &&
TryResolveBranchBlock(blocks, thenLast, out var joinTarget) && joinTarget > target && joinTarget <= end &&
!(latch >= 0 && joinTarget == end))
{
if (!emit)
{
if (!TryStructureRange(blocks, next, target, joinTarget, emit: false, out error) ||
!TryStructureRange(blocks, target, joinTarget, joinTarget, emit: false, out error))
{
return false;
}
}
else
{
TryGetBranchCondition(terminator.Opcode, out var taken);
var thenLabel = _module.AllocateId();
var elseLabel = _module.AllocateId();
var joinLabel = _module.AllocateId();
_module.AddStatement(SpirvOp.SelectionMerge, joinLabel, 0);
_module.AddStatement(SpirvOp.BranchConditional, taken, elseLabel, thenLabel);
_module.AddLabel(thenLabel);
if (!TryStructureRange(blocks, next, target, joinTarget, emit: true, out error))
{
return false;
}
_module.AddStatement(SpirvOp.Branch, joinLabel);
_module.AddLabel(elseLabel);
if (!TryStructureRange(blocks, target, joinTarget, joinTarget, emit: true, out error))
{
return false;
}
_module.AddStatement(SpirvOp.Branch, joinLabel);
_module.AddLabel(joinLabel);
}
index = joinTarget;
continue;
}
// if (!taken) { skipped range }
if (!emit)
{
if (!TryStructureRange(blocks, next, target, target, emit: false, out error))
{
return false;
}
}
else
{
TryGetBranchCondition(terminator.Opcode, out var taken);
var bodyLabel = _module.AllocateId();
var mergeLabel = _module.AllocateId();
_module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0);
_module.AddStatement(SpirvOp.BranchConditional, taken, mergeLabel, bodyLabel);
_module.AddLabel(bodyLabel);
if (!TryStructureRange(blocks, next, target, target, emit: true, out error))
{
return false;
}
_module.AddStatement(SpirvOp.Branch, mergeLabel);
_module.AddLabel(mergeLabel);
}
index = target;
}
return true;
}
// The innermost structured loop's exit block and merge label, for its breaks; -1 outside loops.
private int _structuredLoopExit = -1;
private uint _structuredLoopMerge;
// The last block in [header, end) that branches back to header, unconditionally or on a
// wave-uniform condition.
private bool TryFindLoopLatch(IReadOnlyList<ShaderBlock> blocks, int header, int end, out int latch)
{
latch = -1;
var instructions = _request.Program.Instructions;
for (var index = end - 1; index >= header; index--)
{
var terminator = instructions[blocks[index].EndIndex - 1];
if ((IsStructuredCondition(terminator.Opcode) || terminator.Opcode == "SBranch") &&
TryResolveBranchBlock(blocks, terminator, out var target) && target == header)
{
latch = index;
return true;
}
}
return false;
}
// do { blocks [header, latch] } while (latch condition), bounded by the same step guard as
// the dispatcher so a guest loop that never ends cannot wedge the GPU queue.
private bool TryStructureLoop(IReadOnlyList<ShaderBlock> blocks, int header, int latch, bool emit, out string error)
{
var outerExit = _structuredLoopExit;
var outerMerge = _structuredLoopMerge;
try
{
return TryStructureLoopCore(blocks, header, latch, emit, out error);
}
finally
{
_structuredLoopExit = outerExit;
_structuredLoopMerge = outerMerge;
}
}
private bool TryStructureLoopCore(IReadOnlyList<ShaderBlock> blocks, int header, int latch, bool emit, out string error)
{
_structuredLoopExit = latch + 1;
if (!emit)
{
_structuredLoopMerge = 0;
return TryStructureRange(blocks, header, latch + 1, latch + 1, emit: false, out error, latch);
}
var headerLabel = _module.AllocateId();
var bodyLabel = _module.AllocateId();
var continueLabel = _module.AllocateId();
var mergeLabel = _module.AllocateId();
_structuredLoopMerge = mergeLabel;
_module.AddStatement(SpirvOp.Branch, headerLabel);
_module.AddLabel(headerLabel);
_module.AddStatement(SpirvOp.LoopMerge, mergeLabel, continueLabel, 0);
_module.AddStatement(SpirvOp.Branch, bodyLabel);
_module.AddLabel(bodyLabel);
if (!TryStructureRange(blocks, header, latch + 1, latch + 1, emit: true, out error, latch))
{
return false;
}
_module.AddStatement(SpirvOp.Branch, continueLabel);
_module.AddLabel(continueLabel);
var terminator = _request.Program.Instructions[blocks[latch].EndIndex - 1];
var again = _module.ConstantBool(true);
if (terminator.Opcode != "SBranch")
{
TryGetBranchCondition(terminator.Opcode, out again);
}
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));
again = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, again, withinLimit);
}
_module.AddStatement(SpirvOp.BranchConditional, again, headerLabel, mergeLabel);
_module.AddLabel(mergeLabel);
return true;
}
// SHARPEMU_STRUCTURED_FORWARD_BLOCKS=0 always emits the block dispatcher loop.
private static readonly bool StructuredForwardBlocks = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_STRUCTURED_FORWARD_BLOCKS"), "0", StringComparison.Ordinal);
// The natural loops of the program: each block targeted by a backward branch heads a
// loop that ends at the last block branching back to it. Loops must nest; a program
// whose loops overlap keeps the block dispatcher.
private Dictionary<int, int> _loopLatchByHeader = [];
private bool TryBuildLoopRegions(IReadOnlyList<ShaderBlock> blocks, out Dictionary<int, int> latchByHeader)
{
latchByHeader = [];
for (var index = 0; index < blocks.Count; index++)
{
var terminator = _request.Program.Instructions[blocks[index].EndIndex - 1];
if (terminator.Opcode != "SBranch" && !terminator.Opcode.StartsWith("SCbranch", StringComparison.Ordinal))
{
continue;
}
if (!TryGetBranchTargetPc(terminator, out var targetPc))
{
return false;
}
if (IsExitBranchTarget(_request.Program.Instructions, targetPc))
{
continue;
}
if (!TryFindBlock(blocks, targetPc, out var target))
{
return false;
}
if (target <= index)
{
latchByHeader[target] = latchByHeader.TryGetValue(target, out var latch) ? Math.Max(latch, index) : index;
}
}
foreach (var (outerHeader, outerLatch) in latchByHeader)
{
foreach (var (innerHeader, innerLatch) in latchByHeader)
{
if (outerHeader < innerHeader && innerHeader <= outerLatch && innerLatch > outerLatch)
{
return false;
}
}
}
return true;
}
// Emits blocks first..last in order; a nested loop header opens a structured loop.
private bool TryEmitStructuredRange(IReadOnlyList<ShaderBlock> blocks, int first, int last, int enclosingHeader, out string error)
{
error = string.Empty;
for (var index = first; index <= last;)
{
if (index != enclosingHeader && _loopLatchByHeader.TryGetValue(index, out var latch))
{
if (!TryEmitStructuredLoop(blocks, index, latch, out error))
{
return false;
}
index = latch + 1;
continue;
}
var isNext = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
Load(_boolType, _programActive),
_module.AddInstruction(SpirvOp.IEqual, _boolType, Load(_uintType, _programCounter), UInt((uint)index)));
var blockBody = _module.AllocateId();
var blockMerge = _module.AllocateId();
_module.AddStatement(SpirvOp.SelectionMerge, blockMerge, 0);
_module.AddStatement(SpirvOp.BranchConditional, isNext, blockBody, blockMerge);
_module.AddLabel(blockBody);
if (!TryEmitBlock(blocks, index, out error))
{
error = $"block=0x{blocks[index].StartPc:X}: {error}";
return false;
}
_module.AddStatement(SpirvOp.Branch, blockMerge);
_module.AddLabel(blockMerge);
index++;
}
return true;
}
// One natural loop: the body runs its blocks in order and repeats while the next block
// is the header. An exit leaves the program counter past the loop (or at an outer
// header), so the guards after the loop pick up where the invocation continues.
private bool TryEmitStructuredLoop(IReadOnlyList<ShaderBlock> blocks, int header, int latch, out string error)
{
// Once a single-block loop is entered, its back edge already proves that
// this block is active and next. Keep its entry guard outside the loop;
// a conditional around every iteration obstructs driver optimization.
var singleBlock = header == latch;
var entryMerge = singleBlock ? _module.AllocateId() : 0u;
var skippedEntry = singleBlock ? _module.AllocateId() : 0u;
var loopHeader = _module.AllocateId();
var loopBody = _module.AllocateId();
var loopContinue = _module.AllocateId();
var loopMerge = _module.AllocateId();
if (singleBlock)
{
var enters = LogicalAnd(Load(_boolType, _programActive),
_module.AddInstruction(SpirvOp.IEqual, _boolType, Load(_uintType, _programCounter), UInt((uint)header)));
_module.AddStatement(SpirvOp.SelectionMerge, entryMerge, 0);
_module.AddStatement(SpirvOp.BranchConditional, enters, loopHeader, skippedEntry);
}
else
{
_module.AddStatement(SpirvOp.Branch, loopHeader);
}
_module.AddLabel(loopHeader);
_module.AddStatement(SpirvOp.LoopMerge, loopMerge, loopContinue, 0);
_module.AddStatement(SpirvOp.Branch, loopBody);
_module.AddLabel(loopBody);
if (!(singleBlock
? TryEmitBlock(blocks, header, out error)
: TryEmitStructuredRange(blocks, header, latch, header, out error)))
{
return false;
}
_module.AddStatement(SpirvOp.Branch, loopContinue);
_module.AddLabel(loopContinue);
var again = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
Load(_boolType, _programActive),
_module.AddInstruction(SpirvOp.IEqual, _boolType, Load(_uintType, _programCounter), UInt((uint)header)));
if (_maxDispatcherSteps > 0)
{
// The dispatcher's safety valve, counted per loop iteration: a mistranslated
// exit ends the invocation instead of wedging the GPU queue.
var steps = IAdd(Load(_uintType, _iterationGuard), UInt(1));
Store(_iterationGuard, steps);
var withinLimit = _module.AddInstruction(SpirvOp.ULessThan, _boolType, steps, UInt((uint)_maxDispatcherSteps));
again = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, again, withinLimit);
}
_module.AddStatement(SpirvOp.BranchConditional, again, loopHeader, loopMerge);
_module.AddLabel(loopMerge);
if (singleBlock)
{
_module.AddStatement(SpirvOp.Branch, entryMerge);
_module.AddLabel(skippedEntry);
// The previous guarded do-while still counted one iteration when
// the block was skipped. Preserve that diagnostic safety budget.
if (_maxDispatcherSteps > 0)
{
Store(_iterationGuard, IAdd(Load(_uintType, _iterationGuard), UInt(1)));
}
_module.AddStatement(SpirvOp.Branch, entryMerge);
_module.AddLabel(entryMerge);
}
return true;
}
private static string FormatBlockStarts(IReadOnlyList<ShaderBlock> 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<Gen5ShaderInstruction> 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(WaveMaskAny(106, _vcc)),
"SCbranchVccnz" => WaveMaskAny(106, _vcc),
"SCbranchExecz" => LogicalNot(WaveMaskAny(126, _exec)),
"SCbranchExecnz" => WaveMaskAny(126, _exec),
// The emulator does not expose a shader debug session.
"SCbranchCdbgsys" or
"SCbranchCdbguser" or
"SCbranchCdbgsysOrUser" or
"SCbranchCdbgsysAndUser" => _module.ConstantBool(false),
_ => 0,
};
return condition != 0;
}
private bool TryEmitInstruction(
Gen5ShaderInstruction instruction,
out string error)
{
error = string.Empty;
// No shader trap handler is installed, so S_TRAP has no effect.
if (instruction.Opcode == "STrap")
{
return true;
}
if (instruction.Opcode is
"SNop" or
"SSetregB32" or
"SWaitcnt" or
"SInstPrefetch" or
"STtraceData" or
// Wave scheduling priority hint; no effect on results.
"SSetprio" 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")
{
return true;
}
if (instruction.Opcode == "SBarrier")
{
if (_stage == Gen5SpirvStage.Compute)
{
// s_waitcnt vmcnt(0) + s_barrier also publishes buffer and image
// stores to the workgroup: AcquireRelease over uniform, workgroup
// and image memory.
var workgroup = UInt(2);
var semantics = UInt(0x8 | 0x40 | 0x100 | 0x800);
_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 Gen5RayIntersectControl rayIntersect)
{
_deviceAddressInstructionPc = instruction.Pc;
EmitRayIntersect(rayIntersect, instruction.Opcode == "ImageBvh64IntersectRay");
return true;
}
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 (instruction.Control is Gen5DataShareControl { Gds: true } globalShare)
{
return TryEmitGlobalDataShare(instruction, globalShare, out error);
}
if (instruction.Control is not Gen5DataShareControl control)
{
error = "invalid LDS instruction";
return false;
}
switch (instruction.Opcode)
{
case "DsSwizzleB32":
return TryEmitDataShareSwizzle(instruction, control, out error);
case "DsBpermuteB32":
return TryEmitDataShareBpermute(instruction, control, out error);
}
if (_lds == 0 || _ldsElementPointer == 0)
{
error = "invalid LDS instruction";
return false;
}
switch (instruction.Opcode)
{
case "DsAppend":
case "DsConsume":
return TryEmitDataShareWaveCounter(instruction, control, out error);
case "DsMskorB32":
return TryEmitDataShareAtomic(instruction, control, out error);
case "DsWriteB32":
case "DsWriteAddtidB32":
{
if (instruction.Sources.Count < 2)
{
error = "missing LDS write source";
return false;
}
var address = GetRawSource(instruction, 0);
if (instruction.Opcode == "DsWriteAddtidB32")
{
address = IAdd(BitwiseAnd(address, UInt(0xFFFF)), ShiftLeftLogical(GuestWaveLane(), UInt(2)));
}
StoreLds(
LdsPointer(address, control.SingleOffsetBytes),
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.SingleOffsetBytes;
StoreLds(LdsPointer(address, offset), GetRawSource(instruction, 1));
StoreLds(
LdsPointer(address, offset + sizeof(uint)),
GetRawSource(instruction, 2));
return true;
}
case "DsAddU64":
case "DsOrB64":
{
if (instruction.Sources.Count < 3)
{
error = "missing LDS 64-bit atomic source";
return false;
}
var atomicAddress = GetRawSource(instruction, 0);
var isOr = instruction.Opcode == "DsOrB64";
// The LDS allocation is a uint array. Reinterpreting its
// pointer as ulong with OpBitcast is invalid SPIR-V, so use
// the dword fallback even when native 64-bit atomics exist.
// The pair is not atomic against concurrent 64-bit use.
var lowPointer = LdsPointer(
atomicAddress,
control.SingleOffsetBytes);
var highPointer = LdsPointer(
atomicAddress,
control.SingleOffsetBytes + sizeof(uint));
EmitExecConditional(() =>
{
var lowValue = GetRawSource(instruction, 1);
var highValue = GetRawSource(instruction, 2);
if (isOr)
{
// OR is bitwise-independent: no dword influences the
// other, so two 32-bit ORs are the 64-bit OR.
EmitAtomic(
SpirvOp.AtomicOr,
_uintType,
lowPointer,
scope: 2,
semantics: 0x108,
value: () => lowValue,
comparator: () => lowValue);
EmitAtomic(
SpirvOp.AtomicOr,
_uintType,
highPointer,
scope: 2,
semantics: 0x108,
value: () => highValue,
comparator: () => highValue);
return;
}
// ADD needs the carry out of the low dword folded into
// the high one. Each lane detects its own wrap from the
// pre-add value its atomic returned and adds exactly one
// carry, so the high dword ends up with the true count of
// wraps however the lanes interleave.
var originalLow = EmitAtomic(
SpirvOp.AtomicIAdd,
_uintType,
lowPointer,
scope: 2,
semantics: 0x108,
value: () => lowValue,
comparator: () => lowValue);
var sumLow = IAdd(originalLow, lowValue);
var wrapped = _module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
sumLow,
originalLow);
var carry = _module.AddInstruction(
SpirvOp.Select,
_uintType,
wrapped,
UInt(1),
UInt(0));
var highWithCarry = IAdd(highValue, carry);
EmitAtomic(
SpirvOp.AtomicIAdd,
_uintType,
highPointer,
scope: 2,
semantics: 0x108,
value: () => highWithCarry,
comparator: () => highWithCarry);
});
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.SingleOffsetBytes;
for (var dword = 0; dword < dwordCount; dword++)
{
StoreLds(
LdsPointer(address, offset + (uint)(dword * sizeof(uint))),
GetRawSource(instruction, 1 + dword));
}
return true;
}
case "DsWrite2B64":
case "DsWrite2St64B64":
return TryEmitDataShareWritePair64(instruction, control, out error);
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":
case "DsReadAddtidB32":
{
if (instruction.Destinations.Count < 1 ||
instruction.Sources.Count < 1)
{
error = "missing LDS read operand";
return false;
}
var address = GetRawSource(instruction, 0);
if (instruction.Opcode == "DsReadAddtidB32")
{
address = IAdd(BitwiseAnd(address, UInt(0xFFFF)), ShiftLeftLogical(GuestWaveLane(), UInt(2)));
}
var value = Load(
_uintType,
LdsPointer(address, control.SingleOffsetBytes));
StoreV(instruction.Destinations[0].Value, value);
return true;
}
case "DsReadI8":
{
if (instruction.Destinations.Count < 1 || instruction.Sources.Count < 1)
{
error = "missing LDS signed byte read operand";
return false;
}
var address = GetRawSource(instruction, 0);
var byteAddress = control.SingleOffsetBytes == 0
? address
: IAdd(address, UInt(control.SingleOffsetBytes));
var word = Load(_uintType, LdsPointer(address, control.SingleOffsetBytes));
var shift = ShiftLeftLogical(BitwiseAnd(byteAddress, UInt(3)), UInt(3));
var packed = ShiftRightLogical(word, shift);
var signedByte = _module.AddInstruction(
SpirvOp.BitFieldSExtract,
_intType,
Bitcast(_intType, packed),
UInt(0),
UInt(8));
StoreV(instruction.Destinations[0].Value, Bitcast(_uintType, signedByte));
return true;
}
case "DsReadB64":
case "DsReadB96":
case "DsReadB128":
{
var dwordCount = instruction.Opcode switch { "DsReadB64" => 2, "DsReadB96" => 3, _ => 4 };
if (instruction.Destinations.Count < dwordCount ||
instruction.Sources.Count < 1)
{
error = "missing LDS read operand";
return false;
}
var address = GetRawSource(instruction, 0);
var offset = control.SingleOffsetBytes;
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 "DsRead2B64":
return TryEmitDataShareReadPair64(instruction, control, out error);
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 bool TryEmitDataShareWaveCounter(
Gen5ShaderInstruction instruction,
Gen5DataShareControl control,
out string error)
{
error = string.Empty;
if (instruction.Sources.Count < 1 || instruction.Destinations.Count < 1)
{
error = $"missing {instruction.Opcode} operand";
return false;
}
var offset = control.SingleOffsetBytes;
var m0 = GetRawSource(instruction, 0);
var baseAddress = ShiftRightLogical(m0, UInt(16));
var sizeBytes = BitwiseAnd(m0, UInt(0xFFFF));
var inBounds = _module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
UInt(offset + 3),
sizeBytes);
var pointer = LdsPointer(baseAddress, offset);
var destination = instruction.Destinations[0].Value;
var active = Load(_boolType, _exec);
var activeMask = BooleanToWaveMask(active);
var activeLow = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
activeMask);
var activeHigh = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
activeMask,
_module.Constant64(_ulongType, 32)));
// OpBitCount needs a 32-bit operand without maintenance9.
var activeCount = IAdd(
_module.AddInstruction(SpirvOp.BitCount, _uintType, activeLow),
_module.AddInstruction(SpirvOp.BitCount, _uintType, activeHigh));
var firstLane = _module.AddInstruction(
SpirvOp.Select,
_uintType,
IsNotZero(activeLow),
Ext(73, _uintType, activeLow),
IAdd(Ext(73, _uintType, activeHigh), UInt(32)));
var isFirstActive = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
inBounds,
_module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
active,
_module.AddInstruction(
SpirvOp.IEqual,
_boolType,
GuestWaveLane(),
firstLane)));
EmitConditional(isFirstActive, () =>
{
uint original;
if (_stage == Gen5SpirvStage.Compute)
{
original = EmitAtomic(
instruction.Opcode == "DsAppend"
? SpirvOp.AtomicIAdd
: SpirvOp.AtomicISub,
_uintType,
pointer,
scope: 2,
semantics: 0x108,
value: () => activeCount,
comparator: () => UInt(0));
}
else
{
// Vulkan graphics stages cannot use Workgroup storage.
// Keep the counter in the first active lane's private LDS
// model, but preserve the RDNA2 wave operation: change it
// by the active-lane count and broadcast the old value.
original = Load(_uintType, pointer);
var changed = instruction.Opcode == "DsAppend"
? IAdd(original, activeCount)
: _module.AddInstruction(
SpirvOp.ISub,
_uintType,
original,
activeCount);
Store(pointer, changed);
}
StoreV(destination, original);
});
var firstValue = LoadV(destination);
uint broadcast;
if (_emulateWave64)
{
broadcast = ExchangeWave64Value(isFirstActive, firstValue);
}
else
{
broadcast = ShuffleLane(firstValue, firstLane);
}
var validResult = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
inBounds,
IsNotZero64(activeMask));
StoreV(
destination,
_module.AddInstruction(
SpirvOp.Select,
_uintType,
validResult,
broadcast,
UInt(0)));
return true;
}
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);
// Inactive lanes must not read and write back another lane's shared value.
EmitConditional(active, () => Store(pointer, value));
}
private bool TryEmitDataShareAtomic(
Gen5ShaderInstruction instruction,
Gen5DataShareControl control,
out string error)
{
error = string.Empty;
if (instruction.Opcode == "DsMskorB32")
{
if (instruction.Sources.Count < 3)
{
error = "missing LDS masked-OR source";
return false;
}
var maskedPointer = LdsPointer(
GetRawSource(instruction, 0),
control.SingleOffsetBytes);
EmitExecConditional(() =>
{
var original = Load(_uintType, maskedPointer);
var updated = BitwiseOr(
BitwiseAnd(
original,
_module.AddInstruction(
SpirvOp.Not,
_uintType,
GetRawSource(instruction, 1))),
GetRawSource(instruction, 2));
EmitAtomic(
SpirvOp.AtomicCompareExchange,
_uintType,
maskedPointer,
scope: 2,
semantics: 0x108,
value: () => updated,
comparator: () => original);
});
return true;
}
if (instruction.Opcode is "DsMinF32" or "DsMaxF32")
{
if (instruction.Sources.Count < 3)
{
error = $"missing LDS operands for {instruction.Opcode}";
return false;
}
var floatAddress = GetRawSource(instruction, 0);
var floatPointer = LdsPointer(floatAddress, control.SingleOffsetBytes);
EmitExecConditional(() =>
EmitDataShareFloatAtomic(
floatPointer,
GetRawSource(instruction, 1),
GetRawSource(instruction, 2),
instruction.Opcode == "DsMaxF32",
scope: 2,
semantics: 0x108));
return true;
}
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.SingleOffsetBytes);
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;
}
private void EmitDataShareFloatAtomic(
uint pointer,
uint data,
uint compare,
bool maxValue,
uint scope,
uint semantics)
{
var preheader = _module.AllocateId();
var header = _module.AllocateId();
var continueLabel = _module.AllocateId();
var mergeLabel = _module.AllocateId();
var exchanged = _module.AllocateId();
_module.AddStatement(SpirvOp.Branch, preheader);
_module.AddLabel(preheader);
var initial = _module.AddInstruction(
SpirvOp.AtomicLoad,
_uintType,
pointer,
UInt(scope),
UInt((semantics & ~0xCu) | 0x2u));
_module.AddStatement(SpirvOp.Branch, header);
_module.AddLabel(header);
var observed = _module.AddInstruction(
SpirvOp.Phi,
_uintType,
initial,
preheader,
exchanged,
continueLabel);
var observedFloat = Bitcast(_floatType, observed);
var compareFloat = Bitcast(_floatType, compare);
var replace = _module.AddInstruction(
maxValue ? SpirvOp.FOrdGreaterThan : SpirvOp.FOrdLessThan,
_boolType,
maxValue ? observedFloat : compareFloat,
maxValue ? compareFloat : observedFloat);
var next = _module.AddInstruction(SpirvOp.Select, _uintType, replace, data, observed);
_module.AddStatement(
SpirvOp.AtomicCompareExchange,
_uintType,
exchanged,
pointer,
UInt(scope),
UInt(semantics),
UInt((semantics & ~0x8u) | 0x2u),
next,
observed);
var success = _module.AddInstruction(SpirvOp.IEqual, _boolType, exchanged, observed);
_module.AddStatement(SpirvOp.LoopMerge, mergeLabel, continueLabel, 0);
_module.AddStatement(SpirvOp.BranchConditional, success, mergeLabel, continueLabel);
_module.AddLabel(continueLabel);
_module.AddStatement(SpirvOp.Branch, header);
_module.AddLabel(mergeLabel);
}
private uint EmitBufferFloatAtomic(
uint pointer,
uint value,
bool maxValue,
uint scope,
uint semantics)
{
var preheader = _module.AllocateId();
var header = _module.AllocateId();
var continueLabel = _module.AllocateId();
var mergeLabel = _module.AllocateId();
var exchanged = _module.AllocateId();
_module.AddStatement(SpirvOp.Branch, preheader);
_module.AddLabel(preheader);
var initial = _module.AddInstruction(
SpirvOp.AtomicLoad,
_uintType,
pointer,
UInt(scope),
UInt((semantics & ~0xCu) | 0x2u));
_module.AddStatement(SpirvOp.Branch, header);
_module.AddLabel(header);
var observed = _module.AddInstruction(
SpirvOp.Phi,
_uintType,
initial,
preheader,
exchanged,
continueLabel);
var observedFloat = Bitcast(_floatType, observed);
var valueFloat = Bitcast(_floatType, value);
var replace = _module.AddInstruction(
maxValue ? SpirvOp.FOrdGreaterThan : SpirvOp.FOrdLessThan,
_boolType,
valueFloat,
observedFloat);
var next = _module.AddInstruction(SpirvOp.Select, _uintType, replace, value, observed);
_module.AddStatement(
SpirvOp.AtomicCompareExchange,
_uintType,
exchanged,
pointer,
UInt(scope),
UInt(semantics),
UInt((semantics & ~0x8u) | 0x2u),
next,
observed);
var success = _module.AddInstruction(SpirvOp.IEqual, _boolType, exchanged, observed);
_module.AddStatement(SpirvOp.LoopMerge, mergeLabel, continueLabel, 0);
_module.AddStatement(SpirvOp.BranchConditional, success, mergeLabel, continueLabel);
_module.AddLabel(continueLabel);
_module.AddStatement(SpirvOp.Branch, header);
_module.AddLabel(mergeLabel);
return observed;
}
// 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" or "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<uint> value,
Func<uint> 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;
}
if (_perVertexAttributes.Contains(interpolation.Attribute))
{
return TryEmitInterpolationParameter(instruction, interpolation, input, destination, out error);
}
if (_perVertexSourcedAttributes.TryGetValue(interpolation.Attribute, out var flatSource))
{
EmitPerVertexSourcedInterpolation(interpolation, input, destination, flatSource);
return true;
}
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;
{
return TryEmitLayoutScalarMemory(instruction, control, out error);
}
}
private bool TryEmitGlobalMemory(
Gen5ShaderInstruction instruction,
Gen5GlobalMemoryControl control,
out string error)
{
error = string.Empty;
{
return TryEmitLayoutGlobalMemory(instruction, control, out error);
}
}
private bool TryEmitBufferMemory(
Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control,
out string error)
{
error = string.Empty;
if (instruction.Opcode is "BufferWbinvl1" or "BufferWbinvl1Vol")
{
// The host storage path is already coherent; the guest instruction
// invalidates a cache level and does not access the buffer itself.
return true;
}
if (control.Typed && instruction.Opcode.Contains("D16", StringComparison.Ordinal))
{
error = $"unsupported buffer opcode {instruction.Opcode}";
return false;
}
if (_stage == Gen5SpirvStage.Vertex &&
_vertexInputsByPc.TryGetValue(instruction.Pc, out var vertexInput))
{
return TryEmitVertexInputFetch(control, vertexInput, out error);
}
if (_request.Memory.Find(instruction.Pc) is { DeviceDescriptor: true })
{
return TryEmitDeviceDescriptorBufferMemory(instruction, control, out error);
}
if (_request.Memory.TryGetIndex(instruction.Pc, 0, out var accessMemoryIndex))
{
var accessMemory = _request.Memory[accessMemoryIndex];
if (accessMemory.PlanningOnly)
{
// The access belongs to the linearized tail of a fused
// shader. Its descriptor is supplied by the continuation
// object and is intentionally absent from this plan; keep
// the dead linear instruction side-effect free.
return true;
}
var strategy = accessMemory.BufferDescriptor?.ChooseStrategy(
control.Typed,
accessMemory.Formatted,
accessMemory.Access == MemoryAccess.Atomic)
?? BufferLoweringStrategy.NativeBinding;
if (strategy == BufferLoweringStrategy.PhysicalStorageBuffer)
{
return TryEmitPhysicalStorageBufferMemory(instruction, control, out error);
}
if (strategy == BufferLoweringStrategy.BoundedCandidateTable)
{
return TryEmitBoundedCandidateTableMemory(instruction, control, accessMemoryIndex, out error);
}
}
if (!TryResolveLayoutBuffer(instruction.Pc, out var bindingIndex, out _))
{
error = "missing buffer-memory binding";
return false;
}
return EmitResolvedBufferMemory(instruction, control, bindingIndex, out error);
}
// One buffer operation against a resolved candidate binding: the dense binding path
// and every arm of a bounded candidate table share it, so the operation itself
// stays independent of how its descriptor was selected.
private bool EmitResolvedBufferMemory(
Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control,
int bindingIndex,
out string error)
{
error = string.Empty;
var info = _request.Resources.Info;
if (bindingIndex < 0 || bindingIndex >= info.Buffers.Count)
{
error = $"buffer binding {bindingIndex} is out of range";
return false;
}
var specialized = info.Buffers[bindingIndex];
var stride = UInt(specialized.PackedStride & 0x3FFF);
var descriptorFormat = specialized.DescriptorFormat;
var descriptorWord3 = UInt((specialized.DescriptorFormat << 12) | (specialized.DescriptorSwizzle & 0xFFF));
var scalarOffset = instruction.Sources.Count > 2
? GetRawSource(instruction, 2)
: UInt(0);
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 is "BufferAtomicSwapX2" or "BufferAtomicOrX2")
{
EmitExecConditional(() =>
{
var firstInRange = IsBufferWordInRange(bindingIndex, dwordAddress);
var secondAddress = IAdd(dwordAddress, UInt(1));
var secondInRange = IsBufferWordInRange(bindingIndex, secondAddress);
EmitConditional(LogicalAnd(firstInRange, secondInRange), () =>
{
var atomicOp = instruction.Opcode == "BufferAtomicSwapX2"
? SpirvOp.AtomicExchange
: SpirvOp.AtomicOr;
var first = EmitAtomic(
atomicOp,
_uintType,
BufferWordPointer(bindingIndex, dwordAddress),
scope: 1,
semantics: 0x48,
value: () => LoadV(control.VectorData),
comparator: () => UInt(0));
var second = EmitAtomic(
atomicOp,
_uintType,
BufferWordPointer(bindingIndex, secondAddress),
scope: 1,
semantics: 0x48,
value: () => LoadV(control.VectorData + 1),
comparator: () => UInt(0));
if (control.Glc)
{
StoreV(control.VectorData, first);
StoreV(control.VectorData + 1, second);
}
});
});
return true;
}
if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
{
var atomicSuffix = instruction.Opcode["BufferAtomic".Length..];
if (atomicSuffix is "SwapX2" or "OrX2")
{
var wideAtomicOp = atomicSuffix == "SwapX2"
? SpirvOp.AtomicExchange
: SpirvOp.AtomicOr;
EmitExecConditional(() =>
{
var secondAddress = IAdd(dwordAddress, UInt(1));
var inRange = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
IsBufferWordInRange(bindingIndex, dwordAddress),
IsBufferWordInRange(bindingIndex, secondAddress));
EmitConditional(inRange, () =>
{
var originalLow = EmitAtomic(
wideAtomicOp,
_uintType,
BufferWordPointer(bindingIndex, dwordAddress),
scope: 1,
semantics: 0x48,
value: () => LoadV(control.VectorData),
comparator: () => UInt(0));
var originalHigh = EmitAtomic(
wideAtomicOp,
_uintType,
BufferWordPointer(bindingIndex, secondAddress),
scope: 1,
semantics: 0x48,
value: () => LoadV(control.VectorData + 1),
comparator: () => UInt(0));
if (control.Glc)
{
StoreV(control.VectorData, originalLow);
StoreV(control.VectorData + 1, originalHigh);
}
});
});
return true;
}
if (atomicSuffix is "Fmin" or "Fmax")
{
EmitExecConditional(() =>
{
var inRange = IsBufferWordInRange(bindingIndex, dwordAddress);
EmitConditional(inRange, () =>
{
var original = EmitBufferFloatAtomic(
BufferWordPointer(bindingIndex, dwordAddress),
LoadV(control.VectorData),
maxValue: atomicSuffix == "Fmax",
scope: 1,
semantics: 0x48);
if (control.Glc)
{
StoreV(control.VectorData, original);
}
});
});
return true;
}
if (!TryGetAtomicOp(atomicSuffix, 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 is "BufferStoreFormatX" or "BufferStoreFormatXy" or
"BufferStoreFormatXyz" or "BufferStoreFormatXyzw")
{
if (descriptorFormat == 0)
{
return true;
}
if (!Gfx10UnifiedFormat.TryDecode(descriptorFormat, out _, out _))
{
error = $"unsupported buffer store format {descriptorFormat}";
return false;
}
EmitExecConditional(() => TryEmitBufferFormatStore(
bindingIndex, byteAddress, control, descriptorWord3, descriptorFormat));
return true;
}
if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal) ||
instruction.Opcode.StartsWith("BufferStoreFormat", StringComparison.Ordinal) ||
instruction.Opcode.StartsWith("TBufferStoreFormat", StringComparison.Ordinal) ||
instruction.Opcode.StartsWith("BufferStoreByte", StringComparison.Ordinal) ||
instruction.Opcode.StartsWith("BufferStoreShort", StringComparison.Ordinal))
{
EmitExecConditional(() =>
{
if (control.Typed && TryEmitBufferFormatStore(bindingIndex, byteAddress, control, descriptorWord3, control.TypedFormat))
{
return;
}
if (TryGetSubdwordStoreInfo(
instruction.Opcode,
out var byteCount,
out var sourceShift))
{
StoreBufferBytes(
bindingIndex,
byteAddress,
LoadV(control.VectorData),
byteCount,
sourceShift);
return;
}
// BUFFER_STORE/LOAD_DWORD(x2/x3/x4) are dword-aligned by the GCN ISA, same as the GLOBAL case above — no per-byte reassembly needed.
for (uint index = 0; index < control.DwordCount; index++)
{
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
StoreBufferWord(
bindingIndex,
indexedDwordAddress,
LoadV(control.VectorData + index));
}
});
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;
}
// A typed load converts with the instruction format, a formatted untyped load
// with the descriptor format and swizzle; raw dword loads take the path below.
if (IsFormatBufferLoad(instruction.Opcode))
{
if (!control.Typed)
{
EmitBufferFormatLoad(
bindingIndex,
byteAddress,
descriptorWord3,
control.VectorData,
control.DwordCount);
return true;
}
if (TryEmitTypedBufferFormatLoad(bindingIndex, byteAddress, control, descriptorWord3))
{
return true;
}
}
for (uint index = 0; index < control.DwordCount; index++)
{
var indexedDwordAddress = index == 0
? dwordAddress
: IAdd(dwordAddress, UInt(index));
StoreV(
control.VectorData + index,
LoadBufferWord(bindingIndex, indexedDwordAddress));
}
return true;
}
// BufferLoweringStrategy.BoundedCandidateTable: a runtime V# whose descriptors cannot
// be reconstructed from its raw words. The runtime V# sits in the scalar resource
// registers; its base-address dword is the probe key into the flattened candidate
// mapping, and each arm binds a native candidate and runs the ordinary buffer op.
private bool TryEmitBoundedCandidateTableMemory(
Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control,
int memoryIndex,
out string error)
{
error = string.Empty;
if (!_request.BufferCandidateTableByMemoryIndex.TryGetValue(memoryIndex, out var table))
{
// A formatted load needs a statically known Vulkan view format. Most
// runtime V#s prove a bounded SRT candidate set above, but a few Yotei
// material-lookup paths merge descriptors through control flow and have
// no finite, safe candidate table. Do not reinterpret an arbitrary
// device address with a guessed format: a null read is the defined
// fallback, matching the bindless-image fallback in ResourceTracker.
if (instruction.Opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal))
{
for (uint index = 0; index < control.DwordCount; index++)
{
StoreV(control.VectorData + index, UInt(0));
}
return true;
}
error = $"runtime buffer descriptor has no candidate table for {instruction.Opcode}";
return false;
}
if (table.CandidateCount == 0)
{
error = "runtime buffer descriptor candidate table is empty";
return false;
}
if (table.CandidateCount == 1)
{
return EmitResolvedBufferMemory(instruction, control, (int)table.FirstCandidate, out error);
}
if (!HasFlattenedTable)
{
error = "runtime buffer descriptor candidate table without a flattened table binding";
return false;
}
if (instruction.Sources.Count < 2 || instruction.Sources[1].Kind != Gen5OperandKind.ScalarRegister)
{
error = "runtime buffer descriptor has no scalar resource base";
return false;
}
var probeKey = LoadS(instruction.Sources[1].Value);
var selector = SelectBufferCandidate(table, probeKey);
var emitted = true;
var caseError = string.Empty;
for (uint index = 0; index < table.CandidateCount && emitted; index++)
{
var candidate = table.FirstCandidate + index;
EmitConditional(_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt(index)), () =>
{
if (!EmitResolvedBufferMemory(instruction, control, (int)candidate, out caseError))
{
emitted = false;
}
});
}
error = caseError;
return emitted;
}
// Searches the sorted base-address mapping of a candidate table for the runtime V#'s
// probe key; the result is the candidate-local index, candidate 0 when absent.
private uint SelectBufferCandidate(BufferCandidateTableUse table, uint key)
{
var mapping = UInt(table.MappingOffset);
var count = LoadFlattenedWord(mapping);
var low = UInt(0);
var high = count;
for (uint iteration = 0; iteration < table.SearchIterations; iteration++)
{
var span = _module.AddInstruction(SpirvOp.ISub, _uintType, high, low);
var middle = IAdd(low, ShiftRightLogical(span, UInt(1)));
var probeSlot = IAdd(IAdd(mapping, UInt(1)), ShiftLeftLogical(middle, UInt(1)));
var probeKey = LoadFlattenedWord(probeSlot);
var moveUp = LogicalAnd(
_module.AddInstruction(SpirvOp.ULessThan, _boolType, probeKey, key),
_module.AddInstruction(SpirvOp.ULessThan, _boolType, low, high));
low = _module.AddInstruction(SpirvOp.Select, _uintType, moveUp, IAdd(middle, UInt(1)), low);
high = _module.AddInstruction(SpirvOp.Select, _uintType, moveUp, high, middle);
}
var foundSlot = IAdd(IAdd(mapping, UInt(1)), ShiftLeftLogical(low, UInt(1)));
var found = LogicalAnd(
_module.AddInstruction(SpirvOp.ULessThan, _boolType, low, count),
_module.AddInstruction(SpirvOp.IEqual, _boolType, LoadFlattenedWord(foundSlot), key));
var mapped = LoadFlattenedWord(IAdd(foundSlot, UInt(1)));
var inRange = LogicalAnd(
found,
_module.AddInstruction(SpirvOp.ULessThan, _boolType, mapped, UInt(table.CandidateCount)));
return _module.AddInstruction(SpirvOp.Select, _uintType, inRange, mapped, UInt(0));
}
private bool TryEmitDeviceDescriptorBufferMemory(
Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control,
out string error)
{
error = string.Empty;
if (control.Typed || instruction.Opcode.StartsWith("TBuffer", StringComparison.Ordinal))
{
error = $"device buffer descriptor operation {instruction.Opcode} is not supported";
return false;
}
_deviceAddressInstructionPc = instruction.Pc;
var (baseAddress, size, stride, descriptorWord3) = LoadDeviceBufferDescriptor(control.ScalarResource);
var scalarOffset = instruction.Sources.Count > 2
? GetRawSource(instruction, 2)
: UInt(0);
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));
if (IsFormatBufferLoad(instruction.Opcode))
{
EmitDeviceBufferFormatLoad(
baseAddress,
size,
byteAddress,
descriptorWord3,
control.VectorData,
control.DwordCount);
return true;
}
if (instruction.Opcode.Contains("Format", StringComparison.Ordinal))
{
error = $"device buffer descriptor operation {instruction.Opcode} is not supported";
return false;
}
if (instruction.Opcode.StartsWith("BufferAtomic", StringComparison.Ordinal))
{
return TryEmitDeviceDescriptorBufferAtomic(instruction, control, baseAddress, size, byteAddress, out error);
}
if (TryGetSubdwordStoreInfo(instruction.Opcode, out var storeByteCount, out var sourceShift))
{
EmitExecConditional(() =>
{
var inRange = IsDeviceBufferByteRangeInRange(size, byteAddress, storeByteCount);
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask));
EmitConditional(inRange, () =>
StoreDeviceBytes(address, LoadV(control.VectorData), storeByteCount, sourceShift, _module.ConstantBool(true)));
});
return true;
}
if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal))
{
EmitExecConditional(() =>
{
for (uint index = 0; index < control.DwordCount; index++)
{
var componentAddress = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreDeviceBufferWord(
baseAddress,
size,
componentAddress,
LoadV(control.VectorData + index));
}
});
return true;
}
if (TryGetSubdwordLoadInfo(
instruction.Opcode,
out var loadByteCount,
out var signExtend,
out var d16,
out var d16High))
{
var inRange = IsDeviceBufferByteRangeInRange(size, byteAddress, loadByteCount);
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask));
Store(_deviceBufferWordScratch, UInt(0));
EmitConditional(inRange, () => Store(
_deviceBufferWordScratch,
LoadSubdwordDeviceValue(
address,
LoadV(control.VectorData),
loadByteCount,
signExtend,
d16,
d16High)));
StoreV(control.VectorData, Load(_uintType, _deviceBufferWordScratch));
return true;
}
if (instruction.Opcode.StartsWith("BufferLoadDword", StringComparison.Ordinal))
{
for (uint index = 0; index < control.DwordCount; index++)
{
var componentAddress = index == 0
? byteAddress
: IAdd(byteAddress, UInt(index * sizeof(uint)));
StoreV(
control.VectorData + index,
LoadDeviceBufferWord(baseAddress, size, componentAddress));
}
return true;
}
error = $"unsupported device buffer descriptor opcode {instruction.Opcode}";
return false;
}
private bool TryEmitDeviceDescriptorBufferAtomic(
Gen5ShaderInstruction instruction,
Gen5BufferMemoryControl control,
uint baseAddress,
uint size,
uint byteAddress,
out string error)
{
error = string.Empty;
var atomicSuffix = instruction.Opcode["BufferAtomic".Length..];
if (atomicSuffix is "SwapX2" or "OrX2")
{
var atomicOp = atomicSuffix == "SwapX2" ? SpirvOp.AtomicExchange : SpirvOp.AtomicOr;
EmitExecConditional(() =>
{
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, 2 * sizeof(uint));
var firstAddress = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
var secondAddress = And64(IAdd64(baseAddress, Widen(IAdd(byteAddress, UInt(sizeof(uint))))), ULong(DeviceAddressMask & ~3ul));
EmitConditional(valid, () =>
{
var (firstPointer, firstMapped) = ResolveDeviceAddress(firstAddress);
var (secondPointer, secondMapped) = ResolveDeviceAddress(secondAddress);
var bothMapped = LogicalAnd(firstMapped, secondMapped);
EmitConditional(bothMapped, () =>
{
var originalLow = EmitAtomic(
atomicOp, _uintType, DeviceWordPointer(firstPointer), 1, 0x48,
() => LoadV(control.VectorData), () => UInt(0));
var originalHigh = EmitAtomic(
atomicOp, _uintType, DeviceWordPointer(secondPointer), 1, 0x48,
() => LoadV(control.VectorData + 1), () => UInt(0));
if (control.Glc)
{
StoreV(control.VectorData, originalLow);
StoreV(control.VectorData + 1, originalHigh);
}
});
});
});
return true;
}
if (atomicSuffix is "Fmin" or "Fmax")
{
EmitExecConditional(() =>
{
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, sizeof(uint));
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
EmitConditional(valid, () =>
{
var (pointer, mapped) = ResolveDeviceAddress(address);
EmitConditional(mapped, () =>
{
var original = EmitBufferFloatAtomic(
DeviceWordPointer(pointer),
LoadV(control.VectorData),
maxValue: atomicSuffix == "Fmax",
scope: 1,
semantics: 0x48);
if (control.Glc)
StoreV(control.VectorData, original);
});
});
});
return true;
}
if (!TryGetAtomicOp(atomicSuffix, out var atomicOperation))
{
error = $"unsupported buffer atomic opcode {instruction.Opcode}";
return false;
}
EmitExecConditional(() =>
{
var valid = IsDeviceBufferByteRangeInRange(size, byteAddress, sizeof(uint));
var address = And64(IAdd64(baseAddress, Widen(byteAddress)), ULong(DeviceAddressMask & ~3ul));
EmitConditional(valid, () =>
{
var (pointer, mapped) = ResolveDeviceAddress(address);
EmitConditional(mapped, () =>
{
var original = EmitAtomic(
atomicOperation,
_uintType,
DeviceWordPointer(pointer),
1,
0x48,
() => LoadV(control.VectorData),
() => LoadV(control.VectorData + 1));
if (control.Glc)
StoreV(control.VectorData, original);
});
});
});
return true;
}
private void EmitDeviceBufferFormatLoad(
uint baseAddress,
uint size,
uint byteAddress,
uint descriptorWord3,
uint vectorData,
uint componentCount)
{
var unifiedFormat = BitwiseAnd(
ShiftRightLogical(descriptorWord3, UInt(12)),
UInt(0x7F));
var (dataFormat, numberFormat) = DecodeGfx10BufferFormat(unifiedFormat);
var canonical = new uint[4];
var componentBounds = new uint[4];
for (var component = 0; component < canonical.Length; component++)
{
canonical[component] = LoadGfx10DeviceBufferFormatComponent(
baseAddress,
size,
byteAddress,
dataFormat,
numberFormat,
component,
out componentBounds[component]);
}
var selectors = new uint[componentCount];
var inBounds = _module.ConstantBool(true);
for (uint destination = 0; destination < componentCount; destination++)
{
var selector = BitwiseAnd(
ShiftRightLogical(descriptorWord3, UInt(destination * 3)),
UInt(7));
selectors[destination] = selector;
var selectedInBounds = _module.ConstantBool(true);
for (uint component = 0; component < 4; component++)
{
selectedInBounds = _module.AddInstruction(
SpirvOp.Select,
_boolType,
_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt(component + 4)),
componentBounds[component],
selectedInBounds);
}
inBounds = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, inBounds, selectedInBounds);
}
var one = Gfx10FormatOne(numberFormat);
for (uint destination = 0; destination < componentCount; destination++)
{
var selector = selectors[destination];
var constant = SelectUInt(selector, 1, one, UInt(0));
var value = constant;
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,
_module.AddInstruction(SpirvOp.Select, _uintType, inBounds, value, constant));
}
}
private uint LoadGfx10DeviceBufferFormatComponent(
uint baseAddress,
uint size,
uint elementAddress,
uint dataFormat,
uint numberFormat,
int component,
out uint componentInBounds)
{
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);
}
foreach (var layout in Gfx10UnifiedFormat.ComponentLayouts)
{
if (layout.Component == (uint)component)
SetLayout(layout.DataFormat, layout.ByteOffset, layout.BitOffset, layout.BitCount);
}
var componentAddress = IAdd(elementAddress, byteOffset);
var componentBytes = ShiftRightLogical(IAdd(IAdd(bitOffset, bitCount), UInt(7)), UInt(3));
var lastByteOffset = _module.AddInstruction(SpirvOp.ISub, _uintType, componentBytes, UInt(1));
var hasComponent = _module.AddInstruction(SpirvOp.INotEqual, _boolType, bitCount, UInt(0));
var inRange = IsDeviceBufferElementInRange(size, componentAddress, lastByteOffset);
var accessAllowed = LogicalAnd(hasComponent, inRange);
componentInBounds = _module.AddInstruction(
SpirvOp.LogicalOr,
_boolType,
LogicalNot(hasComponent),
inRange);
var packed = UInt(0);
for (uint index = 0; index < sizeof(uint); index++)
{
var address = index == 0 ? componentAddress : IAdd(componentAddress, UInt(index));
var word = LoadDeviceBufferWord(baseAddress, size, address, accessAllowed);
var shift = ShiftLeftLogical(BitwiseAnd(address, UInt(3)), UInt(3));
var value = BitwiseAnd(ShiftRightLogical(word, shift), UInt(0xFF));
packed = BitwiseOr(packed, ShiftLeftLogical(value, UInt(index * 8)));
}
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 void EmitBufferFormatLoad(
int bindingIndex,
uint byteAddress,
uint descriptorWord3,
uint vectorData,
uint componentCount)
{
var unifiedFormat = BitwiseAnd(
ShiftRightLogical(descriptorWord3, UInt(12)),
UInt(0x7F));
var (dataFormat, numberFormat) = DecodeGfx10BufferFormat(unifiedFormat);
var canonical = new uint[4];
var componentBounds = new uint[4];
for (var component = 0; component < canonical.Length; component++)
{
canonical[component] = LoadGfx10BufferFormatComponent(
bindingIndex,
byteAddress,
dataFormat,
numberFormat,
component,
out componentBounds[component]);
}
// Only selected memory components contribute to the shared bounds check.
var selectors = new uint[componentCount];
var inBounds = _module.ConstantBool(true);
for (uint destination = 0; destination < componentCount; destination++)
{
var selector = BitwiseAnd(
ShiftRightLogical(descriptorWord3, UInt(destination * 3)), UInt(7));
selectors[destination] = selector;
var selectedInBounds = _module.ConstantBool(true);
for (uint component = 0; component < 4; component++)
{
selectedInBounds = _module.AddInstruction(
SpirvOp.Select, _boolType,
_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt(component + 4)),
componentBounds[component], selectedInBounds);
}
inBounds = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, inBounds, selectedInBounds);
}
var one = Gfx10FormatOne(numberFormat);
for (uint destination = 0; destination < componentCount; destination++)
{
var selector = selectors[destination];
var constant = SelectUInt(selector, 1, one, UInt(0));
var value = constant;
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,
_module.AddInstruction(SpirvOp.Select, _uintType, inBounds, value, constant));
}
}
// Check the first and last dwords of the required range together.
private uint IsBufferElementInRange(int bindingIndex, uint byteAddress, uint lastByteOffset)
{
var first = IsBufferWordInRange(bindingIndex, ShiftRightLogical(byteAddress, UInt(2)));
var last = IsBufferWordInRange(
bindingIndex,
ShiftRightLogical(IAdd(byteAddress, lastByteOffset), UInt(2)));
return _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, first, last);
}
// Component i of a typed load comes from memory component i; components the
// format does not have read as zero. An unbound descriptor reads as zero.
private bool TryEmitTypedBufferFormatLoad(
int bindingIndex,
uint byteAddress,
Gen5BufferMemoryControl control,
uint descriptorWord3)
{
if (!Gfx10UnifiedFormat.TryDecode(control.TypedFormat, out var dataFormat, out var numberFormat))
{
return false;
}
var componentCount = Gfx10UnifiedFormat.ComponentCount(dataFormat);
if (componentCount == 0)
{
return false;
}
// All transferred components must be bound and inside the binding.
var valid = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
IsDescriptorBound(descriptorWord3),
IsBufferElementInRange(
bindingIndex,
byteAddress,
UInt(Gfx10UnifiedFormat.GetAccessByteSize(dataFormat, control.DwordCount) - 1)));
var dataFormatId = UInt(dataFormat);
var numberFormatId = UInt(numberFormat);
for (uint destination = 0; destination < control.DwordCount; destination++)
{
var value = destination < componentCount
? LoadGfx10BufferFormatComponent(
bindingIndex,
byteAddress,
dataFormatId,
numberFormatId,
(int)destination,
out _)
: UInt(0);
StoreV(
control.VectorData + destination,
_module.AddInstruction(SpirvOp.Select, _uintType, valid, value, UInt(0)));
}
return true;
}
// A formatted store converts each register with the selected number format and places
// the bits at the component's offset; all transferred components are stored or dropped.
private bool TryEmitBufferFormatStore(
int bindingIndex,
uint byteAddress,
Gen5BufferMemoryControl control,
uint descriptorWord3,
uint unifiedFormat)
{
if (!Gfx10UnifiedFormat.TryDecode(unifiedFormat, out var dataFormat, out var numberFormat))
{
return false;
}
var componentCount = Math.Min(control.DwordCount, Gfx10UnifiedFormat.ComponentCount(dataFormat));
if (componentCount == 0)
{
return false;
}
var elementBytes = Gfx10UnifiedFormat.GetAccessByteSize(dataFormat, componentCount);
var allowed = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
IsDescriptorBound(descriptorWord3),
IsBufferElementInRange(bindingIndex, byteAddress, UInt(elementBytes - 1)));
EmitConditional(allowed, () =>
{
if (Gfx10UnifiedFormat.HasWholeDwordComponents(dataFormat))
{
// Dword components are dword aligned and keep their register bits.
var dwordAddress = ShiftRightLogical(byteAddress, UInt(2));
for (uint component = 0; component < componentCount; component++)
{
Gfx10UnifiedFormat.TryGetComponentLayout(dataFormat, component, out var byteOffset, out _, out _);
StoreBufferWord(
bindingIndex,
byteOffset == 0 ? dwordAddress : IAdd(dwordAddress, UInt(byteOffset / 4)),
LoadV(control.VectorData + component));
}
return;
}
var element = new (uint Value, uint Mask)[(elementBytes + 3) / 4];
for (var index = 0; index < element.Length; index++)
{
element[index] = (UInt(0), 0);
}
for (uint component = 0; component < componentCount; component++)
{
Gfx10UnifiedFormat.TryGetComponentLayout(dataFormat, component, out var byteOffset, out var bitOffset, out var bitCount);
var encoded = EncodeGfx10BufferComponent(
LoadV(control.VectorData + component),
bitCount,
numberFormat,
dataFormat);
var dword = (int)(byteOffset / 4);
var bit = ((byteOffset & 3) * 8) + bitOffset;
var (value, mask) = element[dword];
element[dword] = (
BitwiseOr(value, bit == 0 ? encoded : ShiftLeftLogical(encoded, UInt(bit))),
mask | (((1u << (int)bitCount) - 1) << (int)bit));
}
StoreBufferElementBits(bindingIndex, byteAddress, element);
});
return true;
}
// Converts one register to the bits its component stores, per the number format.
private uint EncodeGfx10BufferComponent(uint value, uint bitCount, uint numberFormat, uint dataFormat)
{
if (bitCount == 32)
{
return value;
}
var mask = (1u << (int)bitCount) - 1;
var signedMaximum = mask >> 1;
var signedMinimum = -(int)signedMaximum - 1;
switch (numberFormat)
{
case 0:
return RoundedFloatToUnsigned(value, 0f, 1f, mask);
case 1:
return BitwiseAnd(RoundedFloatToSigned(value, -1f, 1f, signedMaximum), UInt(mask));
case 2:
return RoundedFloatToUnsigned(value, 0f, mask, 1f);
case 3:
return BitwiseAnd(RoundedFloatToSigned(value, signedMinimum, signedMaximum, 1f), UInt(mask));
case 4:
return Ext(38, _uintType, value, UInt(mask));
case 5:
return BitwiseAnd(
Bitcast(
_uintType,
Ext(
45,
_intType,
Bitcast(_intType, value),
_module.Constant(_intType, unchecked((uint)signedMinimum)),
_module.Constant(_intType, signedMaximum))),
UInt(mask));
case 7:
if (dataFormat is 6 or 7)
{
return EncodeUnsignedMiniFloat(value, bitCount);
}
if (bitCount == 16)
{
var pair = _module.AddInstruction(
SpirvOp.CompositeConstruct,
_vec2Type,
Bitcast(_floatType, value),
Float(0f));
return BitwiseAnd(Ext(58, _uintType, pair), UInt(0xFFFF));
}
return BitwiseAnd(value, UInt(mask));
default:
return BitwiseAnd(value, UInt(mask));
}
}
// NaN stores as zero; the value is clamped, scaled and rounded to nearest even.
private uint ClampedScaledFloat(uint value, float minimum, float maximum, float scale)
{
var input = Bitcast(_floatType, value);
input = _module.AddInstruction(
SpirvOp.Select,
_floatType,
_module.AddInstruction(SpirvOp.IsNan, _boolType, input),
Float(0f),
input);
var clamped = Ext(43, _floatType, input, Float(minimum), Float(maximum));
if (scale != 1f)
{
clamped = _module.AddInstruction(SpirvOp.FMul, _floatType, clamped, Float(scale));
}
return Ext(2, _floatType, clamped);
}
private uint RoundedFloatToUnsigned(uint value, float minimum, float maximum, float scale) =>
_module.AddInstruction(
SpirvOp.ConvertFToU,
_uintType,
ClampedScaledFloat(value, minimum, maximum, scale));
private uint RoundedFloatToSigned(uint value, float minimum, float maximum, float scale) =>
Bitcast(
_uintType,
_module.AddInstruction(
SpirvOp.ConvertFToS,
_intType,
ClampedScaledFloat(value, minimum, maximum, scale)));
// Encodes a float as an unsigned 10 or 11 bit mini-float, rounding to nearest even.
private uint EncodeUnsignedMiniFloat(uint value, uint bitCount)
{
var mantissaBits = (int)bitCount - 5;
var shift = 23 - mantissaBits;
var mantissaMask = (1u << mantissaBits) - 1;
var input = Bitcast(_floatType, value);
var isNan = _module.AddInstruction(SpirvOp.IsNan, _boolType, input);
var positive = _module.AddInstruction(SpirvOp.FOrdGreaterThan, _boolType, input, Float(0f));
var exponent = BitwiseAnd(ShiftRightLogical(value, UInt(23)), UInt(0xFF));
// Below 2^-14 the result is a denormal: the mantissa alone scales the value.
var denormal = _module.AddInstruction(
SpirvOp.ConvertFToU,
_uintType,
Ext(
2,
_floatType,
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
input,
Float(1u << (14 + mantissaBits)))));
// Rounding the dropped mantissa bits may carry into the exponent.
var roundBias = IAdd(
UInt((1u << (shift - 1)) - 1),
BitwiseAnd(ShiftRightLogical(value, UInt((uint)shift)), UInt(1)));
var rounded = IAdd(value, roundBias);
var roundedExponent = BitwiseAnd(ShiftRightLogical(rounded, UInt(23)), UInt(0xFF));
var normal = BitwiseOr(
ShiftLeftLogical(
_module.AddInstruction(SpirvOp.ISub, _uintType, roundedExponent, UInt(112)),
UInt((uint)mantissaBits)),
BitwiseAnd(ShiftRightLogical(rounded, UInt((uint)shift)), UInt(mantissaMask)));
var overflow = _module.AddInstruction(
SpirvOp.UGreaterThanEqual,
_boolType,
roundedExponent,
UInt(143));
var result = _module.AddInstruction(
SpirvOp.Select,
_uintType,
overflow,
UInt(31u << mantissaBits),
normal);
result = _module.AddInstruction(
SpirvOp.Select,
_uintType,
_module.AddInstruction(SpirvOp.ULessThan, _boolType, exponent, UInt(113)),
denormal,
result);
result = _module.AddInstruction(SpirvOp.Select, _uintType, positive, result, UInt(0));
return _module.AddInstruction(
SpirvOp.Select,
_uintType,
isNan,
UInt((1u << (int)bitCount) - 1),
result);
}
// A descriptor whose format field is INVALID is not bound.
private uint IsDescriptorBound(uint descriptorWord3)
{
var descriptorFormat = BitwiseAnd(
ShiftRightLogical(descriptorWord3, UInt(12)),
UInt(0x7F));
return _module.AddInstruction(SpirvOp.INotEqual, _boolType, descriptorFormat, UInt(0));
}
private (uint DataFormat, uint NumberFormat) DecodeGfx10BufferFormat(
uint unifiedFormat)
{
// A specialized descriptor makes the format a translation-time constant.
if (_module.TryGetConstantValue(unifiedFormat, out var knownFormat) && knownFormat < 128)
{
Gfx10UnifiedFormat.TryDecode(knownFormat, out var knownDataFormat, out var knownNumberFormat);
return (UInt(knownDataFormat), UInt(knownNumberFormat));
}
// 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,
out uint componentInBounds)
{
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);
}
// The layout is selected at run time from the descriptor's data format.
foreach (var layout in Gfx10UnifiedFormat.ComponentLayouts)
{
if (layout.Component == (uint)component)
{
SetLayout(layout.DataFormat, layout.ByteOffset, layout.BitOffset, layout.BitCount);
}
}
var packed = LoadUnalignedBufferWord(
bindingIndex,
IAdd(elementAddress, byteOffset));
var componentBytes = ShiftRightLogical(IAdd(IAdd(bitOffset, bitCount), UInt(7)), UInt(3));
componentInBounds = _module.AddInstruction(
SpirvOp.LogicalOr, _boolType,
_module.AddInstruction(SpirvOp.IEqual, _boolType, bitCount, UInt(0)),
IsBufferElementInRange(bindingIndex, IAdd(elementAddress, byteOffset),
_module.AddInstruction(SpirvOp.ISub, _uintType, componentBytes, UInt(1))));
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);
}
// A byte or short store merges into its dword atomically, so neighbouring
// sub-word stores from other invocations keep their bytes.
private void StoreBufferBytes(
int bindingIndex,
uint byteAddress,
uint value,
uint byteCount,
uint sourceShift)
{
if (sourceShift != 0)
{
value = ShiftRightLogical(value, UInt(sourceShift));
}
StoreBufferElementBits(
bindingIndex,
byteAddress,
[(value, byteCount == 1 ? 0xFFu : 0xFFFFu)]);
}
// Stores an element of up to four dwords at any byte alignment. Each touched
// dword keeps the bits outside the element's mask.
private void StoreBufferElementBits(
int bindingIndex,
uint byteAddress,
IReadOnlyList<(uint Value, uint Mask)> element)
{
var alignment = BitwiseAnd(byteAddress, UInt(3));
var shift = ShiftLeftLogical(alignment, UInt(3));
var aligned = _module.AddInstruction(SpirvOp.IEqual, _boolType, alignment, UInt(0));
var carryShift = _module.AddInstruction(SpirvOp.ISub, _uintType, UInt(32), shift);
var firstDword = ShiftRightLogical(byteAddress, UInt(2));
for (var index = 0; index <= element.Count; index++)
{
var value = UInt(0);
var mask = UInt(0);
if (index < element.Count)
{
var (elementValue, elementMask) = element[index];
value = ShiftLeftLogical(BitwiseAnd(elementValue, UInt(elementMask)), shift);
mask = ShiftLeftLogical(UInt(elementMask), shift);
}
if (index > 0)
{
// An unaligned element carries its high bits into the next dword.
var (previousValue, previousMask) = element[index - 1];
value = BitwiseOr(
value,
_module.AddInstruction(
SpirvOp.Select,
_uintType,
aligned,
UInt(0),
ShiftRightLogical(BitwiseAnd(previousValue, UInt(previousMask)), carryShift)));
mask = BitwiseOr(
mask,
_module.AddInstruction(
SpirvOp.Select,
_uintType,
aligned,
UInt(0),
ShiftRightLogical(UInt(previousMask), carryShift)));
}
StoreBufferMaskedWord(
bindingIndex,
index == 0 ? firstDword : IAdd(firstDword, UInt((uint)index)),
value,
mask);
}
}
// Writes the masked bits of one dword: a plain store for a full mask, else an
// atomic read-modify-write that keeps the other bits.
private void StoreBufferMaskedWord(int bindingIndex, uint dwordAddress, uint value, uint mask)
{
var touched = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
_module.AddInstruction(SpirvOp.INotEqual, _boolType, mask, UInt(0)),
IsBufferWordInRange(bindingIndex, dwordAddress));
EmitConditional(touched, () =>
{
var pointer = BufferWordPointer(bindingIndex, dwordAddress);
var full = _module.AddInstruction(SpirvOp.IEqual, _boolType, mask, UInt(uint.MaxValue));
EmitConditional(
full,
() => Store(pointer, value),
() => EmitAtomicWordUpdate(
pointer,
observed => BitwiseOr(
BitwiseAnd(observed, _module.AddInstruction(SpirvOp.Not, _uintType, mask)),
value)));
});
}
// Compare-exchange loop: the merge runs on the observed word until the
// exchange succeeds. Returns the observed (pre-exchange) word at the
// point the loop exits, which is a valid use here because it is
// defined by the OpPhi in `header`, and `header` dominates `mergeLabel`.
private uint EmitAtomicWordUpdate(uint pointer, Func<uint, uint> merge)
{
var preheader = _module.AllocateId();
var header = _module.AllocateId();
var continueLabel = _module.AllocateId();
var mergeLabel = _module.AllocateId();
var exchanged = _module.AllocateId();
_module.AddStatement(SpirvOp.Branch, preheader);
_module.AddLabel(preheader);
var initial = _module.AddInstruction(SpirvOp.AtomicLoad, _uintType, pointer, UInt(1), UInt(0));
_module.AddStatement(SpirvOp.Branch, header);
_module.AddLabel(header);
var observed = _module.AddInstruction(
SpirvOp.Phi,
_uintType,
initial,
preheader,
exchanged,
continueLabel);
var next = merge(observed);
// The phi above names this result before it is emitted.
_module.AddStatement(
SpirvOp.AtomicCompareExchange,
_uintType,
exchanged,
pointer,
UInt(1),
UInt(0),
UInt(0),
next,
observed);
var success = _module.AddInstruction(SpirvOp.IEqual, _boolType, exchanged, observed);
_module.AddStatement(SpirvOp.LoopMerge, mergeLabel, continueLabel, 0);
_module.AddStatement(SpirvOp.BranchConditional, success, mergeLabel, continueLabel);
_module.AddLabel(continueLabel);
_module.AddStatement(SpirvOp.Branch, header);
_module.AddLabel(mergeLabel);
return observed;
}
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) ||
opcode == "ImageGetLod";
private bool TryEmitVertexInputFetch(
Gen5BufferMemoryControl control,
SpirvVertexInput input,
out string error)
{
error = string.Empty;
if (control.DwordCount == 0)
{
error =
$"invalid vertex input fetch components={control.DwordCount}";
return false;
}
var loaded = Load(input.Type, input.Variable);
for (uint component = 0; component < control.DwordCount; component++)
{
uint raw;
var selector = (input.DestinationSelect >> (int)(component * 3)) & 0x7u;
if (selector == 0)
{
raw = UInt(0);
}
else if (selector == 1)
{
raw = UInt(input.NumberFormat is 4u or 5u ? 1u : 0x3F80_0000u);
}
else if (selector is >= 4u and <= 7u)
{
var sourceComponent = selector - 4u;
if (sourceComponent >= input.ComponentCount)
{
error =
$"vertex input destination selector exceeds source components: selector={selector} components={input.ComponentCount}";
return false;
}
var value = input.ComponentCount == 1
? loaded
: _module.AddInstruction(
SpirvOp.CompositeExtract,
input.ComponentType,
loaded,
sourceComponent);
raw = input.ComponentKind == VertexInputComponentKind.Uint
? value
: Bitcast(_uintType, value);
}
else
{
error = $"unsupported vertex input destination selector={selector}";
return false;
}
StoreV(control.VectorData + component, raw);
}
return true;
}
private bool TryEmitImage(
Gen5ShaderInstruction instruction,
Gen5ImageControl image,
out string error)
{
error = string.Empty;
SpirvImageResource resource;
uint imageObject;
uint dstSelect;
uint mipLevel;
{
if (TryGetImageElementCases(instruction, image, out var selector, out var elements, out error))
{
// One case per descriptor over a constant element, like a switch on the selector.
var emitted = true;
var caseError = string.Empty;
for (var index = 0; index < elements.Count && emitted; index++)
{
var elementCase = elements[index];
EmitConditional(_module.AddInstruction(SpirvOp.IEqual, _boolType, selector, UInt((uint)index)), () =>
{
if (!TryResolveLayoutImage(instruction, image, out var caseResource, out var caseImageObject, out var caseDstSelect, out caseError,
elementCase))
{
emitted = false;
return;
}
// A sampled mip load keeps its mip operand; a storage case is already its own mip view.
var caseMipLevel = instruction.Opcode == "ImageLoadMip" && !caseResource.IsStorage
? LoadImageIntegerAddress(image, (int)ImageCoordinateComponentCount(caseResource))
: UInt(0);
if (!EmitImageOperation(instruction, image, caseResource, caseImageObject, caseDstSelect, caseMipLevel, out caseError))
{
emitted = false;
}
});
}
error = caseError;
return emitted;
}
if (error.Length != 0)
{
return false;
}
if (!TryResolveLayoutImage(instruction, image, out resource, out imageObject, out dstSelect, out error))
{
return false;
}
// A sampled mip load reads its level from the address operand after the coordinates.
mipLevel = instruction.Opcode == "ImageLoadMip"
? LoadImageIntegerAddress(image, (int)ImageCoordinateComponentCount(resource))
: UInt(0);
}
return EmitImageOperation(instruction, image, resource, imageObject, dstSelect, mipLevel, out error);
}
// One image operation over a resolved image object; its results are register writes.
private bool EmitImageOperation(
Gen5ShaderInstruction instruction,
Gen5ImageControl image,
SpirvImageResource resource,
uint imageObject,
uint dstSelect,
uint mipLevel,
out string error)
{
error = string.Empty;
if (instruction.Opcode == "ImageGetResinfo")
{
var sizeComponentCount = ImageCoordinateComponentCount(resource);
var queryImage = imageObject;
var size = _module.AddInstruction(
resource.IsStorage || resource.Multisampled
? SpirvOp.ImageQuerySize
: SpirvOp.ImageQuerySizeLod,
ImageIntegerCoordinateType(sizeComponentCount),
resource.IsStorage || resource.Multisampled
? [queryImage]
: [queryImage, LoadImageIntegerAddress(image, 0)]);
// NVIDIA's compiler crashes on an unused level query of a 3D image.
var levels = (image.Dmask & 0x8u) != 0 && !resource.IsStorage && !resource.Multisampled
? _module.AddInstruction(
SpirvOp.ImageQueryLevels,
_uintType,
queryImage)
: UInt(1);
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 = sizeComponentCount == 1
? size
: _module.AddInstruction(
SpirvOp.CompositeExtract,
_intType,
size,
component);
value = Bitcast(_uintType, signedValue);
}
else if (component == 3)
{
value = levels;
}
else
{
value = UInt(0);
}
StoreV(image.VectorData + outputIndex++, value);
}
return true;
}
if (instruction.Opcode == "ImageGetLod")
{
var coordinateComponentCount = ImageCoordinateComponentCount(resource);
var coordinates = BuildFloatCoordinates(
image,
0,
coordinateComponentCount,
resource);
var queried = _module.AddInstruction(
SpirvOp.ImageQueryLod,
_vec2Type,
imageObject,
coordinates);
uint outputIndex = 0;
var mask = image.Dmask != 0 ? image.Dmask : 1u;
for (uint component = 0; component < 2; component++)
{
if ((mask & (1u << (int)component)) == 0)
{
continue;
}
var value = _module.AddInstruction(
SpirvOp.CompositeExtract,
_floatType,
queried,
component);
StoreV(image.VectorData + outputIndex++, Bitcast(_uintType, 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];
for (var component = 0; component < components.Length; component++)
{
var sourceIndex = Gen5ShaderTranslator.GetImageStoreSourceIndex(
dstSelect,
image.Dmask,
component);
if (sourceIndex >= 0)
{
var raw = LoadImageStoreComponent(
image,
resource,
(uint)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);
texel = PackImageTexel(resource, texel);
EmitExecConditional(() =>
_module.AddStatement(
SpirvOp.ImageWrite,
imageObject,
coordinates,
texel));
return true;
}
if (instruction.Opcode.StartsWith("ImageAtomic", StringComparison.Ordinal))
{
if (!resource.IsStorage)
{
error = "image atomic is not bound as storage";
return false;
}
// IMAGE_ATOMIC_FMIN/FMAX target float-format storage images and
// have no native SPIR-V storage-image atomic without pulling in
// SPV_EXT_shader_atomic_float_min_max. Lower them as a
// compare-and-swap loop on the underlying bit pattern instead
// (same technique used by other float-atomic emulations that
// avoid that extension dependency).
if (instruction.Opcode is "ImageAtomicFmax" or "ImageAtomicFmin")
{
var isMax = instruction.Opcode == "ImageAtomicFmax";
var floatCoordinateCount = ImageCoordinateComponentCount(resource);
var floatCoordinates = BuildIntegerCoordinates(
image,
0,
floatCoordinateCount);
EmitExecConditional(() =>
{
var pointer = _module.AddInstruction(
SpirvOp.ImageTexelPointer,
_module.TypePointer(SpirvStorageClass.Image, _uintType),
resource.Variable,
floatCoordinates,
UInt(0));
var srcBits = Bitcast(_uintType, LoadV(image.VectorData));
var old = EmitAtomicWordUpdate(pointer, observed =>
{
var oldFloat = Bitcast(_floatType, observed);
var srcFloat = Bitcast(_floatType, srcBits);
var pickSrc = _module.AddInstruction(
isMax ? SpirvOp.FOrdLessThan : SpirvOp.FOrdGreaterThan,
_boolType,
oldFloat,
srcFloat);
var chosen = _module.AddInstruction(
SpirvOp.Select, _floatType, pickSrc, srcFloat, oldFloat);
return Bitcast(_uintType, chosen);
});
if (image.Glc)
{
StoreV(image.VectorData, old);
}
});
return true;
}
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 coordinates = BuildIntegerCoordinates(
image,
0,
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 coordinates = BuildIntegerCoordinates(
image,
0,
coordinateComponentCount);
sampled = _module.AddInstruction(
SpirvOp.ImageRead,
resource.VectorType,
imageObject,
coordinates);
}
else
{
// A sampled image is fetched through its image type; a request has no sampler here.
var fetchedImage = imageObject;
var coordinateComponentCount =
ImageCoordinateComponentCount(resource);
var coordinates = BuildIntegerCoordinates(
image,
0,
coordinateComponentCount);
if (resource.Multisampled)
{
var sample = LoadImageIntegerAddress(
image,
(int)coordinateComponentCount);
sampled = _module.AddInstruction(
SpirvOp.ImageFetch,
resource.VectorType,
fetchedImage,
coordinates,
0x40,
sample);
}
else
{
sampled = _module.AddInstruction(
SpirvOp.ImageFetch,
resource.VectorType,
fetchedImage,
coordinates,
2,
mipLevel);
}
}
sampled = UnpackImageTexel(resource, sampled);
}
else if (instruction.Opcode.StartsWith(
"ImageSample",
StringComparison.Ordinal))
{
var hasOffset =
instruction.Opcode.EndsWith("O", StringComparison.Ordinal);
var hasCompare =
instruction.Opcode.Contains("SampleC", StringComparison.Ordinal) &&
!instruction.Opcode.StartsWith("ImageSampleCd", StringComparison.Ordinal);
var hasGradients =
instruction.Opcode.Contains("SampleD", StringComparison.Ordinal) ||
instruction.Opcode.Contains("SampleCd", StringComparison.Ordinal) ||
instruction.Opcode.Contains("SampleCCd", 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);
if (hasCompare && resource.ConversionFormat != GuestImageFormat.Invalid)
{
error = "image sample uses depth comparison with a packed integer image";
return false;
}
// 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,
resource)
: 0u;
var gradientY = hasGradients
? BuildFloatCoordinates(
image,
addressCursor + (int)spatialComponentCount,
spatialComponentCount,
resource)
: 0u;
if (hasGradients)
{
addressCursor += checked((int)(spatialComponentCount * 2));
}
var coordinates = BuildFloatCoordinates(
image,
addressCursor,
coordinateComponentCount,
resource);
// Non-pixel samples require explicit derivatives or a level of detail.
// Use level zero when the instruction supplies neither.
var explicitLod = hasGradients || hasZeroLod || hasLod ||
_stage != Gen5SpirvStage.Pixel;
var lod = hasZeroLod
? Float(0)
: hasLod
? LoadImageFloatAddress(
image,
addressCursor + (int)coordinateComponentCount)
: explicitLod
? Float(0)
: 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<uint>
{
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);
}
}
if (hasCompare && resource.EmulatedCompareFunction >= 0)
{
// A color format cannot back a Vulkan depth-compare view; compare
// the sampled first channel like RDNA does for such formats.
var texel = _module.AddInstruction(
explicitLod ? SpirvOp.ImageSampleExplicitLod : SpirvOp.ImageSampleImplicitLod,
resource.VectorType,
[.. operands]);
var depth = EmulatedDepthCompare(
reference,
_module.AddInstruction(SpirvOp.CompositeExtract, _floatType, texel, 0u),
resource.EmulatedCompareFunction);
sampled = _module.AddInstruction(
SpirvOp.CompositeConstruct,
resource.VectorType,
depth,
depth,
depth,
Float(1f));
}
else if (hasCompare)
{
// The sampler carries the compare; the depth result fills x, y, z.
var drefOperands = new List<uint> { imageObject, coordinates, reference };
drefOperands.AddRange(operands.Skip(2));
var depth = _module.AddInstruction(
explicitLod ? SpirvOp.ImageSampleDrefExplicitLod : SpirvOp.ImageSampleDrefImplicitLod,
_floatType,
[.. drefOperands]);
sampled = _module.AddInstruction(
SpirvOp.CompositeConstruct,
resource.VectorType,
depth,
depth,
depth,
Float(1f));
}
else
{
sampled = _module.AddInstruction(
explicitLod
? SpirvOp.ImageSampleExplicitLod
: SpirvOp.ImageSampleImplicitLod,
resource.VectorType,
[.. operands]);
sampled = UnpackImageTexel(resource, sampled);
}
}
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 gatherHorizontal =
instruction.Opcode == "ImageGather4H";
if (hasCompare && resource.ConversionFormat != GuestImageFormat.Invalid)
{
error = "image gather uses depth comparison with a packed integer image";
return false;
}
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,
resource);
if (resource.Dimension == SpirvImageDim.Dim1D)
{
var levelZero =
instruction.Opcode.Contains("Lz", StringComparison.Ordinal);
if (resource.Arrayed ||
hasCompare ||
hasOffset ||
gatherHorizontal ||
!levelZero)
{
error = resource.Arrayed
? "unsupported 1D-array image gather"
: "unsupported 1D image gather variant";
return false;
}
sampled = EmitOneDimensionalGatherLz(
image,
resource,
imageObject,
coordinates);
sampled = UnpackImageGather(resource, image.Dmask, sampled);
writeAllComponents = true;
goto GatherComplete;
}
var operands = new List<uint>
{
imageObject,
coordinates,
};
var emulatedCompare = hasCompare && resource.EmulatedCompareFunction >= 0;
if (emulatedCompare)
{
// Gather the first channel and compare each texel in the shader.
operands.Add(UInt(0));
}
else if (hasCompare)
{
operands.Add(reference);
}
else
{
uint component = 0;
if (resource.ConversionFormat == GuestImageFormat.Invalid)
{
while (component < 3 &&
(image.Dmask & (1u << (int)component)) == 0)
{
component++;
}
}
operands.Add(UInt(component));
}
if (hasOffset)
{
operands.Add(0x10u);
operands.Add(offset);
}
else if (gatherHorizontal)
{
if (resource.Dimension == SpirvImageDim.Dim1D)
{
error = "unsupported 1D horizontal image gather";
return false;
}
operands.Add(0x20u);
operands.Add(BuildHorizontalGatherOffsets());
}
sampled = _module.AddInstruction(
hasCompare && !emulatedCompare ? SpirvOp.ImageDrefGather : SpirvOp.ImageGather,
resource.VectorType,
[.. operands]);
if (emulatedCompare)
{
var gathered = sampled;
var compared = new uint[4];
for (var texel = 0u; texel < 4; texel++)
{
compared[texel] = EmulatedDepthCompare(
reference,
_module.AddInstruction(SpirvOp.CompositeExtract, _floatType, gathered, texel),
resource.EmulatedCompareFunction);
}
sampled = _module.AddInstruction(SpirvOp.CompositeConstruct, resource.VectorType, compared);
}
else if (!hasCompare)
{
sampled = UnpackImageGather(resource, image.Dmask, sampled);
}
writeAllComponents = true;
GatherComplete:;
}
else
{
error = $"unsupported image opcode {instruction.Opcode}";
return false;
}
var outputValues = new List<uint>(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 EmitOneDimensionalGatherLz(
Gen5ImageControl image,
SpirvImageResource resource,
uint sampledImage,
uint coordinate)
{
var imageValue = Load(resource.ImageType, resource.Variable);
var width = _module.AddInstruction(
SpirvOp.ImageQuerySizeLod,
_intType,
imageValue,
_module.Constant(_intType, 0));
var widthFloat = _module.AddInstruction(
SpirvOp.ConvertSToF,
_floatType,
width);
var centered = _module.AddInstruction(
SpirvOp.FSub,
_floatType,
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
coordinate,
widthFloat),
Float(0.5f));
var left = Ext(8, _floatType, centered);
uint component = 0;
if (resource.ConversionFormat == GuestImageFormat.Invalid)
{
while (component < 3 &&
(image.Dmask & (1u << (int)component)) == 0)
{
component++;
}
}
var values = new uint[2];
for (var index = 0; index < values.Length; index++)
{
var sampleCoordinate = _module.AddInstruction(
SpirvOp.FDiv,
_floatType,
_module.AddInstruction(
SpirvOp.FAdd,
_floatType,
left,
Float(index == 0 ? 0.5f : 1.5f)),
widthFloat);
var texel = _module.AddInstruction(
SpirvOp.ImageSampleExplicitLod,
resource.VectorType,
sampledImage,
sampleCoordinate,
2u,
Float(0));
values[index] = _module.AddInstruction(
SpirvOp.CompositeExtract,
resource.ComponentType,
texel,
component);
}
return _module.AddInstruction(
SpirvOp.CompositeConstruct,
resource.VectorType,
values[0],
values[1],
values[1],
values[0]);
}
private static bool TryGetPackedImageConversion(
SpirvImageResource resource,
out int componentCount,
out ReadOnlySpan<uint> componentBits,
out ReadOnlySpan<uint> componentOffsets)
{
if (resource.ConversionFormat == GuestImageFormat.Format11x2x10Uint)
{
componentCount = 3;
componentBits = [11u, 11u, 10u];
componentOffsets = [0u, 11u, 22u];
return true;
}
componentCount = 0;
componentBits = default;
componentOffsets = default;
return false;
}
private uint UnpackImageTexel(
SpirvImageResource resource,
uint texel)
{
if (resource.ConversionFormat is GuestImageFormat.Format8Uscaled or GuestImageFormat.Format8x2Uscaled)
{
var scaled = _module.AddInstruction(SpirvOp.FMul, resource.VectorType, texel,
_module.AddInstruction(SpirvOp.CompositeConstruct, resource.VectorType,
Float(255), Float(255), Float(1), Float(1)));
var channels = new uint[4];
for (var component = 0; component < channels.Length; component++)
{
var selector = (resource.ShaderSwizzle >> (component * 3)) & 7u;
channels[component] = selector switch
{
1u => Float(1),
>= 4u => _module.AddInstruction(SpirvOp.CompositeExtract, _floatType, scaled, selector - 4u),
_ => Float(0),
};
}
return _module.AddInstruction(SpirvOp.CompositeConstruct, resource.VectorType, channels);
}
if (!TryGetPackedImageConversion(
resource,
out var componentCount,
out var componentBits,
out var componentOffsets))
{
return texel;
}
var packed = _module.AddInstruction(
SpirvOp.CompositeExtract,
_uintType,
texel,
0u);
var components = new uint[4];
for (var component = 0; component < componentCount; component++)
{
components[component] = _module.AddInstruction(
SpirvOp.BitFieldUExtract,
_uintType,
packed,
UInt(componentOffsets[component]),
UInt(componentBits[component]));
}
for (var component = componentCount; component < components.Length; component++)
{
components[component] = components[component % componentCount];
}
var selected = new uint[4];
for (var component = 0; component < selected.Length; component++)
{
var selector = (resource.ShaderSwizzle >> (component * 3)) & 7u;
selected[component] = selector switch
{
1u => UInt(1),
>= 4u => components[(selector - 4u) % (uint)componentCount],
_ => UInt(0),
};
}
return _module.AddInstruction(
SpirvOp.CompositeConstruct,
resource.VectorType,
selected);
}
private uint UnpackImageGather(
SpirvImageResource resource,
uint dmask,
uint gathered)
{
if (!TryGetPackedImageConversion(
resource,
out var componentCount,
out var componentBits,
out var componentOffsets))
{
return gathered;
}
uint component = 0;
while (component < 3 && (dmask & (1u << (int)component)) == 0)
{
component++;
}
var selector = (resource.ShaderSwizzle >> ((int)component * 3)) & 7u;
if (selector < 4u)
{
var value = selector == 1u ? UInt(1) : UInt(0);
return _module.AddInstruction(
SpirvOp.CompositeConstruct,
resource.VectorType,
value,
value,
value,
value);
}
var physical = (selector - 4u) % (uint)componentCount;
var values = new uint[4];
for (var lane = 0; lane < values.Length; lane++)
{
var packed = _module.AddInstruction(
SpirvOp.CompositeExtract,
_uintType,
gathered,
(uint)lane);
values[lane] = _module.AddInstruction(
SpirvOp.BitFieldUExtract,
_uintType,
packed,
UInt(componentOffsets[(int)physical]),
UInt(componentBits[(int)physical]));
}
return _module.AddInstruction(
SpirvOp.CompositeConstruct,
resource.VectorType,
values);
}
private uint PackImageTexel(
SpirvImageResource resource,
uint texel)
{
if (!TryGetPackedImageConversion(
resource,
out var componentCount,
out var componentBits,
out var componentOffsets))
{
return texel;
}
var packed = UInt(0);
for (var component = 0; component < componentCount; component++)
{
var value = _module.AddInstruction(
SpirvOp.CompositeExtract,
_uintType,
texel,
(uint)component);
var maximum = (1u << (int)componentBits[component]) - 1u;
var within = _module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
value,
UInt(maximum));
var clamped = _module.AddInstruction(
SpirvOp.Select,
_uintType,
within,
value,
UInt(maximum));
var shifted = componentOffsets[component] == 0
? clamped
: ShiftLeftLogical(clamped, UInt(componentOffsets[component]));
packed = BitwiseOr(packed, shifted);
}
return _module.AddInstruction(
SpirvOp.CompositeConstruct,
resource.VectorType,
packed,
UInt(0),
UInt(0),
UInt(0));
}
private uint BuildHorizontalGatherOffsets()
{
var vec2Int = _module.TypeVector(_intType, 2);
var offsets = new uint[4];
for (var index = 0; index < offsets.Length; index++)
{
offsets[index] = _module.ConstantComposite(
vec2Int,
_module.Constant(_intType, unchecked((uint)(index - 1))),
_module.Constant(_intType, 0));
}
return _module.ConstantComposite(
_module.TypeArray(vec2Int, 4),
offsets);
}
private static uint ImageSpatialComponentCount(
SpirvImageResource resource) =>
ImageSpatialComponentCountOf(resource.Dimension);
private static uint ImageCoordinateComponentCount(
SpirvImageResource resource) =>
ImageSpatialComponentCount(resource) + (resource.Arrayed ? 1u : 0u);
private uint ImageIntegerCoordinateType(uint componentCount) =>
componentCount == 1
? _intType
: _module.TypeVector(_intType, componentCount);
private uint IntegerTypeForComponents(uint componentCount) =>
componentCount == 1 ? _intType : _module.TypeVector(_intType, componentCount);
private uint FloatTypeForComponents(uint componentCount) =>
componentCount == 1 ? _floatType : _module.TypeVector(_floatType, componentCount);
private uint BuildFloatCoordinates(
Gen5ImageControl image,
int start,
uint componentCount,
SpirvImageResource resource)
{
var components = new uint[checked((int)componentCount)];
for (var component = 0; component < components.Length; component++)
{
components[component] = LoadImageFloatAddress(
image,
start + component);
}
if (resource.Cube && components.Length >= 2)
{
components[0] = _module.AddInstruction(SpirvOp.FSub, _floatType, components[0], Float(1));
components[1] = _module.AddInstruction(SpirvOp.FSub, _floatType, components[1], Float(1));
if (components.Length >= 3)
{
var guestLayer = _module.AddInstruction(SpirvOp.ConvertFToU, _uintType, components[2]);
var padding = ShiftLeftLogical(ShiftRightLogical(guestLayer, UInt(3)), UInt(1));
var hostLayer = _module.AddInstruction(SpirvOp.ISub, _uintType, guestLayer, padding);
components[2] = _module.AddInstruction(SpirvOp.ConvertUToF, _floatType, hostLayer);
}
}
if (componentCount == 1)
{
return components[0];
}
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));
}
if (componentCount == 1)
{
return components[0];
}
return _module.AddInstruction(
SpirvOp.CompositeConstruct,
ImageIntegerCoordinateType(componentCount),
components);
}
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));
}
if (componentCount == 1)
{
return components[0];
}
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 =
spatialComponentCount == 1
? _intType
: _module.TypeVector(_intType, spatialComponentCount);
var spatialFloatType =
spatialComponentCount == 1
? _floatType
: _module.TypeVector(_floatType, spatialComponentCount);
var queryComponentCount = resource.Arrayed
? coordinateComponentCount
: spatialComponentCount;
var queryIntegerType =
queryComponentCount == 1
? _intType
: _module.TypeVector(_intType, queryComponentCount);
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,
queryIntegerType,
image,
clampedLod);
if (resource.Arrayed)
{
if (spatialComponentCount == 1)
{
size = _module.AddInstruction(
SpirvOp.CompositeExtract,
_intType,
size,
0u);
}
else
{
var spatialSizeComponents =
new uint[checked((int)spatialComponentCount)];
for (uint component = 0;
component < spatialComponentCount;
component++)
{
spatialSizeComponents[component] = component;
}
size = _module.AddInstruction(
SpirvOp.VectorShuffle,
spatialIntegerType,
[size, size, .. spatialSizeComponents]);
}
}
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 arrayOffsetComponents =
new uint[checked((int)coordinateComponentCount)];
for (uint component = 0;
component < spatialComponentCount;
component++)
{
arrayOffsetComponents[component] =
spatialComponentCount == 1
? normalizedOffset
: _module.AddInstruction(
SpirvOp.CompositeExtract,
_floatType,
normalizedOffset,
component);
}
arrayOffsetComponents[coordinateComponentCount - 1] = Float(0);
var arrayOffset = _module.AddInstruction(
SpirvOp.CompositeConstruct,
_module.TypeVector(_floatType, coordinateComponentCount),
arrayOffsetComponents);
return _module.AddInstruction(
SpirvOp.FAdd,
_module.TypeVector(_floatType, coordinateComponentCount),
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)
{
// RDNA2 EXP.VM communicates the current EXEC mask even for a
// NULL export or an MRT not bound by this host pass. Record it
// before resolving the data target; the final VM export wins.
if (export.ValidMask && _pixelValidMaskActive != 0)
{
Store(_pixelValidMaskActive, Load(_boolType, _exec));
}
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)
{
var outputComponent = (uint)component;
if (!output.ComponentMapping.IsIdentity)
{
for (var physicalComponent = 0u; physicalComponent < 4; physicalComponent++)
{
if (output.ComponentMapping.Map(physicalComponent) == component)
{
outputComponent = physicalComponent;
break;
}
}
}
values[component] = _module.AddInstruction(
SpirvOp.CompositeExtract,
output.Kind switch
{
Gen5PixelOutputKind.Uint => _uintType,
Gen5PixelOutputKind.Sint => _intType,
_ => _floatType,
},
Load(output.Type, output.Variable),
outputComponent);
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 (output.ComponentMapping != Gen5ColorComponentMapping.Identity)
{
vector = _module.AddInstruction(
SpirvOp.VectorShuffle,
output.Type,
vector,
vector,
output.ComponentMapping.Map(0),
output.ComponentMapping.Map(1),
output.ComponentMapping.Map(2),
output.ComponentMapping.Map(3));
}
if (Environment.GetEnvironmentVariable(
"SHARPEMU_FORCE_TITLE_EXPORT_EXEC") == "1" &&
_request.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)
{
EmitAuxPositionExport(instruction, export);
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 (export.Target == 12 && _request.ClipSpace.Enabled)
{
outputValue = ConvertPositionToClipSpace(outputValue);
}
if (_request.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);
if (export.Target == 12 && _invalidPositionClipDistance != uint.MaxValue)
{
var equal = _module.AddInstruction(
SpirvOp.FOrdEqual,
_module.TypeVector(_boolType, 4),
outputValue,
_module.ConstantNull(_vec4Type));
var invalid = _module.AddInstruction(SpirvOp.All, _boolType, equal);
// A zero position has an undefined perspective divide. Collapse its primitive
// to the remaining edge, as the guest's clipping-error cull does.
var distance = _module.AddInstruction(
SpirvOp.Select, _floatType, invalid, Float(-1f), Float(0f));
StoreDistanceConditional(_clipDistanceOutput, _invalidPositionClipDistance, distance);
}
return true;
}
private readonly record struct PositionExportComponent(
uint ClipDistance,
uint CullDistance,
bool PointSize,
bool Layer,
bool Viewport);
private static PositionExportComponent DecodePositionExportComponent(
uint control,
uint positionIndex,
uint component)
{
if (positionIndex == 0 || component >= 4)
{
return new(uint.MaxValue, uint.MaxValue, false, false, false);
}
var slot = positionIndex - 1;
var vector = 3u;
for (var index = 0u; index < 3; index++)
{
if ((control & (1u << (int)(21 + index))) == 0)
{
continue;
}
if (slot == 0)
{
vector = index;
break;
}
slot--;
}
if (vector == 3)
{
return new(uint.MaxValue, uint.MaxValue, false, false, false);
}
if (vector == 0)
{
return new(
uint.MaxValue,
uint.MaxValue,
component == 0 && (control & (1u << 16)) != 0,
component == 2 && (control & (1u << 18)) != 0,
component == 2 && (control & (1u << 19)) != 0);
}
var scalar = (vector - 1) * 4 + component;
var lower = scalar == 0 ? 0u : (1u << (int)scalar) - 1u;
var clip = control & 0xffu;
var cull = (control >> 8) & 0xffu;
return new(
(clip & (1u << (int)scalar)) != 0
? (uint)System.Numerics.BitOperations.PopCount(clip & lower)
: uint.MaxValue,
(cull & (1u << (int)scalar)) != 0
? (uint)System.Numerics.BitOperations.PopCount(cull & lower)
: uint.MaxValue,
false,
false,
false);
}
private void DeclareAuxPositionOutputs()
{
var needPointSize = false;
var needLayer = false;
var needViewport = false;
var clipCount = 0u;
var cullCount = 0u;
foreach (var export in _request.Program.Instructions
.Select(static instruction => instruction.Control)
.OfType<Gen5ExportControl>()
.Where(static export => export.Target is >= 13 and < 16))
{
var positionIndex = export.Target - 12;
for (var component = 0u; component < 4; component++)
{
if ((export.EnableMask & (1u << (int)component)) == 0)
{
continue;
}
var output = DecodePositionExportComponent(
_request.PositionExportControl,
positionIndex,
component);
needPointSize |= output.PointSize;
needLayer |= output.Layer;
needViewport |= output.Viewport;
if (output.ClipDistance != uint.MaxValue)
{
clipCount = Math.Max(clipCount, output.ClipDistance + 1);
}
if (output.CullDistance != uint.MaxValue)
{
cullCount = Math.Max(cullCount, output.CullDistance + 1);
}
}
}
if (_request.SupportsClipDistance && clipCount + cullCount < 8 &&
_request.Program.Instructions.Any(static instruction =>
instruction.Control is Gen5ExportControl { Target: 12, EnableMask: not 0 }))
{
// Use a separate plane so auxiliary position exports keep their own distances.
_invalidPositionClipDistance = clipCount++;
}
if (needPointSize)
{
_pointSizeOutput = DeclareBuiltInOutput(
_floatType,
SpirvBuiltIn.PointSize,
"gl_PointSize");
}
if (needLayer || needViewport)
{
_module.AddExtension("SPV_EXT_shader_viewport_index_layer");
_module.AddCapability(SpirvCapability.ShaderViewportIndexLayerExt);
}
if (needLayer)
{
_layerOutput = DeclareBuiltInOutput(
_uintType,
SpirvBuiltIn.Layer,
"gl_Layer");
}
if (needViewport)
{
_viewportIndexOutput = DeclareBuiltInOutput(
_uintType,
SpirvBuiltIn.ViewportIndex,
"gl_ViewportIndex");
}
if (clipCount != 0)
{
_module.AddCapability(SpirvCapability.ClipDistance);
_clipDistanceCount = clipCount;
_clipDistanceOutput = DeclareBuiltInOutput(
_module.TypeArray(_floatType, clipCount),
SpirvBuiltIn.ClipDistance,
"gl_ClipDistance");
}
if (cullCount != 0)
{
_module.AddCapability(SpirvCapability.CullDistance);
_cullDistanceCount = cullCount;
_cullDistanceOutput = DeclareBuiltInOutput(
_module.TypeArray(_floatType, cullCount),
SpirvBuiltIn.CullDistance,
"gl_CullDistance");
}
}
private uint DeclareBuiltInOutput(
uint type,
SpirvBuiltIn builtIn,
string name)
{
var pointer = _module.TypePointer(SpirvStorageClass.Output, type);
var variable = _module.AddGlobalVariable(pointer, SpirvStorageClass.Output);
_module.AddName(variable, name);
_module.AddDecoration(
variable,
SpirvDecoration.BuiltIn,
(uint)builtIn);
_interfaces.Add(variable);
return variable;
}
private void InitializeDistanceOutput(uint variable, uint count)
{
if (variable == 0)
{
return;
}
var pointerType = _module.TypePointer(SpirvStorageClass.Output, _floatType);
for (var index = 0u; index < count; index++)
{
var pointer = _module.AddInstruction(
SpirvOp.AccessChain,
pointerType,
variable,
UInt(index));
Store(pointer, Float(0f));
}
}
private void EmitAuxPositionExport(
Gen5ShaderInstruction instruction,
Gen5ExportControl export)
{
var positionIndex = export.Target - 12;
for (var component = 0u; component < 4; component++)
{
if ((export.EnableMask & (1u << (int)component)) == 0)
{
continue;
}
var output = DecodePositionExportComponent(
_request.PositionExportControl,
positionIndex,
component);
if (!output.PointSize &&
!output.Layer &&
!output.Viewport &&
output.ClipDistance == uint.MaxValue &&
output.CullDistance == uint.MaxValue)
{
continue;
}
var raw = export.Compressed
? Bitcast(
_uintType,
LoadCompressedExportComponent(instruction, (int)component))
: LoadV(instruction.Sources[(int)component].Value);
if (output.Layer && _layerOutput != 0)
{
StoreConditional(
_layerOutput,
BitwiseAnd(raw, UInt(0x7ff)),
_uintType);
}
if (output.Viewport && _viewportIndexOutput != 0)
{
var viewport = _module.AddInstruction(
SpirvOp.BitFieldUExtract,
_uintType,
raw,
UInt(16),
UInt(4));
StoreConditional(_viewportIndexOutput, viewport, _uintType);
}
var value = Bitcast(_floatType, raw);
if (output.PointSize && _pointSizeOutput != 0)
{
StoreConditional(_pointSizeOutput, value, _floatType);
}
StoreDistanceConditional(
_clipDistanceOutput,
output.ClipDistance,
value);
StoreDistanceConditional(
_cullDistanceOutput,
output.CullDistance,
value);
}
}
private void StoreDistanceConditional(
uint variable,
uint index,
uint value)
{
if (variable == 0 || index == uint.MaxValue)
{
return;
}
var pointer = _module.AddInstruction(
SpirvOp.AccessChain,
_module.TypePointer(SpirvStorageClass.Output, _floatType),
variable,
UInt(index));
StoreConditional(pointer, value, _floatType);
}
private void StoreConditional(uint variable, uint value, uint type)
{
var selected = _module.AddInstruction(
SpirvOp.Select,
type,
Load(_boolType, _exec),
value,
Load(type, variable));
Store(variable, selected);
}
private uint ConvertPositionToClipSpace(uint position)
{
var transform = _request.ClipSpace;
var components = new uint[4];
for (var component = 0u; component < 4; component++)
{
components[component] = _module.AddInstruction(
SpirvOp.CompositeExtract,
_floatType,
position,
component);
}
components[0] = ConvertClipCoordinate(
components[0],
transform.ScaleX,
transform.OffsetX,
transform.HalfExtentX);
components[1] = ConvertClipCoordinate(
components[1],
transform.ScaleY,
transform.OffsetY,
transform.HalfExtentY);
return _module.AddInstruction(
SpirvOp.CompositeConstruct,
_vec4Type,
components);
}
private uint ConvertClipCoordinate(
uint coordinate,
float scale,
float offset,
float halfExtent)
{
var window = _module.AddInstruction(
SpirvOp.FMul,
_floatType,
coordinate,
Float(scale));
var biased = _module.AddInstruction(
SpirvOp.FAdd,
_floatType,
window,
Float(offset));
var divided = _module.AddInstruction(
SpirvOp.FDiv,
_floatType,
biased,
Float(halfExtent));
return _module.AddInstruction(
SpirvOp.FSub,
_floatType,
divided,
Float(1f));
}
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) &&
_request.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) &&
_request.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) &&
_request.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) &&
_request.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" &&
TraceShaderAddressMatches("SHARPEMU_TRACE_PACKED_EXPORT_ADDRESS");
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 _request.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 = _request.Program.Instructions.Count - 1; index >= 0; index--)
{
var candidate = _request.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" &&
_request.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 bool TraceShaderAddressMatches(string environmentVariable)
{
var filter = Environment.GetEnvironmentVariable(environmentVariable);
if (string.IsNullOrWhiteSpace(filter))
{
return true;
}
var span = filter.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) &&
_request.Program.Address == address;
}
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)
{
{
// The packed memory-offset byte of this buffer, loaded at entry.
return IAdd(byteAddress, Load(_uintType, RuntimeBufferBiasPointer(binding)));
}
}
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);
// Without subgroup operations each invocation is a one-lane wave, so a V_WRITELANE to
// lane N > 0 used to be dropped and V_READLANE returned lane 0. Compilers spill SGPRs
// (EXEC included) to VGPR lanes like this; restoring the wrong EXEC made waterfall
// loops spin until the dispatcher guard. Only VGPRs that are also read back get slots.
private List<(uint Register, uint Lane)> FindLaneSpillSlots()
{
var slots = new List<(uint Register, uint Lane)>();
if (_emulateWave64)
{
return slots;
}
var ownsLaneZero = !UsesSubgroupOperations();
var readRegisters = _request.Program.Instructions
.Where(static instruction => instruction.Opcode == "VReadlaneB32" &&
instruction.Sources.Count > 0 &&
instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister)
.Select(static instruction => instruction.Sources[0].Value)
.ToHashSet();
foreach (var instruction in _request.Program.Instructions)
{
if (instruction.Opcode == "VWritelaneB32" &&
TryGetVectorDestination(instruction, out var register) &&
readRegisters.Contains(register) &&
TryGetConstantLane(instruction, out var lane) &&
(lane != 0 || !ownsLaneZero) &&
!slots.Contains((register, lane)))
{
slots.Add((register, lane));
}
}
return slots;
}
// The lane select operand (source 1) of V_READLANE/V_WRITELANE when it is a constant.
private bool TryGetConstantLane(Gen5ShaderInstruction instruction, out uint lane)
{
lane = 0;
if (instruction.Sources.Count < 2)
{
return false;
}
var operand = instruction.Sources[1];
uint value;
if (operand.Kind == Gen5OperandKind.LiteralConstant)
{
value = operand.Value;
}
else if (operand.Kind != Gen5OperandKind.EncodedConstant ||
!TryDecodeInlineConstant(operand.Value, out value))
{
return false;
}
lane = value & LaneSelectMask;
return true;
}
private uint LaneSelectMask => _stage == Gen5SpirvStage.Compute ? _waveLaneCount - 1 : 63u;
private uint ReadLaneSpillSlot(Gen5ShaderInstruction instruction, uint selectedLane, uint value)
{
if (instruction.Sources[0].Kind != Gen5OperandKind.VectorRegister)
{
return value;
}
var register = instruction.Sources[0].Value;
if (TryGetConstantLane(instruction, out var lane))
{
return _laneSpillSlots.TryGetValue((register, lane), out var slot) ? Load(_uintType, slot) : value;
}
return SelectLaneSpillSlot(register, selectedLane, value);
}
// Folds the spill slots of register into value: selected lane == slot lane reads the slot.
private uint SelectLaneSpillSlot(uint register, uint selectedLane, uint value)
{
foreach (var ((slotRegister, slotLane), variable) in _laneSpillSlots)
{
if (slotRegister != register)
{
continue;
}
var isSlotLane = _module.AddInstruction(SpirvOp.IEqual, _boolType, selectedLane, UInt(slotLane));
value = _module.AddInstruction(SpirvOp.Select, _uintType, isSlotLane, Load(_uintType, variable), value);
}
return value;
}
private uint ScalarPointer(uint register)
{
if (register >= ScalarRegisterCount)
{
throw new InvalidOperationException($"Scalar register s{register} is outside the register file.");
}
return RegisterVariable(_scalarRegisterVariables, register, "s");
}
private uint RegisterVariable(Dictionary<uint, uint> variables, uint register, string prefix)
{
if (!variables.TryGetValue(register, out var variable))
{
variable = _module.AddGlobalVariable(_privateUintPointer, SpirvStorageClass.Private, _module.ConstantNull(_uintType));
_interfaces.Add(variable);
_module.AddName(variable, $"{prefix}{register}");
variables.Add(register, variable);
}
return variable;
}
private uint RuntimeBufferBiasPointer(int binding) =>
_module.AddInstruction(
SpirvOp.AccessChain,
_privateUintPointer,
_runtimeBufferBiases,
UInt(checked((uint)binding)));
private uint VectorPointer(uint register)
{
if (_vectorRegisters != 0)
{
return _module.AddInstruction(
SpirvOp.AccessChain,
_privateUintPointer,
_vectorRegisters,
UInt(register));
}
if (register >= VectorRegisterCount)
{
throw new InvalidOperationException($"Vector register v{register} is outside the register file.");
}
return RegisterVariable(_vectorRegisterVariables, register, "v");
}
// The V_MOVREL* opcodes address the VGPR file with a register number that
// is only known at run time (encoded number + M0), so the access chain
// takes a computed index instead of a constant. The index is masked to
// the array bounds: SPIR-V leaves an out-of-range Private access chain
// undefined, and a mask costs nothing next to the surrounding load.
// Every register a relative access can reach: from the lowest V_MOVREL* base to past the
// highest register the program names (a multi-dword result may extend beyond its listed
// first register). An index outside it reads zero and writes nothing.
private static (uint First, uint Last) DynamicVectorRange(Gen5ShaderProgram program)
{
var first = VectorRegisterCount - 1;
var last = 0u;
foreach (var instruction in program.Instructions)
{
foreach (var operand in instruction.Sources.Concat(instruction.Destinations))
{
if (operand.Kind == Gen5OperandKind.VectorRegister)
{
last = Math.Max(last, operand.Value);
if (instruction.Opcode.StartsWith("VMovrel", StringComparison.Ordinal))
{
first = Math.Min(first, operand.Value);
}
}
}
}
last = Math.Min(last + 8, VectorRegisterCount - 1);
return (Math.Min(first, last), last);
}
private uint LoadVDynamic(uint registerIndex)
{
var result = UInt(0);
for (var register = _dynamicVectorRange.First; register <= _dynamicVectorRange.Last; register++)
{
result = _module.AddInstruction(
SpirvOp.Select,
_uintType,
_module.AddInstruction(SpirvOp.IEqual, _boolType, registerIndex, UInt(register)),
Load(_uintType, VectorPointer(register)),
result);
}
return result;
}
private void StoreVDynamic(uint registerIndex, uint value)
{
// With EXEC known to be all ones the write needs no EXEC test.
var exec = _execKnownFull ? 0 : Load(_boolType, _exec);
for (var register = _dynamicVectorRange.First; register <= _dynamicVectorRange.Last; register++)
{
var pointer = VectorPointer(register);
var selected = _module.AddInstruction(SpirvOp.IEqual, _boolType, registerIndex, UInt(register));
if (exec != 0)
{
selected = _module.AddInstruction(SpirvOp.LogicalAnd, _boolType, exec, selected);
}
Store(pointer, _module.AddInstruction(SpirvOp.Select, _uintType, selected, value, Load(_uintType, pointer)));
}
}
private uint PackedHalfPointer(uint register) =>
_module.AddInstruction(
SpirvOp.AccessChain,
_privateVec2Pointer,
PackedHalfRegisters(),
UInt(register));
// Declared on first use when Private. AMD's compiler keeps an unused 4 KiB private
// array as a named .bss global: two stages then fail to link, and the driver copies
// the NOBITS section as file data and reads past the end of the ELF.
private uint PackedHalfRegisters()
{
if (_packedHalfRegisters != 0)
{
return _packedHalfRegisters;
}
var arrayType = _module.TypeArray(_vec2Type, VectorRegisterCount);
_packedHalfRegisters = _module.AddGlobalVariable(
_module.TypePointer(SpirvStorageClass.Private, arrayType),
SpirvStorageClass.Private,
_module.ConstantNull(arrayType));
_interfaces.Add(_packedHalfRegisters);
_module.AddName(_packedHalfRegisters, "vgprPackedHalf");
return _packedHalfRegisters;
}
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)
{
if (_waveLaneCount != 32 || register == 106)
{
Store(_vcc, IsLaneSetInMaskRegisters(106));
}
}
else if (register is 126 or 127)
{
if (_waveLaneCount != 32 || register == 126)
{
Store(_exec, IsLaneSetInMaskRegisters(126));
}
}
}
// In wave32 a lane mask is its low register alone (VCC_HI and EXEC_HI are ordinary SGPRs),
// so the lane's bit is tested on one dword instead of a composed 64-bit mask.
private uint IsLaneSetInMaskRegisters(uint lowRegister)
{
if (_waveLaneCount != 32)
{
return IsWaveMaskActive(LoadS64(lowRegister));
}
var laneBit = _subgroupInvocationIdInput == 0
? UInt(1)
: ShiftLeftLogical(UInt(1), GuestWaveLane());
return IsNotZero(BitwiseAnd(LoadS(lowRegister), laneBit));
}
// SHARPEMU_EXEC_GUARD_ELISION=0 guards every vector register write with EXEC again.
private static readonly bool ExecGuardElision = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_EXEC_GUARD_ELISION"), "0", StringComparison.Ordinal);
private IReadOnlySet<uint>? _execFullPcs;
// Set while an instruction that starts with every EXEC lane set is emitted: its vector
// register writes need no EXEC guard, so the old values need not stay live until them.
private bool _execKnownFull;
private bool IsExecKnownFull(uint pc)
{
if (!ExecGuardElision || !_request.EnableExecGuardElision)
{
return false;
}
_execFullPcs ??= Ir.Gen5ExecFullAnalysis.Analyze(_request.Program, wave32: _waveLaneCount == 32);
return _execFullPcs.Contains(pc);
}
private void StoreV(uint register, uint value, bool guardWithExec = true)
{
if (guardWithExec && !_execKnownFull)
{
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)
{
if (_execKnownFull)
{
Store(PackedHalfPointer(register), value);
return;
}
var active = Load(_boolType, _exec);
if (Environment.GetEnvironmentVariable(
"SHARPEMU_FORCE_PACKED_STORE_EXEC_VALUES") == "1" &&
_request.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.");
}
if (_flagVariables.Contains(pointer))
{
return _module.AddInstruction(
SpirvOp.INotEqual,
_boolType,
_module.AddInstruction(SpirvOp.Load, _uintType, pointer),
UInt(0));
}
return _module.AddInstruction(SpirvOp.Load, type, pointer);
}
private void Store(uint pointer, uint value)
{
if (_flagVariables.Contains(pointer))
{
value = _module.AddInstruction(
SpirvOp.Select,
_uintType,
value,
UInt(1),
UInt(0));
}
_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 _uintPairType;
private uint _intPairType;
private (uint Low, uint High) MultiplyExtended(uint left, uint right, bool signed)
{
var elementType = signed ? _intType : _uintType;
if (signed && _intPairType == 0)
{
_intPairType = _module.TypeStruct(_intType, _intType);
}
else if (!signed && _uintPairType == 0)
{
_uintPairType = _module.TypeStruct(_uintType, _uintType);
}
var product = _module.AddInstruction(
signed ? SpirvOp.SMulExtended : SpirvOp.UMulExtended,
signed ? _intPairType : _uintPairType,
left,
right);
return (
_module.AddInstruction(SpirvOp.CompositeExtract, elementType, product, 0),
_module.AddInstruction(SpirvOp.CompositeExtract, elementType, product, 1));
}
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 ShiftRightArithmetic64(uint left, uint right) =>
Bitcast(
_ulongType,
_module.AddInstruction(
SpirvOp.ShiftRightArithmetic,
_longType,
Bitcast(_longType, 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);
}
// Reads value from another lane of the host subgroup. Without subgroup
// support the invocation is a one-lane wave, so the only lane is itself;
// emitting the shuffle there would need a capability the module lacks.
// 1.0 when "reference <function> texel" holds, else 0.0. The function is the
// guest sampler's depth compare field, which uses VkCompareOp's order.
private uint EmulatedDepthCompare(uint reference, uint texel, int function)
{
SpirvOp op;
switch (function)
{
case 0: return Float(0f);
case 7: return Float(1f);
case 1: op = SpirvOp.FOrdLessThan; break;
case 2: op = SpirvOp.FOrdEqual; break;
case 3: op = SpirvOp.FOrdLessThanEqual; break;
case 4: op = SpirvOp.FOrdGreaterThan; break;
case 5: op = SpirvOp.FUnordNotEqual; break;
case 6: op = SpirvOp.FOrdGreaterThanEqual; break;
default: throw new InvalidOperationException($"invalid depth compare function {function}");
}
var passed = _module.AddInstruction(op, _boolType, reference, texel);
return _module.AddInstruction(SpirvOp.Select, _floatType, passed, Float(1f), Float(0f));
}
private uint ShuffleLane(uint value, uint lane) =>
_subgroupInvocationIdInput == 0
? value
: _module.AddInstruction(SpirvOp.GroupNonUniformShuffle, _uintType, UInt(3), value, lane);
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);
}
if (_emulateWave64)
{
var lane = GuestWaveLane();
var owned = OwnHalfBallot(condition);
var exchange = BeginWave64Exchange();
EmitConditional(IsHalfWaveLeader(lane), () =>
Store(Wave64ExchangePointer(exchange, ShiftRightLogical(lane, UInt(5))), owned));
EmitWave64Barrier();
return Pair64(
Load(_uintType, Wave64ExchangePointer(exchange, UInt(0))),
Load(_uintType, Wave64ExchangePointer(exchange, UInt(1))));
}
var ballot = _module.AddInstruction(
SpirvOp.GroupNonUniformBallot,
_uvec4Type,
UInt(3),
condition);
var low = _module.AddInstruction(
SpirvOp.CompositeExtract,
_uintType,
ballot,
0);
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 OwnHalfBallot(uint condition)
{
var ballot = _module.AddInstruction(
SpirvOp.GroupNonUniformBallot,
_uvec4Type,
UInt(3),
condition);
var component = ShiftRightLogical(Load(_uintType, _subgroupInvocationIdInput), UInt(5));
// Avoid dynamic extraction of subgroup ballot words: on the NVIDIA
// device tests it produced zero masks. Select the same word explicitly.
var result = _module.AddInstruction(SpirvOp.CompositeExtract, _uintType, ballot, 0);
for (uint index = 1; index < 4; index++)
{
var word = _module.AddInstruction(SpirvOp.CompositeExtract, _uintType, ballot, index);
result = _module.AddInstruction(SpirvOp.Select, _uintType,
_module.AddInstruction(SpirvOp.IEqual, _boolType, component, UInt(index)), word, result);
}
return result;
}
private uint BooleanToHalfWaveMask(uint condition)
{
var owned = _module.AddInstruction(SpirvOp.UConvert, _ulongType, OwnHalfBallot(condition));
return _module.AddInstruction(
SpirvOp.Select,
_ulongType,
_module.AddInstruction(SpirvOp.UGreaterThanEqual, _boolType, GuestWaveLane(), UInt(32)),
ShiftLeftLogical64(owned, _module.Constant64(_ulongType, 32)),
owned);
}
private uint BothHalvesOfOwnBallot(uint condition)
{
var owned = _module.AddInstruction(SpirvOp.UConvert, _ulongType, OwnHalfBallot(condition));
return BitwiseOr64(owned, ShiftLeftLogical64(owned, _module.Constant64(_ulongType, 32)));
}
private uint IsHalfWaveLeader(uint lane) =>
_module.AddInstruction(SpirvOp.IEqual, _boolType, BitwiseAnd(lane, UInt(31)), UInt(0));
private uint BeginWave64Exchange()
{
var parity = Load(_uintType, _wave64ExchangeParity);
Store(_wave64ExchangeParity, BitwiseXor(parity, UInt(1)));
return IAdd(
UInt(_wave64ExchangeOffset),
_module.AddInstruction(SpirvOp.IMul, _uintType, parity, UInt(Wave64ExchangeSlotCount)));
}
private uint Wave64ExchangePointer(uint exchange, uint slot) =>
_module.AddInstruction(
SpirvOp.AccessChain,
_wave64ExchangeElementPointer,
_wave64Exchange,
IAdd(exchange, slot));
private uint ExchangeWave64Value(uint writer, uint value)
{
var exchange = BeginWave64Exchange();
EmitConditional(writer, () => Store(Wave64ExchangePointer(exchange, UInt(0)), value));
EmitWave64Barrier();
return Load(_uintType, Wave64ExchangePointer(exchange, UInt(0)));
}
private void EmitExactWaveMasks(IReadOnlyList<uint> pairs)
{
var lane = GuestWaveLane();
var upperHalf = ShiftRightLogical(lane, UInt(5));
var lowerHalf = _module.AddInstruction(SpirvOp.IEqual, _boolType, upperHalf, UInt(0));
const int pairsPerExchange = (int)Wave64ExchangeSlotCount / 2;
for (var start = 0; start < pairs.Count; start += pairsPerExchange)
{
var count = Math.Min(pairsPerExchange, pairs.Count - start);
var owned = new uint[count];
for (var index = 0; index < count; index++)
{
var pair = pairs[start + index];
owned[index] = _module.AddInstruction(SpirvOp.Select, _uintType, lowerHalf, LoadS(pair), LoadS(pair + 1));
}
var exchange = BeginWave64Exchange();
EmitConditional(IsHalfWaveLeader(lane), () =>
{
for (var index = 0; index < count; index++)
{
Store(Wave64ExchangePointer(exchange, IAdd(UInt((uint)index * 2), upperHalf)), owned[index]);
}
});
EmitWave64Barrier();
for (var index = 0; index < count; index++)
{
var pair = pairs[start + index];
StoreS(pair, Load(_uintType, Wave64ExchangePointer(exchange, UInt((uint)index * 2))));
StoreS(pair + 1, Load(_uintType, Wave64ExchangePointer(exchange, UInt((uint)index * 2 + 1))));
}
}
}
private uint WaveMaskAny(uint register, uint laneActive) =>
_emulateWave64 && _subgroupInvocationIdInput != 0
? IsNotZero64(LoadS64(register))
: SubgroupAny(Load(_boolType, laneActive));
private static readonly bool HalfWaveMasks = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_WAVE64_HALF_MASKS"), "0", StringComparison.Ordinal);
private bool _halfMaskPlanBuilt;
private Ir.Gen5Wave64HalfMaskPlan? HalfMaskPlan()
{
if (_halfMaskPlanBuilt)
{
return _halfMaskPlan;
}
_halfMaskPlanBuilt = true;
if (HalfWaveMasks && UsesWave64Exchange() && _subgroupInvocationIdInput != 0)
{
var sharedFlatPcs = _request.Memory.Entries
.Where(static memory => memory.AddressSpace is FlatAddressSpace.Shared or FlatAddressSpace.SharedOrPrivate)
.Select(static memory => memory.Pc)
.ToHashSet();
_halfMaskPlan = Ir.Gen5Wave64HalfMaskAnalysis.Analyze(_request.Program, sharedFlatPcs);
}
return _halfMaskPlan;
}
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()));
// In wave32 a lane mask fills its register alone and the next one keeps its value.
// That includes VCC and EXEC: compilers use VCC_HI (s107) as an ordinary SGPR.
private void StoreWaveMask(uint register, uint condition)
{
if (_waveLaneCount == 32)
{
StoreS(register, Narrow(BooleanToWaveMask(condition)));
return;
}
if (_emulateWave64 &&
_subgroupInvocationIdInput != 0 &&
_emittingPc is { } pc &&
HalfMaskPlan() is { } plan &&
plan.HalfMaskWrites.TryGetValue(pc, out var halfRegister) &&
halfRegister == register)
{
StoreS64(register, BooleanToHalfWaveMask(condition));
return;
}
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 void EmitConditional(uint condition, Action whenTrue, Action whenFalse)
{
var trueLabel = _module.AllocateId();
var falseLabel = _module.AllocateId();
var mergeLabel = _module.AllocateId();
_module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0);
_module.AddStatement(SpirvOp.BranchConditional, condition, trueLabel, falseLabel);
_module.AddLabel(trueLabel);
whenTrue();
_module.AddStatement(SpirvOp.Branch, mergeLabel);
_module.AddLabel(falseLabel);
whenFalse();
_module.AddStatement(SpirvOp.Branch, mergeLabel);
_module.AddLabel(mergeLabel);
}
// Only instructions that address LDS memory need the array. ds_swizzle/ds_bpermute move data
// between lanes and GDS instructions use the global data share, so a graphics stage using
// just those must not get an 8 KiB zero-initialized per-invocation array: Metal pays for it
// in compile time (seconds for a large pixel shader) and in private memory per pixel.
private bool UsesLds() =>
_request.Program.Instructions.Any(static instruction =>
instruction.Control is Gen5DataShareControl { Gds: false } &&
instruction.Opcode is not ("DsSwizzleB32" or "DsBpermuteB32")) ||
_request.Memory.Entries.Any(static memory =>
memory.AddressSpace is FlatAddressSpace.Shared or FlatAddressSpace.SharedOrPrivate);
private bool UsesSubgroupShuffle() =>
_request.Program.Instructions.Any(instruction =>
instruction.Control is Gen5DppControl or Gen5Dpp8Control ||
instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32" or "VReadlaneB32" or
"DsAppend" or "DsConsume" or "DsSwizzleB32" or "DsBpermuteB32");
private bool UsesSubgroupBroadcast() =>
_request.Program.Instructions.Any(instruction =>
instruction.Opcode == "VReadfirstlaneB32");
private bool UsesWaveControl() =>
_request.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("VCmp", StringComparison.Ordinal) ||
instruction.Sources.Any(IsWaveMaskOperand) ||
instruction.Destinations.Any(IsWaveMaskOperand));
private bool UsesSubgroupOperations() =>
_enableGraphicsSubgroupOperations &&
(UsesSubgroupShuffle() ||
UsesSubgroupBroadcast() ||
UsesWaveControl() ||
_request.Program.Instructions.Any(static instruction =>
instruction.Opcode is "VMbcntLoU32B32" or "VMbcntHiU32B32" or "DsWriteAddtidB32" or "DsReadAddtidB32"));
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<ShaderBlock> BuildBasicBlocks(
IReadOnlyList<Gen5ShaderInstruction> instructions)
{
if (instructions.Count == 0)
{
return [];
}
var leaders = new SortedSet<uint> { 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<ShaderBlock>(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 static int FindInstructionIndex(
IReadOnlyList<Gen5ShaderInstruction> 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<ShaderBlock> 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);
}
}