From 52e17b5056069eeb5ea74226a5c35b2808e407dc Mon Sep 17 00:00:00 2001 From: Berk Date: Sat, 4 Jul 2026 13:51:08 +0300 Subject: [PATCH] [shader-decoder-part1] Implemented a shader decoder (Part 1) (#12) * [shader-decoder-part1] Implemented a shader decoder for Gen5 shaders, including IR generation, metadata reading, scalar evaluation, and SPIR-V translation. Updated related exports and video output components to support the new shader decoding functionality. * [shader decoder] correct RDNA2 operands, fixing synchronization problems * [shader-decoder] RDNA2 decoder improvements * [shader-decoder] fix RDNA2 shift masking and sprite draws * [shader-decoder] improve RDNA2 shader decoder to support more instructions and fix some issues with the previous implementation. --- src/SharpEmu.Libs/Agc/AgcExports.cs | 2505 ++++++++- src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs | 313 ++ .../Agc/Gen5ShaderMetadataReader.cs | 154 + .../Agc/Gen5ShaderScalarEvaluator.cs | 1742 ++++++ src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs | 1784 ++++++ .../Agc/Gen5SpirvTranslator.Alu.cs | 2374 ++++++++ src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs | 2513 +++++++++ src/SharpEmu.Libs/Agc/SpirvFixedShaders.cs | 167 + src/SharpEmu.Libs/Agc/SpirvModuleBuilder.cs | 885 +++ .../Kernel/KernelMemoryCompatExports.cs | 31 + src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 23 +- .../VideoOut/VulkanVideoPresenter.cs | 4849 ++++++++++++++++- 12 files changed, 17232 insertions(+), 108 deletions(-) create mode 100644 src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs create mode 100644 src/SharpEmu.Libs/Agc/Gen5ShaderMetadataReader.cs create mode 100644 src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs create mode 100644 src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs create mode 100644 src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs create mode 100644 src/SharpEmu.Libs/Agc/SpirvFixedShaders.cs create mode 100644 src/SharpEmu.Libs/Agc/SpirvModuleBuilder.cs diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index dc9a657..7f7cc83 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -17,14 +17,23 @@ public static class AgcExports private const uint ItSetBase = 0x11; private const uint ItIndexBufferSize = 0x13; private const uint ItIndexBase = 0x26; + private const uint ItDrawIndirect = 0x24; + private const uint ItDrawIndexIndirect = 0x25; + private const uint ItDrawIndex2 = 0x27; private const uint ItIndexType = 0x2A; + private const uint ItDrawIndexAuto = 0x2D; private const uint ItNumInstances = 0x2F; + private const uint ItDrawIndexMultiAuto = 0x30; private const uint ItDrawIndexOffset2 = 0x35; + private const uint ItWriteData = 0x37; private const uint ItDispatchDirect = 0x15; private const uint ItDispatchIndirect = 0x16; private const uint ItWaitRegMem = 0x3C; private const uint ItEventWrite = 0x46; + private const uint ItDmaData = 0x50; + private const uint ItSetContextReg = 0x69; private const uint ItSetShReg = 0x76; + private const uint ItSetUconfigReg = 0x79; private const uint ItGetLodStats = 0x8E; private const uint RZero = 0x00; private const uint RDrawIndexAuto = 0x04; @@ -53,11 +62,52 @@ public static class AgcExports private const uint SpiPsInputAddr = 0x1B4; private const uint ComputePgmLo = 0x20C; private const uint ComputePgmHi = 0x20D; + private const uint ComputePgmRsrc2 = 0x213; + private const uint ComputeNumThreadX = 0x207; + private const uint ComputeNumThreadY = 0x208; + private const uint ComputeNumThreadZ = 0x209; private const uint SpiPsInputCntl0 = 0x191; private const uint VgtPrimitiveType = 0x242; + private const uint PaScScreenScissorTl = 0x0C; + private const uint PaScScreenScissorBr = 0x0D; + private const uint CbTargetMask = 0x8E; + private const uint PaScWindowOffset = 0x80; + private const uint PaScWindowScissorTl = 0x81; + private const uint PaScWindowScissorBr = 0x82; + private const uint PaScGenericScissorTl = 0x90; + private const uint PaScGenericScissorBr = 0x91; + private const uint PaScVportScissor0Tl = 0x94; + private const uint PaScVportScissor0Br = 0x95; + private const uint PaClVportXScale = 0x10F; + private const uint PaClVportXOffset = 0x110; + private const uint PaClVportYScale = 0x111; + private const uint PaClVportYOffset = 0x112; + private const uint PaScVportZMin0 = 0xB4; + private const uint PaScVportZMax0 = 0xB5; + private const uint CbBlendRed = 0x105; + private const uint CbBlendGreen = 0x106; + private const uint CbBlendBlue = 0x107; + private const uint CbBlendAlpha = 0x108; + private const uint CbColorControl = 0x202; + private const uint CbColor0Base = 0x318; + private const uint CbColorRegisterStride = 15; + private const uint CbColor0Info = 0x31C; + private const uint CbColor0BaseExt = 0x390; + private const uint CbColor0Attrib2 = 0x3B0; + private const uint CbColor0Attrib3 = 0x3B8; + private const uint CbBlend0Control = 0x1E0; + private const uint PaScModeCntl0 = 0x292; + private const int ColorTargetCount = 8; private const uint PsTextureUserDataRegister = 0xC; + private const uint VsUserDataRegister = 0x4C; + private const uint GsUserDataRegister = 0x8C; + private const uint EsUserDataRegister = 0xCC; + private const uint ComputeUserDataRegister = 0x240; + private const uint NggUserDataScalarRegisterBase = 8; private const uint Gen5TextureFormatR8G8B8A8Unorm = 56; + private const uint Gen5TextureFormatR16G16B16A16Float = 71; private const uint Gen5TextureType2D = 9; + private const ulong MaxPresentedTextureBytes = 128UL * 1024UL * 1024UL; private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000; private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200; private const ulong VideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000; @@ -91,23 +141,57 @@ public static class AgcExports private static readonly object _submitTraceGate = new(); private static readonly HashSet _tracedDcbSizes = new(); private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new(); + private static readonly HashSet<(ulong Es, ulong Ps)> _tracedShaderDecodePairs = new(); + private static readonly HashSet<(ulong Es, ulong Ps, ulong Target, ulong Texture, uint VertexCount)> _tracedShaderDraws = new(); + private static readonly HashSet<(ulong Ps, string Error)> _tracedShaderFailures = new(); + private static readonly HashSet<(int Handle, int Index, ulong Address, string Path)> _tracedDisplayBuffers = new(); + private static readonly HashSet _tracedComputeShaders = new(); + private static readonly HashSet _tracedSubmittedDrawOpcodes = new(); + private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new(); + private static readonly Dictionary< + (ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output), + (byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new(); + private static readonly Dictionary< + (ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ), + byte[]> _computeSpirvCache = new(); + private static readonly Dictionary _shaderHeadersByCode = new(); private static long _dcbWriteDataTraceCount; private static long _dcbWaitRegMemTraceCount; private static long _createShaderTraceCount; private static long _packetPayloadTraceCount; + private static bool _tracedMissingPixelShaderBindings; private static long _unsatisfiedWaitTraceCount; private static long _shaderTranslationMissTraceCount; + private static long _translatedDrawTraceCount; + private static long _standardDmaTraceCount; private static readonly object _softwarePresenterGate = new(); private static readonly Dictionary<(ulong Source, ulong Destination), ulong> _softwarePresenterFingerprints = new(); + private static readonly Dictionary<(ulong Shader, ulong Source, ulong Destination), ulong> _softwareComputeBlitFingerprints = new(); private static readonly object _registerDefaultsGate = new(); private static readonly ConditionalWeakTable _registerDefaultsAllocations = new(); private static readonly ConditionalWeakTable _submittedGpuStates = new(); private static readonly RegisterDefaultGroup[] PrimaryRegisterDefaults = [ + new(0, 0, 0xE24F806D, [new(CbColorControl, 0x00CC0010)]), new(0, 3, 0x0BC65DA4, [new(0x08F, 0)]), new(0, 4, 0x9E5AD592, [new(0x08E, 0)]), new(0, 12, 0x6DE4C312, [new(0x203, 0)]), + new(0, 28, 0x1EB8D73A, [new(PaScModeCntl0, 0x00000002)]), + new(0, 31, 0xA20EFC70, [new(PaScWindowOffset, 0)]), + new(0, 58, 0x43FBD769, + [ + new(CbBlendRed, 0), + new(CbBlendBlue, 0), + new(CbBlendGreen, 0), + new(CbBlendAlpha, 0), + ]), + new(0, 59, 0xEF550356, [new(CbBlend0Control, 0x20010001)]), + new(0, 67, 0x918106BB, + [ + new(PaScGenericScissorTl, 0x80000000), + new(PaScGenericScissorBr, 0x40004000), + ]), new(0, 72, 0x38E92C91, [ new(0x318, 0), @@ -142,6 +226,11 @@ public static class AgcExports new(0x0B4, 0), new(0x0B5, 0), ]), + new(0, 77, 0x078D7060, + [ + new(PaScWindowScissorTl, 0x80000000), + new(PaScWindowScissorBr, 0x40004000), + ]), new(1, 13, 0xC918DF3E, [new(0x20C, 0), new(0x20D, 0)]), new(1, 14, 0xC9751C9C, [new(0x0C8, 0), new(0x0C9, 0)]), new(1, 18, 0xC9E01B31, [new(0x008, 0), new(0x009, 0)]), @@ -181,8 +270,81 @@ public static class AgcExports uint Width, uint Height, uint Format, + uint NumberType, uint TileMode, - uint Type); + uint Type, + uint BaseLevel, + uint LastLevel, + uint Pitch, + uint DstSelect) + { + public uint MipLevels + { + get + { + var largestDimension = Math.Max(Width, Height); + uint maximumMipLevels = 1; + while (largestDimension > 1) + { + largestDimension >>= 1; + maximumMipLevels++; + } + + var descriptorMipLevels = LastLevel >= BaseLevel + ? LastLevel - BaseLevel + 1 + : 1; + return Math.Min(descriptorMipLevels, maximumMipLevels); + } + } + } + + private readonly record struct RenderTargetDescriptor( + uint Slot, + ulong Address, + uint Width, + uint Height, + uint Format, + uint NumberType, + uint TileMode); + + private sealed record TranslatedGuestDraw( + ulong ExportShaderAddress, + ulong PixelShaderAddress, + uint PrimitiveType, + byte[] VertexSpirv, + byte[] PixelSpirv, + uint AttributeCount, + uint VertexCount, + uint InstanceCount, + VulkanGuestIndexBuffer? IndexBuffer, + IReadOnlyList Textures, + IReadOnlyList GlobalMemoryBindings, + IReadOnlyList VertexInputs, + IReadOnlyList RenderTargets, + VulkanGuestRenderState RenderState); + + private sealed record TranslatedImageBinding( + TextureDescriptor Descriptor, + bool IsStorage, + uint MipLevel, + IReadOnlyList SamplerDescriptor); + + private readonly record struct RenderTargetWriter( + ulong Sequence, + ulong ExportShaderAddress, + ulong PixelShaderAddress, + uint VertexCount, + uint PrimitiveType); + + private readonly record struct ComputeImageWriter( + ulong Sequence, + ulong ShaderAddress, + string Opcode); + + private readonly record struct ComputeDispatch( + uint GroupCountX, + uint GroupCountY, + uint GroupCountZ); private sealed class SubmittedDcbState { @@ -191,7 +353,15 @@ public static class AgcExports public Dictionary UcRegisters { get; } = new(); public TextureDescriptor? PresenterTexture { get; set; } public GuestDrawKind GuestDrawKind { get; set; } + public TranslatedGuestDraw? TranslatedDraw { get; set; } + public Dictionary RenderTargetWriters { get; } = new(); + public ulong IndirectArgsAddress { get; set; } public bool SawIndexedDraw { get; set; } + public ulong IndexBufferAddress { get; set; } + public uint IndexBufferCount { get; set; } + public uint IndexSize { get; set; } + public uint InstanceCount { get; set; } = 1; + public uint DrawIndexOffset { get; set; } } private sealed class SubmittedGpuState @@ -199,6 +369,8 @@ public static class AgcExports public object Gate { get; } = new(); public SubmittedDcbState Graphics { get; } = new(); public Dictionary ComputeQueues { get; } = new(); + public Dictionary ComputeImageWriters { get; } = new(); + public ulong WorkSequence { get; set; } } private readonly record struct RegisterDefaultValue(uint Offset, uint Value); @@ -309,6 +481,11 @@ public static class AgcExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } + lock (_submitTraceGate) + { + _shaderHeadersByCode[codeAddress] = headerAddress; + } + TraceCreateShader(destinationAddress, headerAddress, codeAddress, "ok"); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; @@ -1764,7 +1941,7 @@ public static class AgcExports var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); lock (gpuState.Gate) { - ParseSubmittedDcb(ctx, gpuState.Graphics, commandAddress, dwordCount, tracePackets); + ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets); } ctx[CpuRegister.Rax] = 0; @@ -1812,7 +1989,7 @@ public static class AgcExports gpuState.ComputeQueues.Add(ownerHandle, queueState); } - ParseSubmittedDcb(ctx, queueState, commandAddress, dwordCount, tracePackets); + ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets); } ctx[CpuRegister.Rax] = 0; @@ -1854,6 +2031,7 @@ public static class AgcExports private static void ParseSubmittedDcb( CpuContext ctx, + SubmittedGpuState gpuState, SubmittedDcbState state, ulong commandAddress, uint dwordCount, @@ -1913,6 +2091,15 @@ public static class AgcExports ApplySubmittedRegisters(ctx, state, currentAddress, length, op, register); + if (op == ItSetBase && + length >= 4 && + TryReadUInt32(ctx, currentAddress + 4, out var baseSelector) && + baseSelector == 1 && + TryReadUInt64(ctx, currentAddress + 8, out var indirectArgsAddress)) + { + state.IndirectArgsAddress = indirectArgsAddress; + } + if (op == ItEventWrite && length >= 2 && TryReadUInt32(ctx, currentAddress + sizeof(uint), out var eventTypeRaw)) @@ -1938,11 +2125,51 @@ public static class AgcExports ApplySubmittedWriteData(ctx, currentAddress, length, tracePackets); } + if (op == ItWriteData && length >= 4) + { + ApplySubmittedWriteData(ctx, currentAddress, length, tracePackets); + } + if (op == ItNop && register == RDmaData && length >= 8) { ApplySubmittedDmaData(ctx, currentAddress, tracePackets); } + if (op == ItDmaData && length >= 7) + { + ApplySubmittedStandardDmaData(ctx, currentAddress); + } + + if (op == ItIndexBase && + length >= 3 && + TryReadUInt32(ctx, currentAddress + 4, out var indexBaseLo) && + TryReadUInt32(ctx, currentAddress + 8, out var indexBaseHi)) + { + state.IndexBufferAddress = + indexBaseLo | ((ulong)indexBaseHi << 32); + } + + if (op == ItIndexBufferSize && + length >= 2 && + TryReadUInt32(ctx, currentAddress + 4, out var indexBufferCount)) + { + state.IndexBufferCount = indexBufferCount; + } + + if (op == ItIndexType && + length >= 2 && + TryReadUInt32(ctx, currentAddress + 4, out var indexSize)) + { + state.IndexSize = indexSize & 0x3; + } + + if (op == ItNumInstances && + length >= 2 && + TryReadUInt32(ctx, currentAddress + 4, out var instanceCount)) + { + state.InstanceCount = Math.Max(instanceCount, 1); + } + if (op == ItNop && register is RWaitMem32 or RWaitMem64 && length >= (register == RWaitMem32 ? 6u : 9u)) @@ -1955,13 +2182,30 @@ public static class AgcExports ObserveSubmittedStandardWaitRegMem(ctx, currentAddress, tracePackets); } - if (op == ItDrawIndexOffset2 && - length >= 5 && - TryReadUInt32(ctx, currentAddress + 4, out var indexCount) && + if (TryReadSubmittedDrawCount( + ctx, + state, + currentAddress, + length, + op, + out var indexCount) && indexCount != 0) { - state.SawIndexedDraw = true; - TryTranslateGuestDraw(ctx, state, indexCount); + lock (_submitTraceGate) + { + if (_tracedSubmittedDrawOpcodes.Add(op)) + { + TraceAgcShader( + $"agc.draw_packet op=0x{op:X2} count={indexCount}"); + } + } + + var indexed = op is + ItDrawIndex2 or + ItDrawIndexOffset2 or + ItDrawIndexIndirect; + state.SawIndexedDraw |= indexed; + TryTranslateGuestDraw(ctx, gpuState, state, indexCount, indexed); } if (op == ItNop && @@ -1970,8 +2214,24 @@ public static class AgcExports TryReadUInt32(ctx, currentAddress + 4, out var autoIndexCount) && autoIndexCount != 0) { - state.SawIndexedDraw = true; - TryTranslateGuestDraw(ctx, state, autoIndexCount); + TryTranslateGuestDraw( + ctx, + gpuState, + state, + autoIndexCount, + indexed: false); + } + + if ((op is ItDispatchDirect or ItDispatchIndirect) && + TryReadComputeDispatch( + ctx, + state, + currentAddress, + length, + op, + out var dispatch)) + { + ObserveComputeDispatch(ctx, gpuState, state, dispatch); } if (op == ItNop && register == RFlip && length >= 6) @@ -1987,7 +2247,68 @@ public static class AgcExports var flipArg = unchecked((long)(((ulong)flipArgHi << 32) | flipArgLo)); var displayBufferIndex = unchecked((int)displayBufferIndexRaw); - if (state.SawIndexedDraw && state.PresenterTexture is { } sourceTexture) + var handle = unchecked((int)videoOutHandle); + if (VideoOutExports.TryGetDisplayBufferInfo( + handle, + displayBufferIndex, + out var cachedDisplayBuffer) && + VulkanVideoPresenter.TrySubmitGuestImage( + cachedDisplayBuffer.Address, + cachedDisplayBuffer.Width, + cachedDisplayBuffer.Height, + cachedDisplayBuffer.PitchInPixel)) + { + TraceDisplayBuffer( + handle, + displayBufferIndex, + cachedDisplayBuffer, + "gpu-cache"); + } + else if (state.SawIndexedDraw && + state.TranslatedDraw is { } translatedDraw && + VideoOutExports.TryGetDisplayBufferInfo( + handle, + displayBufferIndex, + out var translatedDisplayBuffer)) + { + TraceDisplayBuffer( + handle, + displayBufferIndex, + translatedDisplayBuffer, + "draw-fallback"); + var textures = CreateVulkanGuestDrawTextures(ctx, translatedDraw.Textures, out var fallbackTextureCount); + var globalMemoryBuffers = + CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings); + VulkanVideoPresenter.SubmitTranslatedDraw( + translatedDraw.PixelSpirv, + textures, + globalMemoryBuffers, + translatedDisplayBuffer.Width, + translatedDisplayBuffer.Height, + translatedDraw.AttributeCount); + TraceAgcShader( + $"agc.shader_present ps=0x{translatedDraw.PixelShaderAddress:X16} " + + $"spirv={translatedDraw.PixelSpirv.Length} textures={textures.Count} " + + $"global_buffers={globalMemoryBuffers.Count} " + + $"fallback={fallbackTextureCount} {translatedDisplayBuffer.Width}x{translatedDisplayBuffer.Height}"); + + for (var i = 0; i < translatedDraw.Textures.Count; i++) + { + var binding = translatedDraw.Textures[i]; + var d = binding.Descriptor; + + TraceAgcShader( + $"agc.present_desc[{i}] " + + $"addr=0x{d.Address:X16} " + + $"size={d.Width}x{d.Height} " + + $"fmt={d.Format} " + + $"num={d.NumberType} " + + $"type={d.Type} " + + $"tile={d.TileMode} " + + $"storage={binding.IsStorage}"); + } + } + else if (state.SawIndexedDraw && state.PresenterTexture is { } sourceTexture) { _ = TrySoftwarePresent( ctx, @@ -1998,7 +2319,7 @@ public static class AgcExports else if (state.SawIndexedDraw && state.GuestDrawKind != GuestDrawKind.None && VideoOutExports.TryGetDisplayBufferInfo( - unchecked((int)videoOutHandle), + handle, displayBufferIndex, out var displayBuffer)) { @@ -2008,15 +2329,37 @@ public static class AgcExports displayBuffer.Height); } - _ = VideoOutExports.SubmitFlipFromAgc(ctx, unchecked((int)videoOutHandle), displayBufferIndex, unchecked((int)flipMode), flipArg); + _ = VideoOutExports.SubmitFlipFromAgc(ctx, handle, displayBufferIndex, unchecked((int)flipMode), flipArg); state.SawIndexedDraw = false; state.GuestDrawKind = GuestDrawKind.None; + state.TranslatedDraw = null; } offset += length; } } + private static void TraceDisplayBuffer( + int handle, + int index, + VideoOutExports.DisplayBufferInfo buffer, + string path) + { + lock (_submitTraceGate) + { + if (!_tracedDisplayBuffers.Add((handle, index, buffer.Address, path))) + { + return; + } + } + + TraceAgcShader( + $"agc.display_buffer handle={handle} index={index} " + + $"addr=0x{buffer.Address:X16} fmt=0x{buffer.PixelFormat:X16} " + + $"tile={buffer.TilingMode} size={buffer.Width}x{buffer.Height} " + + $"pitch={buffer.PitchInPixel} path={path}"); + } + private static void ApplySubmittedDmaData( CpuContext ctx, ulong packetAddress, @@ -2043,6 +2386,86 @@ public static class AgcExports } } + private static void ApplySubmittedStandardDmaData( + CpuContext ctx, + ulong packetAddress) + { + if (!TryReadUInt32(ctx, packetAddress + 4, out var control) || + !TryReadUInt32(ctx, packetAddress + 8, out var sourceLow) || + !TryReadUInt32(ctx, packetAddress + 12, out var sourceHigh) || + !TryReadUInt32(ctx, packetAddress + 16, out var destinationLow) || + !TryReadUInt32(ctx, packetAddress + 20, out var destinationHigh) || + !TryReadUInt32(ctx, packetAddress + 24, out var command)) + { + return; + } + + var byteCount = command & 0x1F_FFFFu; + var sourceSelect = (control >> 29) & 0x3u; + var destinationSelect = (control >> 20) & 0x3u; + var destinationSwap = (command >> 24) & 0x3u; + var sourceAddressSpace = (command >> 26) & 0x1u; + var destinationAddressSpace = (command >> 27) & 0x1u; + var sourceAddressIncrement = (command >> 28) & 0x1u; + if (byteCount == 0 || + destinationSwap != 0 || + destinationSelect is not (0 or 3) || + (destinationSelect == 0 && destinationAddressSpace != 0)) + { + return; + } + + var destinationAddress = + destinationLow | ((ulong)destinationHigh << 32); + bool copied; + ulong sourceAddress; + if (sourceSelect is 0 or 3 && + (sourceSelect == 3 || sourceAddressSpace == 0)) + { + sourceAddress = sourceLow | ((ulong)sourceHigh << 32); + if (sourceAddressIncrement != 0) + { + copied = + TryReadUInt32(ctx, sourceAddress, out var fillValue) && + TryFillGuestMemory( + ctx, + fillValue, + destinationAddress, + byteCount); + } + else + { + copied = TryCopyGuestMemory( + ctx, + sourceAddress, + destinationAddress, + byteCount); + } + } + else if (sourceSelect == 2) + { + sourceAddress = 0; + copied = TryFillGuestMemory( + ctx, + sourceLow, + destinationAddress, + byteCount); + } + else + { + return; + } + + if (ShouldTraceHotPath(ref _standardDmaTraceCount)) + { + TraceAgcShader( + $"agc.dma_packet dst=0x{destinationAddress:X16} " + + $"src=0x{sourceAddress:X16} bytes={byteCount} " + + $"src_sel={sourceSelect} fill={sourceAddressIncrement != 0 || sourceSelect == 2} " + + $"copied={copied}"); + } + } + private static void ApplySubmittedWriteData( CpuContext ctx, ulong packetAddress, @@ -2215,7 +2638,7 @@ public static class AgcExports uint op, uint register) { - if (op == ItSetShReg) + if (op is ItSetShReg or ItSetContextReg or ItSetUconfigReg) { if (packetLength < 3 || !TryReadUInt32(ctx, packetAddress + sizeof(uint), out var startRegister)) @@ -2223,6 +2646,12 @@ public static class AgcExports return; } + var directDestination = op switch + { + ItSetShReg => state.ShRegisters, + ItSetContextReg => state.CxRegisters, + _ => state.UcRegisters, + }; for (uint index = 0; index < packetLength - 2; index++) { if (!TryReadUInt32( @@ -2233,7 +2662,7 @@ public static class AgcExports return; } - state.ShRegisters[startRegister + index] = value; + directDestination[startRegister + index] = value; } return; @@ -2270,16 +2699,61 @@ public static class AgcExports } } - private static void TryTranslateGuestDraw( + private static bool TryReadSubmittedDrawCount( CpuContext ctx, SubmittedDcbState state, - uint vertexCount) + ulong packetAddress, + uint packetLength, + uint op, + out uint drawCount) { - if (state.GuestDrawKind != GuestDrawKind.None || vertexCount != 3) + drawCount = 0; + switch (op) { - return; - } + case ItDrawIndexAuto when packetLength >= 3: + return TryReadUInt32(ctx, packetAddress + 4, out drawCount); + case ItDrawIndex2 when packetLength >= 6: + state.DrawIndexOffset = 0; + return TryReadUInt32(ctx, packetAddress + 16, out drawCount); + case ItDrawIndexOffset2 when packetLength >= 5: + if (!TryReadUInt32(ctx, packetAddress + 8, out var indexOffset)) + { + return false; + } + state.DrawIndexOffset = indexOffset; + return TryReadUInt32(ctx, packetAddress + 12, out drawCount); + case ItDrawIndexMultiAuto when packetLength >= 4: + if (!TryReadUInt32(ctx, packetAddress + 12, out var control)) + { + return false; + } + + drawCount = (control >> 21) & 0x7FFu; + return true; + case ItDrawIndirect or ItDrawIndexIndirect + when packetLength >= 5 && state.IndirectArgsAddress != 0: + if (!TryReadUInt32(ctx, packetAddress + 4, out var dataOffset)) + { + return false; + } + + return TryReadUInt32( + ctx, + state.IndirectArgsAddress + dataOffset, + out drawCount); + default: + return false; + } + } + + private static void TryTranslateGuestDraw( + CpuContext ctx, + SubmittedGpuState gpuState, + SubmittedDcbState state, + uint vertexCount, + bool indexed) + { var hasExportShader = TryGetShaderAddress( state.ShRegisters, SpiShaderPgmLoEs, @@ -2292,44 +2766,1627 @@ public static class AgcExports out var pixelShaderAddress); var hasPsInputEna = state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna); var hasPsInputAddr = state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr); - if (!hasExportShader || - !hasPixelShader || - !hasPsInputEna || - !hasPsInputAddr || - !Gen5ShaderTranslator.TryTranslate( - ctx, - exportShaderAddress, - pixelShaderAddress, - psInputEna, - psInputAddr, - out var drawKind)) + state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType); + var renderTargets = GetRenderTargets(state.CxRegisters); + var drawSequence = ++gpuState.WorkSequence; + state.TranslatedDraw = null; + state.GuestDrawKind = GuestDrawKind.None; + foreach (var target in renderTargets) { - TraceShaderTranslationMiss( + state.RenderTargetWriters[target.Address] = new RenderTargetWriter( + drawSequence, + hasExportShader ? exportShaderAddress : 0, + hasPixelShader ? pixelShaderAddress : 0, vertexCount, - hasExportShader, - exportShaderAddress, - hasPixelShader, - pixelShaderAddress, - hasPsInputEna, - psInputEna, - hasPsInputAddr, - psInputAddr); + primitiveType); + + TraceAgcShader( + $"agc.rt_writer seq={drawSequence} target=0x{target.Address:X16} " + + $"fmt={target.Format} tile={target.TileMode} " + + $"size={target.Width}x{target.Height} vertices={vertexCount} " + + $"es=0x{(hasExportShader ? exportShaderAddress : 0):X16} " + + $"ps=0x{(hasPixelShader ? pixelShaderAddress : 0):X16}"); + } + + if (vertexCount == 0 || vertexCount > 1_048_576) + { return; } - state.GuestDrawKind = drawKind; + var translationError = string.Empty; + if (hasExportShader && + hasPixelShader && + hasPsInputEna && + hasPsInputAddr && + TryCreateTranslatedGuestDraw( + ctx, + state, + exportShaderAddress, + pixelShaderAddress, + vertexCount, + indexed, + out var translatedDraw, + out translationError)) + { + state.TranslatedDraw = translatedDraw; + var firstTarget = translatedDraw.RenderTargets.FirstOrDefault(); + if (firstTarget.Address != 0) + { + var textures = CreateVulkanGuestDrawTextures( + ctx, + translatedDraw.Textures, + out _); + var globalMemoryBuffers = + CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings); + var vertexBuffers = + CreateVulkanGuestVertexBuffers(translatedDraw.VertexInputs); + VulkanVideoPresenter.SubmitOffscreenTranslatedDraw( + translatedDraw.PixelSpirv, + textures, + globalMemoryBuffers, + translatedDraw.AttributeCount, + new VulkanGuestRenderTarget( + firstTarget.Address, + firstTarget.Width, + firstTarget.Height, + firstTarget.Format, + firstTarget.NumberType), + translatedDraw.VertexSpirv, + translatedDraw.VertexCount, + translatedDraw.InstanceCount, + translatedDraw.PrimitiveType, + translatedDraw.IndexBuffer, + vertexBuffers, + translatedDraw.RenderState); + } + else + { + var storageTarget = translatedDraw.Textures + .FirstOrDefault(binding => binding.IsStorage); + if (storageTarget is not null) + { + var textures = CreateVulkanGuestDrawTextures( + ctx, + translatedDraw.Textures, + out _); + var globalMemoryBuffers = + CreateVulkanGuestMemoryBuffers(translatedDraw.GlobalMemoryBindings); + VulkanVideoPresenter.SubmitStorageTranslatedDraw( + translatedDraw.PixelSpirv, + textures, + globalMemoryBuffers, + translatedDraw.AttributeCount, + storageTarget.Descriptor.Width, + storageTarget.Descriptor.Height); + } + } + + if (ShouldTraceHotPath(ref _translatedDrawTraceCount)) + { + TraceAgcShader( + $"agc.shader_draw_seen seq={drawSequence} " + + $"es=0x{exportShaderAddress:X16} ps=0x{pixelShaderAddress:X16} " + + $"target=0x{firstTarget.Address:X16}:{firstTarget.Width}x{firstTarget.Height}:fmt{firstTarget.Format}/tile{firstTarget.TileMode} " + + $"textures={translatedDraw.Textures.Count}"); + } + + lock (_submitTraceGate) + { + var firstTextureAddress = translatedDraw.Textures.FirstOrDefault()?.Descriptor.Address ?? 0; + if (_tracedShaderDraws.Add( + (exportShaderAddress, pixelShaderAddress, firstTarget.Address, firstTextureAddress, vertexCount))) + { + TraceTranslatedGuestDraw( + ctx, + gpuState, + state, + translatedDraw, + psInputEna, + psInputAddr); + } + } + + return; + } + + TraceShaderTranslationMiss( + ctx, + state, + vertexCount, + hasExportShader, + exportShaderAddress, + hasPixelShader, + pixelShaderAddress, + hasPsInputEna, + psInputEna, + hasPsInputAddr, + psInputAddr, + hasExportShader && hasPixelShader ? translationError : null); + } + + private static bool TryCreateTranslatedGuestDraw( + CpuContext ctx, + SubmittedDcbState state, + ulong exportShaderAddress, + ulong pixelShaderAddress, + uint vertexCount, + bool indexed, + out TranslatedGuestDraw draw, + out string error) + { + draw = default!; + error = string.Empty; + ulong exportShaderHeader; + ulong pixelShaderHeader; lock (_submitTraceGate) { - if (_tracedShaderTranslations.Add((exportShaderAddress, pixelShaderAddress, drawKind))) + _shaderHeadersByCode.TryGetValue(exportShaderAddress, out exportShaderHeader); + _shaderHeadersByCode.TryGetValue(pixelShaderAddress, out pixelShaderHeader); + } + + if (!Gen5ShaderTranslator.TryCreateState( + ctx, + exportShaderAddress, + exportShaderHeader, + state.ShRegisters, + SelectExportUserDataRegister(state.ShRegisters), + out var exportState, + out error, + userDataScalarRegisterBase: NggUserDataScalarRegisterBase) || + !Gen5ShaderScalarEvaluator.TryEvaluate( + ctx, + exportState, + out var exportEvaluation, + out error, + resolveVertexInputs: true) || + !Gen5ShaderTranslator.TryCreateState( + ctx, + pixelShaderAddress, + pixelShaderHeader, + state.ShRegisters, + PsTextureUserDataRegister, + out var pixelState, + out error) || + !Gen5ShaderScalarEvaluator.TryEvaluate( + ctx, + pixelState, + out var pixelEvaluation, + out error)) + { + return false; + } + + var renderTargets = GetRenderTargets(state.CxRegisters) + .Where(target => + target.Slot == 0 && + HasPixelColorExport(pixelState, target.Slot)) + .ToArray(); + var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType); + var exportStateFingerprint = ComputeShaderStateFingerprint(exportEvaluation); + var pixelStateFingerprint = ComputeShaderStateFingerprint(pixelEvaluation); + var shaderKey = ( + exportShaderAddress, + exportStateFingerprint, + pixelShaderAddress, + pixelStateFingerprint, + outputKind); + (byte[] Vertex, byte[] Pixel) compiled; + lock (_submitTraceGate) + { + _graphicsSpirvCache.TryGetValue(shaderKey, out compiled); + } + + if (compiled.Vertex is null || compiled.Pixel is null) + { + var totalGlobalBuffers = + pixelEvaluation.GlobalMemoryBindings.Count + + exportEvaluation.GlobalMemoryBindings.Count; + if (!Gen5SpirvTranslator.TryCompilePixelShader( + pixelState, + pixelEvaluation, + outputKind, + out var pixelShader, + out error, + globalBufferBase: 0, + totalGlobalBufferCount: totalGlobalBuffers, + imageBindingBase: 0) || + !Gen5SpirvTranslator.TryCompileVertexShader( + exportState, + exportEvaluation, + out var vertexShader, + out error, + globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count, + totalGlobalBufferCount: totalGlobalBuffers, + imageBindingBase: pixelEvaluation.ImageBindings.Count)) { - TraceAgc( - $"agc.shader_translated kind={drawKind} es=0x{exportShaderAddress:X16} " + - $"ps=0x{pixelShaderAddress:X16} vertices={vertexCount}"); + return false; + } + + compiled = (vertexShader.Spirv, pixelShader.Spirv); + DumpSpirv( + "vs", + exportShaderAddress, + exportStateFingerprint, + compiled.Vertex, + exportState.Program); + DumpSpirv( + "ps", + pixelShaderAddress, + pixelStateFingerprint, + compiled.Pixel, + pixelState.Program); + lock (_submitTraceGate) + { + _graphicsSpirvCache.TryAdd(shaderKey, compiled); + } + } + + var imageBindings = pixelEvaluation.ImageBindings + .Concat(exportEvaluation.ImageBindings); + var textures = new List( + pixelEvaluation.ImageBindings.Count + + exportEvaluation.ImageBindings.Count); + foreach (var binding in imageBindings) + { + if (!TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture)) + { + error = $"invalid texture descriptor at pc=0x{binding.Pc:X}"; + return false; + } + + TraceAgcShader( + $"agc.texture_binding ps=0x{pixelShaderAddress:X16} es=0x{exportShaderAddress:X16} " + + $"pc=0x{binding.Pc:X} op={binding.Opcode} storage={(Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode) ? 1 : 0)} " + + $"decoded={FormatTextureDescriptor(texture)} " + + $"raw={FormatShaderDwords(binding.ResourceDescriptor)} sampler={FormatShaderDwords(binding.SamplerDescriptor)}"); + textures.Add( + new TranslatedImageBinding( + texture, + Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode), + binding.MipLevel ?? 0, + binding.SamplerDescriptor)); + } + + var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings + .Concat(exportEvaluation.GlobalMemoryBindings) + .ToArray(); + IReadOnlyList vertexInputs = + exportEvaluation.VertexInputs ?? []; + state.UcRegisters.TryGetValue(VgtPrimitiveType, out var primitiveType); + draw = new TranslatedGuestDraw( + exportShaderAddress, + pixelShaderAddress, + primitiveType, + compiled.Vertex, + compiled.Pixel, + GetInterpolatedAttributeCount(pixelState), + vertexCount, + state.InstanceCount, + indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null, + textures, + globalMemoryBindings, + vertexInputs, + renderTargets, + CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault())); + return true; + } + + private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer( + CpuContext ctx, + SubmittedDcbState state, + uint indexCount) + { + if (state.IndexBufferAddress == 0 || indexCount == 0) + { + return null; + } + + var is32Bit = state.IndexSize != 0; + var bytesPerIndex = is32Bit ? sizeof(uint) : sizeof(ushort); + var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex); + var byteCount = checked((int)(indexCount * (uint)bytesPerIndex)); + var data = new byte[byteCount]; + var address = state.IndexBufferAddress + byteOffset; + return (ctx.Memory.TryRead(address, data) || + KernelMemoryCompatExports.TryReadTrackedLibcHeap(address, data)) + ? new VulkanGuestIndexBuffer(data, is32Bit) + : null; + } + + private static Gen5PixelOutputKind GetPixelOutputKind(uint numberType) => + numberType switch + { + 4 => Gen5PixelOutputKind.Uint, + 5 => Gen5PixelOutputKind.Sint, + _ => Gen5PixelOutputKind.Float, + }; + + private static bool HasPixelColorExport(Gen5ShaderState state, uint target) => + state.Program.Instructions.Any(instruction => + instruction.Control is Gen5ExportControl export && + export.Target == target && + export.EnableMask != 0); + + private static uint GetInterpolatedAttributeCount(Gen5ShaderState state) + { + var maxAttribute = -1; + foreach (var instruction in state.Program.Instructions) + { + if (instruction.Control is Gen5InterpolationControl interpolation) + { + maxAttribute = Math.Max(maxAttribute, (int)interpolation.Attribute); + } + } + + return (uint)(maxAttribute + 1); + } + + private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation) + { + const ulong offsetBasis = 14695981039346656037UL; + const ulong prime = 1099511628211UL; + var hash = offsetBasis; + foreach (var value in evaluation.ScalarRegisters) + { + hash = (hash ^ value) * prime; + } + + if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters) + { + hash = (hash ^ (computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue)) * prime; + hash = (hash ^ (computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue)) * prime; + hash = (hash ^ (computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue)) * prime; + hash = (hash ^ (computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue)) * prime; + } + + return hash; + } + + private static IReadOnlyList GetRenderTargets( + IReadOnlyDictionary registers) + { + var hasTargetMask = registers.TryGetValue(CbTargetMask, out var targetMask); + var targets = new List(ColorTargetCount); + for (uint slot = 0; slot < ColorTargetCount; slot++) + { + var baseRegister = CbColor0Base + slot * CbColorRegisterStride; + if (!registers.TryGetValue(baseRegister, out var baseLow) || + !registers.TryGetValue(CbColor0BaseExt + slot, out var baseHigh) || + !registers.TryGetValue(CbColor0Attrib2 + slot, out var attrib2) || + !registers.TryGetValue(CbColor0Attrib3 + slot, out var attrib3) || + !registers.TryGetValue(CbColor0Info + slot * CbColorRegisterStride, out var info)) + { + continue; + } + + var address = ((ulong)(baseHigh & 0xFFu) << 40) | ((ulong)baseLow << 8); + var writeMask = (targetMask >> ((int)slot * 4)) & 0xFu; + if (address == 0 || (hasTargetMask && writeMask == 0)) + { + continue; + } + + targets.Add(new RenderTargetDescriptor( + slot, + address, + ((attrib2 >> 14) & 0x3FFFu) + 1, + (attrib2 & 0x3FFFu) + 1, + (info >> 2) & 0x1Fu, + (info >> 8) & 0x7u, + (attrib3 >> 14) & 0x1Fu)); + } + + return targets; + } + + private static VulkanGuestRenderState CreateRenderState( + IReadOnlyDictionary registers, + RenderTargetDescriptor target) + { + var scissor = DecodeScissor(registers, target.Width, target.Height); + return new VulkanGuestRenderState( + DecodeBlendState(registers, target.Slot), + scissor, + DecodeViewport(registers, target.Width, target.Height, scissor)); + } + + private static VulkanGuestBlendState DecodeBlendState( + IReadOnlyDictionary registers, + uint slot) + { + var writeMask = 0xFu; + if (registers.TryGetValue(CbTargetMask, out var targetMask)) + { + writeMask = (targetMask >> checked((int)(slot * 4))) & 0xFu; + } + + registers.TryGetValue(CbBlend0Control + slot, out var control); + return new VulkanGuestBlendState( + ((control >> 30) & 1u) != 0, + control & 0x1Fu, + (control >> 8) & 0x1Fu, + (control >> 5) & 0x7u, + (control >> 16) & 0x1Fu, + (control >> 24) & 0x1Fu, + (control >> 21) & 0x7u, + ((control >> 29) & 1u) != 0, + writeMask == 0 ? 0xFu : writeMask); + } + + private static VulkanGuestRect? DecodeScissor( + IReadOnlyDictionary registers, + uint targetWidth, + uint targetHeight) + { + if (targetWidth == 0 || targetHeight == 0) + { + return new VulkanGuestRect(0, 0, 0, 0); + } + + var left = 0; + var top = 0; + var right = checked((int)Math.Min(targetWidth, int.MaxValue)); + var bottom = checked((int)Math.Min(targetHeight, int.MaxValue)); + + var windowOffsetX = 0; + var windowOffsetY = 0; + var enableWindowOffset = true; + if (registers.TryGetValue(PaScWindowScissorTl, out var windowScissorTl)) + { + enableWindowOffset = (windowScissorTl & 0x80000000u) == 0; + } + + if (enableWindowOffset && + registers.TryGetValue(PaScWindowOffset, out var windowOffset)) + { + windowOffsetX = (short)(windowOffset & 0xFFFFu); + windowOffsetY = (short)(windowOffset >> 16); + } + + IntersectScissorPair(registers, PaScScreenScissorTl, PaScScreenScissorBr, ref left, ref top, ref right, ref bottom); + IntersectScissorPair( + registers, + PaScWindowScissorTl, + PaScWindowScissorBr, + ref left, + ref top, + ref right, + ref bottom, + windowOffsetX, + windowOffsetY); + IntersectScissorPair( + registers, + PaScGenericScissorTl, + PaScGenericScissorBr, + ref left, + ref top, + ref right, + ref bottom, + windowOffsetX, + windowOffsetY); + var vportScissorEnabled = + !registers.TryGetValue(PaScModeCntl0, out var modeControl) || + ((modeControl >> 1) & 1u) != 0; + if (vportScissorEnabled) + { + IntersectScissorPair(registers, PaScVportScissor0Tl, PaScVportScissor0Br, ref left, ref top, ref right, ref bottom); + } + + left = Math.Clamp(left, 0, checked((int)targetWidth)); + top = Math.Clamp(top, 0, checked((int)targetHeight)); + right = Math.Clamp(right, left, checked((int)targetWidth)); + bottom = Math.Clamp(bottom, top, checked((int)targetHeight)); + + if (left == 0 && + top == 0 && + right == (int)targetWidth && + bottom == (int)targetHeight) + { + return null; + } + + return new VulkanGuestRect( + left, + top, + checked((uint)(right - left)), + checked((uint)(bottom - top))); + } + + private static VulkanGuestViewport? DecodeViewport( + IReadOnlyDictionary registers, + uint targetWidth, + uint targetHeight, + VulkanGuestRect? scissor) + { + if (targetWidth == 0 || targetHeight == 0) + { + return new VulkanGuestViewport(0, 0, 0, 0, 0, 1); + } + + var minDepth = 0f; + var maxDepth = 1f; + if (registers.TryGetValue(PaScVportZMin0, out var zMinBits) && + registers.TryGetValue(PaScVportZMax0, out var zMaxBits)) + { + var decodedMin = BitConverter.UInt32BitsToSingle(zMinBits); + var decodedMax = BitConverter.UInt32BitsToSingle(zMaxBits); + if (float.IsFinite(decodedMin) && + float.IsFinite(decodedMax) && + decodedMax > decodedMin) + { + minDepth = decodedMin; + maxDepth = decodedMax; + } + } + + if (TryDecodeFiniteFloat(registers, PaClVportXScale, out var xScale) && + TryDecodeFiniteFloat(registers, PaClVportXOffset, out var xOffset) && + TryDecodeFiniteFloat(registers, PaClVportYScale, out var yScale) && + TryDecodeFiniteFloat(registers, PaClVportYOffset, out var yOffset) && + xScale > 0f && + yScale != 0f) + { + return new VulkanGuestViewport( + xOffset - xScale, + yOffset - yScale, + xScale * 2f, + yScale * 2f, + minDepth, + maxDepth); + } + + if (scissor is not { } rect) + { + return minDepth == 0f && maxDepth == 1f + ? null + : new VulkanGuestViewport(0, 0, targetWidth, targetHeight, minDepth, maxDepth); + } + + return new VulkanGuestViewport( + rect.X, + rect.Y, + rect.Width, + rect.Height, + minDepth, + maxDepth); + } + + private static bool TryDecodeFiniteFloat( + IReadOnlyDictionary registers, + uint register, + out float value) + { + value = 0; + if (!registers.TryGetValue(register, out var bits)) + { + return false; + } + + value = BitConverter.UInt32BitsToSingle(bits); + return float.IsFinite(value); + } + + private static void IntersectScissorPair( + IReadOnlyDictionary registers, + uint tlRegister, + uint brRegister, + ref int left, + ref int top, + ref int right, + ref int bottom, + int offsetX = 0, + int offsetY = 0) + { + if (!TryDecodeScissorPair(registers, tlRegister, brRegister, out var pairLeft, out var pairTop, out var pairRight, out var pairBottom)) + { + return; + } + + pairLeft += offsetX; + pairTop += offsetY; + pairRight += offsetX; + pairBottom += offsetY; + + left = Math.Max(left, pairLeft); + top = Math.Max(top, pairTop); + right = Math.Min(right, pairRight); + bottom = Math.Min(bottom, pairBottom); + } + + private static bool TryDecodeScissorPair( + IReadOnlyDictionary registers, + uint tlRegister, + uint brRegister, + out int left, + out int top, + out int right, + out int bottom) + { + left = 0; + top = 0; + right = 0; + bottom = 0; + if (!registers.TryGetValue(tlRegister, out var tl) || + !registers.TryGetValue(brRegister, out var br)) + { + return false; + } + + left = (int)(tl & 0x7FFFu); + top = (int)((tl >> 16) & 0x7FFFu); + right = (int)(br & 0x7FFFu); + bottom = (int)((br >> 16) & 0x7FFFu); + return true; + } + + private static void TraceTranslatedGuestDraw( + CpuContext ctx, + SubmittedGpuState gpuState, + SubmittedDcbState state, + TranslatedGuestDraw draw, + uint psInputEna, + uint psInputAddr) + { + var targets = draw.RenderTargets.Count == 0 + ? "none" + : string.Join( + ',', + draw.RenderTargets.Select(target => + $"{target.Slot}:0x{target.Address:X16}:{target.Width}x{target.Height}:" + + $"fmt{target.Format}/num{target.NumberType}/tile{target.TileMode}")); + var probes = new Dictionary(); + var textures = string.Join( + ',', + draw.Textures.Select(binding => + { + var texture = binding.Descriptor; + var targetSlot = draw.RenderTargets + .FirstOrDefault(target => target.Address == texture.Address) + .Slot; + var target = draw.RenderTargets.Any(candidate => candidate.Address == texture.Address) + ? $"/rt{targetSlot}" + : string.Empty; + if (!probes.TryGetValue(texture.Address, out var probe)) + { + probe = ProbeTexture(ctx, texture); + probes.Add(texture.Address, probe); + } + + state.RenderTargetWriters.TryGetValue(texture.Address, out var sourceWriter); + gpuState.ComputeImageWriters.TryGetValue(texture.Address, out var computeWriter); + var writer = sourceWriter.Sequence >= computeWriter.Sequence && sourceWriter.Sequence != 0 + ? $"/writer={sourceWriter.Sequence}:" + + $"es0x{sourceWriter.ExportShaderAddress:X}:" + + $"ps0x{sourceWriter.PixelShaderAddress:X}:" + + $"v{sourceWriter.VertexCount}:prim0x{sourceWriter.PrimitiveType:X}" + : computeWriter.Sequence != 0 + ? $"/compute={computeWriter.Sequence}:" + + $"cs0x{computeWriter.ShaderAddress:X}:{computeWriter.Opcode}" + : "/writer=none"; + return + $"0x{texture.Address:X16}:{texture.Width}x{texture.Height}:" + + $"fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}" + + $"/storage={binding.IsStorage}{target}/{probe}{writer}"; + })); + var buffers = string.Join( + ',', + draw.GlobalMemoryBindings.Select((binding, index) => + $"{index}:0x{binding.BaseAddress:X16}:{binding.Data.Length}:" + + Convert.ToHexString(binding.Data.AsSpan(0, Math.Min(binding.Data.Length, 32))))); + var indices = draw.IndexBuffer is { } indexBuffer + ? $"{(indexBuffer.Is32Bit ? 32 : 16)}:" + + Convert.ToHexString(indexBuffer.Data.AsSpan(0, Math.Min(indexBuffer.Data.Length, 32))) + : "none"; + var vertexInputs = draw.VertexInputs.Count == 0 + ? "none" + : string.Join( + ',', + draw.VertexInputs.Select(input => + $"{input.Location}:pc=0x{input.Pc:X}:0x{input.BaseAddress:X16}" + + $":stride{input.Stride}:off{input.OffsetBytes}:c{input.ComponentCount}" + + $":fmt{input.DataFormat}/num{input.NumberFormat}")); + var scissor = draw.RenderState.Scissor is { } drawScissor + ? $"{drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height}" + : "full"; + var viewport = draw.RenderState.Viewport is { } drawViewport + ? $"{drawViewport.X:0.###},{drawViewport.Y:0.###}," + + $"{drawViewport.Width:0.###}x{drawViewport.Height:0.###}:" + + $"{drawViewport.MinDepth:0.###}-{drawViewport.MaxDepth:0.###}" + : "full"; + var rasterRegisters = new (string Name, uint Offset)[] + { + ("screen_tl", PaScScreenScissorTl), + ("screen_br", PaScScreenScissorBr), + ("window_off", PaScWindowOffset), + ("window_tl", PaScWindowScissorTl), + ("window_br", PaScWindowScissorBr), + ("generic_tl", PaScGenericScissorTl), + ("generic_br", PaScGenericScissorBr), + ("vport_tl", PaScVportScissor0Tl), + ("vport_br", PaScVportScissor0Br), + ("mode", PaScModeCntl0), + ("xscale", PaClVportXScale), + ("xoffset", PaClVportXOffset), + ("yscale", PaClVportYScale), + ("yoffset", PaClVportYOffset), + }; + var raster = string.Join( + ',', + rasterRegisters.Select(entry => + state.CxRegisters.TryGetValue(entry.Offset, out var value) + ? $"{entry.Name}=0x{value:X8}" + : $"{entry.Name}=missing")); + var blend = draw.RenderState.Blend; + TraceAgcShader( + $"agc.shader_draw es=0x{draw.ExportShaderAddress:X16} " + + $"ps=0x{draw.PixelShaderAddress:X16} spirv={draw.PixelSpirv.Length} " + + $"primitive=0x{draw.PrimitiveType:X} " + + $"blend={(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}/{blend.ColorDstFactor}/{blend.ColorFunc} " + + $"write_mask=0x{blend.WriteMask:X} scissor={scissor} viewport={viewport} " + + $"raster=[{raster}] " + + $"ps_ena=0x{psInputEna:X8} ps_addr=0x{psInputAddr:X8} " + + $"targets=[{targets}] textures=[{textures}] " + + $"buffers=[{buffers}] vertex=[{vertexInputs}] indices=[{indices}]"); + } + + private static IReadOnlyList CreateVulkanGuestDrawTextures( + CpuContext ctx, + IReadOnlyList bindings, + out int fallbackTextureCount) + { + var textures = new List(bindings.Count); + fallbackTextureCount = 0; + foreach (var binding in bindings) + { + if (TryCreateVulkanGuestDrawTexture( + ctx, + binding.Descriptor, + binding.IsStorage, + binding.MipLevel, + binding.SamplerDescriptor, + out var texture)) + { + textures.Add(texture); + if (texture.IsFallback) + { + fallbackTextureCount++; + } + } + } + + return textures; + } + + private static IReadOnlyList CreateVulkanGuestMemoryBuffers( + IReadOnlyList bindings) + { + var buffers = new VulkanGuestMemoryBuffer[bindings.Count]; + for (var index = 0; index < bindings.Count; index++) + { + buffers[index] = new VulkanGuestMemoryBuffer( + bindings[index].BaseAddress, + bindings[index].Data); + } + + return buffers; + } + + private static IReadOnlyList CreateVulkanGuestVertexBuffers( + IReadOnlyList bindings) + { + var buffers = new VulkanGuestVertexBuffer[bindings.Count]; + for (var index = 0; index < bindings.Count; index++) + { + var binding = bindings[index]; + buffers[index] = new VulkanGuestVertexBuffer( + binding.Location, + binding.ComponentCount, + binding.DataFormat, + binding.NumberFormat, + binding.BaseAddress, + binding.Stride, + binding.OffsetBytes, + binding.Data); + } + + return buffers; + } + + private static bool TryCreateVulkanGuestDrawTexture( + CpuContext ctx, + TextureDescriptor descriptor, + bool isStorage, + uint mipLevel, + IReadOnlyList samplerDescriptor, + out VulkanGuestDrawTexture texture) + { + texture = default!; + if (descriptor.Type != Gen5TextureType2D || + descriptor.Width == 0 || + descriptor.Height == 0 || + descriptor.Width > 8192 || + descriptor.Height > 8192) + { + texture = CreateFallbackGuestDrawTexture(isStorage); + return true; + } + + var sourceWidth = descriptor.TileMode == 0 + ? GetLinearTexturePitch( + Math.Max(descriptor.Width, descriptor.Pitch), + descriptor.Height, + descriptor.Format) + : descriptor.Width; + var sourceByteCount = GetTextureByteCount( + descriptor.Format, + sourceWidth, + descriptor.Height); + if (sourceByteCount == 0 || + sourceByteCount > MaxPresentedTextureBytes || + sourceByteCount > int.MaxValue) + { + texture = CreateFallbackGuestDrawTexture(isStorage); + return true; + } + + if (isStorage) + { + var initialPixels = Array.Empty(); + if (descriptor.Address != 0) + { + var storageSource = new byte[(int)sourceByteCount]; + if (ctx.Memory.TryRead(descriptor.Address, storageSource) && + storageSource.AsSpan().IndexOfAnyExcept((byte)0) >= 0) + { + initialPixels = storageSource; + } + } + + texture = new VulkanGuestDrawTexture( + descriptor.Address, + descriptor.Width, + descriptor.Height, + descriptor.Format, + descriptor.NumberType, + initialPixels, + IsFallback: descriptor.Address == 0, + IsStorage: true, + MipLevels: descriptor.MipLevels, + MipLevel: mipLevel, + Pitch: sourceWidth, + TileMode: descriptor.TileMode, + DstSelect: descriptor.DstSelect, + Sampler: ToVulkanSampler(samplerDescriptor)); + return true; + } + + var source = new byte[(int)sourceByteCount]; + if (!ctx.Memory.TryRead(descriptor.Address, source)) + { + texture = CreateFallbackGuestDrawTexture(isStorage); + return true; + } + + var nonZero = 0; + for (var i = 0; i < source.Length; i++) + { + if (source[i] != 0) + { + nonZero++; + if (nonZero >= 64) + { + break; + } + } + } + + TraceAgcShader( + $"agc.texture_source addr=0x{descriptor.Address:X16} " + + $"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " + + $"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " + + $"dst=0x{descriptor.DstSelect:X3} " + + $"bytes={source.Length} nonzero64={nonZero}"); + + var rgba = source; + texture = new VulkanGuestDrawTexture( + descriptor.Address, + descriptor.Width, + descriptor.Height, + descriptor.Format, + descriptor.NumberType, + rgba, + IsFallback: false, + IsStorage: isStorage, + MipLevels: descriptor.MipLevels, + MipLevel: mipLevel, + Pitch: sourceWidth, + TileMode: descriptor.TileMode, + DstSelect: descriptor.DstSelect, + Sampler: ToVulkanSampler(samplerDescriptor)); + return true; + } + + private static VulkanGuestDrawTexture CreateFallbackGuestDrawTexture(bool isStorage = false) => + new( + 0, + 1, + 1, + 56, + NumberType: 0, + [0, 0, 0, 255], + IsFallback: true, + IsStorage: isStorage, + MipLevels: 1, + MipLevel: 0); + + private static VulkanGuestSampler ToVulkanSampler(IReadOnlyList descriptor) => + descriptor.Count >= 4 + ? new VulkanGuestSampler( + descriptor[0], + descriptor[1], + descriptor[2], + descriptor[3]) + : default; + + private static byte[] ConvertRgba16FloatToRgba8(ReadOnlySpan source, uint width, uint height) + { + var destination = new byte[checked((int)((ulong)width * height * 4))]; + var pixelCount = destination.Length / 4; + for (var pixel = 0; pixel < pixelCount; pixel++) + { + var sourceOffset = pixel * 8; + var destinationOffset = pixel * 4; + destination[destinationOffset + 0] = HalfToByte(BinaryPrimitives.ReadUInt16LittleEndian(source[sourceOffset..])); + destination[destinationOffset + 1] = HalfToByte(BinaryPrimitives.ReadUInt16LittleEndian(source[(sourceOffset + 2)..])); + destination[destinationOffset + 2] = HalfToByte(BinaryPrimitives.ReadUInt16LittleEndian(source[(sourceOffset + 4)..])); + destination[destinationOffset + 3] = HalfToByte(BinaryPrimitives.ReadUInt16LittleEndian(source[(sourceOffset + 6)..])); + } + + return destination; + } + + private static byte HalfToByte(ushort bits) + { + var value = (float)BitConverter.UInt16BitsToHalf(bits); + if (!float.IsFinite(value)) + { + return 0; + } + + return (byte)Math.Clamp((int)MathF.Round(value * 255.0f), 0, 255); + } + + private static bool TryReadComputeDispatch( + CpuContext ctx, + SubmittedDcbState state, + ulong packetAddress, + uint packetLength, + uint opcode, + out ComputeDispatch dispatch) + { + dispatch = default; + ulong dimensionsAddress; + uint initiator; + if (opcode == ItDispatchDirect) + { + if (packetLength < 5 || + !TryReadUInt32(ctx, packetAddress + 16, out initiator)) + { + return false; + } + + dimensionsAddress = packetAddress + 4; + } + else if (packetLength >= 4) + { + if (!TryReadUInt64(ctx, packetAddress + 4, out dimensionsAddress) || + !TryReadUInt32(ctx, packetAddress + 12, out initiator)) + { + return false; + } + } + else + { + if (packetLength < 3 || + state.IndirectArgsAddress == 0 || + !TryReadUInt32(ctx, packetAddress + 4, out var dataOffset) || + !TryReadUInt32(ctx, packetAddress + 8, out initiator)) + { + return false; + } + + dimensionsAddress = state.IndirectArgsAddress + dataOffset; + } + + if ((initiator & 1) == 0 || + !TryReadUInt32(ctx, dimensionsAddress, out var groupCountX) || + !TryReadUInt32(ctx, dimensionsAddress + 4, out var groupCountY) || + !TryReadUInt32(ctx, dimensionsAddress + 8, out var groupCountZ) || + groupCountX == 0 || + groupCountY == 0 || + groupCountZ == 0) + { + return false; + } + + dispatch = new ComputeDispatch(groupCountX, groupCountY, groupCountZ); + return true; + } + + private static void ObserveComputeDispatch( + CpuContext ctx, + SubmittedGpuState gpuState, + SubmittedDcbState state, + ComputeDispatch dispatch) + { + if (!TryGetShaderAddress( + state.ShRegisters, + ComputePgmLo, + ComputePgmHi, + out var shaderAddress)) + { + return; + } + + var sequence = ++gpuState.WorkSequence; + ulong shaderHeader; + lock (_submitTraceGate) + { + _shaderHeadersByCode.TryGetValue(shaderAddress, out shaderHeader); + } + + var computeSystemRegisters = DecodeComputeSystemRegisters(state.ShRegisters); + if (!Gen5ShaderTranslator.TryCreateState( + ctx, + shaderAddress, + shaderHeader, + state.ShRegisters, + ComputeUserDataRegister, + out var shaderState, + out var error, + computeSystemRegisters) || + !Gen5ShaderScalarEvaluator.TryEvaluate( + ctx, + shaderState, + out var evaluation, + out error)) + { + lock (_submitTraceGate) + { + if (_tracedComputeShaders.Add(shaderAddress)) + { + TraceAgcShader( + $"agc.compute_shader cs=0x{shaderAddress:X16} error={error}"); + } + } + + return; + } + + var bindings = evaluation.ImageBindings; + var descriptions = new List(bindings.Count); + var translatedBindings = new List(bindings.Count); + var hasStorageBinding = false; + foreach (var binding in bindings) + { + var isStorage = Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode); + var descriptorValid = TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture); + if (!descriptorValid) + { + texture = CreateFallbackTextureDescriptor(binding.ResourceDescriptor); + } + + translatedBindings.Add( + new TranslatedImageBinding( + texture, + isStorage, + binding.MipLevel ?? 0, + binding.SamplerDescriptor)); + hasStorageBinding |= isStorage; + + var descriptorState = descriptorValid ? string.Empty : "/invalid-desc"; + descriptions.Add( + $"{binding.Opcode}@0x{binding.Pc:X}:" + + $"0x{texture.Address:X16}:{texture.Width}x{texture.Height}:" + + $"fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}" + + $"{descriptorState}/{ProbeTexture(ctx, texture)}"); + if (isStorage && descriptorValid && texture.Address != 0) + { + gpuState.ComputeImageWriters[texture.Address] = new ComputeImageWriter( + sequence, + shaderAddress, + binding.Opcode); + + TraceAgcShader( + $"agc.compute_writer addr=0x{texture.Address:X16} " + + $"fmt={texture.Format} num={texture.NumberType} tile={texture.TileMode} " + + $"size={texture.Width}x{texture.Height} " + + $"cs=0x{shaderAddress:X16} op={binding.Opcode}"); + } + } + + var localSizeX = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadX); + var localSizeY = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadY); + var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ); + var gpuDispatch = false; + var computeError = string.Empty; + if (hasStorageBinding && + (ulong)localSizeX * localSizeY * localSizeZ <= 1024) + { + var shaderKey = ( + shaderAddress, + ComputeShaderStateFingerprint(evaluation), + localSizeX, + localSizeY, + localSizeZ); + byte[] computeSpirv; + lock (_submitTraceGate) + { + _computeSpirvCache.TryGetValue(shaderKey, out computeSpirv!); + } + + if (computeSpirv is null && + Gen5SpirvTranslator.TryCompileComputeShader( + shaderState, + evaluation, + localSizeX, + localSizeY, + localSizeZ, + out var compiledCompute, + out computeError)) + { + computeSpirv = compiledCompute.Spirv; + DumpSpirv( + "cs", + shaderAddress, + shaderKey.Item2, + computeSpirv, + shaderState.Program); + } + + if (computeSpirv is not null) + { + lock (_submitTraceGate) + { + _computeSpirvCache.TryAdd(shaderKey, computeSpirv); + } + + var textures = CreateVulkanGuestDrawTextures( + ctx, + translatedBindings, + out _); + var globalMemoryBuffers = + CreateVulkanGuestMemoryBuffers(evaluation.GlobalMemoryBindings); + VulkanVideoPresenter.SubmitComputeDispatch( + shaderAddress, + computeSpirv, + textures, + globalMemoryBuffers, + dispatch.GroupCountX, + dispatch.GroupCountY, + dispatch.GroupCountZ); + gpuDispatch = true; + } + } + + const int blitCount = 0; + + lock (_submitTraceGate) + { + if (_tracedComputeShaders.Add(shaderAddress)) + { + TraceAgcShader( + $"agc.compute_shader cs=0x{shaderAddress:X16} " + + $"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " + + $"local={localSizeX}x{localSizeY}x{localSizeZ} " + + $"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " + + $"gpu={gpuDispatch} blits={blitCount}" + + (computeError.Length == 0 ? string.Empty : $" error={computeError}") + + $" bindings=[{string.Join(',', descriptions)}]"); } } } + private static Gen5ComputeSystemRegisters DecodeComputeSystemRegisters( + IReadOnlyDictionary registers) + { + registers.TryGetValue(ComputePgmRsrc2, out var rsrc2); + var nextRegister = (rsrc2 >> 1) & 0x1Fu; + uint? workGroupX = null; + uint? workGroupY = null; + uint? workGroupZ = null; + uint? threadGroupSize = null; + + if ((rsrc2 & (1u << 7)) != 0) + { + workGroupX = nextRegister++; + } + + if ((rsrc2 & (1u << 8)) != 0) + { + workGroupY = nextRegister++; + } + + if ((rsrc2 & (1u << 9)) != 0) + { + workGroupZ = nextRegister++; + } + + if ((rsrc2 & (1u << 10)) != 0) + { + threadGroupSize = nextRegister++; + } + + return new Gen5ComputeSystemRegisters( + workGroupX, + workGroupY, + workGroupZ, + threadGroupSize); + } + + private static string DescribeComputeSystemRegisters(Gen5ComputeSystemRegisters registers) => + $"x={DescribeRegister(registers.WorkGroupXRegister)}," + + $"y={DescribeRegister(registers.WorkGroupYRegister)}," + + $"z={DescribeRegister(registers.WorkGroupZRegister)}," + + $"size={DescribeRegister(registers.ThreadGroupSizeRegister)}"; + + private static string DescribeRegister(uint? register) => + register.HasValue ? $"s{register.Value}" : "-"; + + private static uint SelectExportUserDataRegister( + IReadOnlyDictionary registers) + { + if (HasUserDataRange(registers, GsUserDataRegister)) + { + return GsUserDataRegister; + } + + if (HasUserDataRange(registers, EsUserDataRegister)) + { + return EsUserDataRegister; + } + + if (HasUserDataRange(registers, VsUserDataRegister)) + { + return VsUserDataRegister; + } + + var esValues = CountUserDataValues(registers, EsUserDataRegister); + var vsValues = CountUserDataValues(registers, VsUserDataRegister); + return esValues == 0 && vsValues != 0 + ? VsUserDataRegister + : EsUserDataRegister; + } + + private static bool HasUserDataRange( + IReadOnlyDictionary registers, + uint startRegister) + { + for (var index = 0u; index < 16; index++) + { + if (registers.ContainsKey(startRegister + index)) + { + return true; + } + } + + return false; + } + + private static int CountUserDataValues( + IReadOnlyDictionary registers, + uint startRegister) + { + var count = 0; + for (var index = 0u; index < 16; index++) + { + count += registers.TryGetValue(startRegister + index, out var value) && + value != 0 + ? 1 + : 0; + } + + return count; + } + + private static uint GetComputeLocalSize( + IReadOnlyDictionary registers, + uint register) + { + return registers.TryGetValue(register, out var value) + ? Math.Max(value & 0x3FFu, 1u) + : 1u; + } + + private static int TryApplySoftwareComputeBlits( + CpuContext ctx, + ulong shaderAddress, + IReadOnlyList<(Gen5ImageBinding Binding, TextureDescriptor Texture)> bindings) + { + var blits = 0; + TextureDescriptor? source = null; + foreach (var (binding, texture) in bindings) + { + if (binding.Opcode.StartsWith("ImageStore", StringComparison.Ordinal)) + { + if (source is { } sourceTexture && + TrySoftwareTextureBlit(ctx, sourceTexture, texture, out var fingerprint)) + { + blits++; + var key = (shaderAddress, sourceTexture.Address, texture.Address); + lock (_softwarePresenterGate) + { + if (!_softwareComputeBlitFingerprints.TryGetValue(key, out var previous) || + previous != fingerprint) + { + _softwareComputeBlitFingerprints[key] = fingerprint; + TraceAgcShader( + $"agc.compute_blit cs=0x{shaderAddress:X16} " + + $"src=0x{sourceTexture.Address:X16}:{sourceTexture.Width}x{sourceTexture.Height}:fmt{sourceTexture.Format}/num{sourceTexture.NumberType}/tile{sourceTexture.TileMode} " + + $"dst=0x{texture.Address:X16}:{texture.Width}x{texture.Height}:fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode} " + + $"fingerprint=0x{fingerprint:X16}"); + } + } + } + else if (source is { } cachedSourceTexture && + VulkanVideoPresenter.TrySubmitGuestImageBlit( + cachedSourceTexture.Address, + cachedSourceTexture.Width, + cachedSourceTexture.Height, + cachedSourceTexture.Format, + texture.Address, + texture.Width, + texture.Height, + texture.Format)) + { + blits++; + TraceAgcShader( + $"agc.compute_gpu_blit cs=0x{shaderAddress:X16} " + + $"src=0x{cachedSourceTexture.Address:X16}:{cachedSourceTexture.Width}x{cachedSourceTexture.Height}:fmt{cachedSourceTexture.Format}/num{cachedSourceTexture.NumberType}/tile{cachedSourceTexture.TileMode} " + + $"dst=0x{texture.Address:X16}:{texture.Width}x{texture.Height}:fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}"); + } + + continue; + } + + if (binding.Opcode.StartsWith("Image", StringComparison.Ordinal)) + { + source = texture; + } + } + + return blits; + } + + private static bool TrySoftwareTextureBlit( + CpuContext ctx, + TextureDescriptor source, + TextureDescriptor destination, + out ulong fingerprint) + { + fingerprint = 0; + var bytesPerTexel = GetTextureBytesPerTexel(source.Format); + if (bytesPerTexel == 0 || + bytesPerTexel != GetTextureBytesPerTexel(destination.Format) || + source.Type != Gen5TextureType2D || + destination.Type != Gen5TextureType2D || + source.Width == 0 || + source.Height == 0 || + destination.Width == 0 || + destination.Height == 0 || + source.Width > 8192 || + source.Height > 8192 || + destination.Width > 8192 || + destination.Height > 8192) + { + return false; + } + + var sourceBytes = checked((ulong)source.Width * source.Height * bytesPerTexel); + var destinationBytes = checked((ulong)destination.Width * destination.Height * bytesPerTexel); + if (sourceBytes == 0 || + destinationBytes == 0 || + sourceBytes > MaxPresentedTextureBytes || + destinationBytes > MaxPresentedTextureBytes || + sourceBytes > int.MaxValue || + destinationBytes > int.MaxValue) + { + return false; + } + + var sourceData = new byte[(int)sourceBytes]; + if (!ctx.Memory.TryRead(source.Address, sourceData)) + { + return false; + } + + var nonzero = 0; + foreach (var value in sourceData) + { + if (value != 0) + { + nonzero++; + break; + } + } + + if (nonzero == 0) + { + return false; + } + + var destinationData = new byte[(int)destinationBytes]; + for (uint y = 0; y < destination.Height; y++) + { + var sourceY = (uint)(((ulong)y * source.Height) / destination.Height); + for (uint x = 0; x < destination.Width; x++) + { + var sourceX = (uint)(((ulong)x * source.Width) / destination.Width); + var sourceOffset = checked((int)(((ulong)sourceY * source.Width + sourceX) * bytesPerTexel)); + var destinationOffset = checked((int)(((ulong)y * destination.Width + x) * bytesPerTexel)); + sourceData.AsSpan(sourceOffset, (int)bytesPerTexel) + .CopyTo(destinationData.AsSpan(destinationOffset, (int)bytesPerTexel)); + } + } + + if (!ctx.Memory.TryWrite(destination.Address, destinationData)) + { + return false; + } + + fingerprint = ComputeFingerprint(destinationData); + return true; + } + + private static string ProbeTexture(CpuContext ctx, TextureDescriptor texture) + { + if (texture.Width == 0 || + texture.Height == 0) + { + return "probe=unsupported"; + } + + var totalBytes = GetTextureByteCount( + texture.Format, + texture.Width, + texture.Height); + if (totalBytes == 0) + { + return "probe=unsupported"; + } + + const int sampleCount = 32; + const int sampleSize = 256; + var sample = new byte[sampleSize]; + var reads = 0; + var nonzero = 0; + const ulong offsetBasis = 14695981039346656037UL; + const ulong prime = 1099511628211UL; + var hash = offsetBasis; + for (var index = 0; index < sampleCount; index++) + { + var maxOffset = totalBytes > sampleSize ? totalBytes - sampleSize : 0; + var offset = sampleCount == 1 + ? 0 + : maxOffset * (ulong)index / (sampleCount - 1); + if (!ctx.Memory.TryRead(texture.Address + offset, sample)) + { + continue; + } + + reads++; + foreach (var value in sample) + { + if (value != 0) + { + nonzero++; + } + + hash = (hash ^ value) * prime; + } + } + + var bytesPerTexel = GetTextureBytesPerTexel(texture.Format); + var texels = bytesPerTexel is > 0 and <= 16 + ? string.Join( + '/', + ProbeTextureTexel(ctx, texture.Address, (int)bytesPerTexel), + ProbeTextureTexel( + ctx, + texture.Address + + (((ulong)(texture.Height / 2) * texture.Width) + (texture.Width / 2)) * + bytesPerTexel, + (int)bytesPerTexel), + ProbeTextureTexel( + ctx, + texture.Address + totalBytes - bytesPerTexel, + (int)bytesPerTexel)) + : "unsupported"; + return $"probe={reads}/{sampleCount}:{nonzero}:0x{hash:X16}:texels={texels}"; + } + + private static string ProbeTextureTexel(CpuContext ctx, ulong address, int size) + { + var texel = new byte[size]; + return ctx.Memory.TryRead(address, texel) + ? Convert.ToHexString(texel) + : "unreadable"; + } + + private static ulong GetTextureBytesPerTexel(uint format) => + format switch + { + 1 => 1UL, + 2 => 2UL, + 3 => 2UL, + 4 => 4UL, + 5 => 4UL, + 6 => 4UL, + 7 => 4UL, + 9 => 4UL, + 10 => 4UL, + 11 => 8UL, + 12 => 8UL, + 13 => 12UL, + 14 => 16UL, + 20 => 4UL, + 22 => 8UL, + 29 => 4UL, + 36 => 1UL, + 49 => 1UL, + Gen5TextureFormatR8G8B8A8Unorm => 4UL, + 62 => 4UL, + 64 => 4UL, + Gen5TextureFormatR16G16B16A16Float => 8UL, + 75 => 8UL, + _ => 0UL, + }; + + private static ulong GetTextureByteCount(uint format, uint width, uint height) + { + var bytesPerTexel = GetTextureBytesPerTexel(format); + if (bytesPerTexel != 0) + { + return checked((ulong)width * height * bytesPerTexel); + } + + var blockBytes = format switch + { + 169 or 170 => 8UL, + 171 or 172 or 173 or 174 or 175 or 176 or + 177 or 178 or 179 or 180 or 181 or 182 => 16UL, + _ => 0UL, + }; + return blockBytes == 0 + ? 0 + : checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes); + } + + private static uint GetLinearTexturePitch(uint pitch, uint height, uint format) + { + var bytesPerTexel = GetTextureBytesPerTexel(format); + if (bytesPerTexel == 0 || height == 0) + { + return pitch; + } + + var pitchAlignment = Math.Max(8UL, 64UL / bytesPerTexel); + var alignedPitch = AlignUp(pitch, pitchAlignment); + var sliceAlignment = Math.Max(64UL, 256UL / bytesPerTexel); + while ((alignedPitch * height) % sliceAlignment != 0) + { + alignedPitch += pitchAlignment; + } + + return checked((uint)alignedPitch); + } + + private static ulong AlignUp(ulong value, ulong alignment) => + (value + alignment - 1) & ~(alignment - 1); + private static void TraceShaderTranslationMiss( + CpuContext ctx, + SubmittedDcbState state, uint vertexCount, bool hasExportShader, ulong exportShaderAddress, @@ -2338,19 +4395,198 @@ public static class AgcExports bool hasPsInputEna, uint psInputEna, bool hasPsInputAddr, - uint psInputAddr) + uint psInputAddr, + string? translationError = null) { - if (!ShouldTraceHotPath(ref _shaderTranslationMissTraceCount)) + var firstFailure = false; + if (!string.IsNullOrEmpty(translationError)) + { + lock (_submitTraceGate) + { + firstFailure = _tracedShaderFailures.Add( + (pixelShaderAddress, translationError)); + } + } + + if (!firstFailure && + !ShouldTraceHotPath(ref _shaderTranslationMissTraceCount)) { return; } - TraceAgc( + if ((!hasPixelShader || !hasPsInputEna || !hasPsInputAddr) && + TryMarkMissingPixelShaderBindingsTrace()) + { + TraceAgcShader( + $"agc.shader_register_candidates " + + DescribeShaderRegisterCandidates(ctx, state.ShRegisters)); + } + + var shaderDecode = string.Empty; + if (hasExportShader && hasPixelShader) + { + var shouldDescribe = false; + ulong exportShaderHeader; + ulong pixelShaderHeader; + lock (_submitTraceGate) + { + shouldDescribe = _tracedShaderDecodePairs.Add((exportShaderAddress, pixelShaderAddress)); + _shaderHeadersByCode.TryGetValue(exportShaderAddress, out exportShaderHeader); + _shaderHeadersByCode.TryGetValue(pixelShaderAddress, out pixelShaderHeader); + } + + if (shouldDescribe) + { + shaderDecode = $" decode={Gen5ShaderTranslator.Describe(ctx, exportShaderAddress, pixelShaderAddress)}"; + TraceAgcShader( + $"agc.shader_words es=0x{exportShaderAddress:X16} " + + Gen5ShaderTranslator.DescribeWords(ctx, exportShaderAddress)); + if (Gen5ShaderTranslator.TryCreateState( + ctx, + exportShaderAddress, + exportShaderHeader, + state.ShRegisters, + SelectExportUserDataRegister(state.ShRegisters), + out var exportState, + out _, + userDataScalarRegisterBase: NggUserDataScalarRegisterBase) && + Gen5ShaderTranslator.TryCreateState( + ctx, + pixelShaderAddress, + pixelShaderHeader, + state.ShRegisters, + PsTextureUserDataRegister, + out var pixelState, + out _)) + { + TraceAgcShader( + $"agc.shader_state es=0x{exportShaderAddress:X16} " + + Gen5ShaderTranslator.DescribeState(exportState)); + TraceAgcShader( + $"agc.shader_state ps=0x{pixelShaderAddress:X16} " + + Gen5ShaderTranslator.DescribeState(pixelState)); + if (Gen5ShaderScalarEvaluator.TryEvaluate( + ctx, + pixelState, + out var evaluation, + out var bindingError)) + { + foreach (var binding in evaluation.ImageBindings) + { + TraceAgcShader( + $"agc.shader_binding ps=0x{pixelShaderAddress:X16} " + + $"pc=0x{binding.Pc:X} op={binding.Opcode} " + + $"resource={FormatShaderDwords(binding.ResourceDescriptor)} " + + $"sampler={FormatShaderDwords(binding.SamplerDescriptor)}"); + } + + foreach (var binding in evaluation.GlobalMemoryBindings) + { + TraceAgcShader( + $"agc.shader_global_binding ps=0x{pixelShaderAddress:X16} " + + $"saddr=s{binding.ScalarAddress} " + + $"base=0x{binding.BaseAddress:X16} bytes={binding.Data.Length} " + + $"pcs={string.Join(',', binding.InstructionPcs.Select(pc => $"0x{pc:X}"))}"); + } + + if (Gen5SpirvTranslator.TryCompilePixelShader( + pixelState, + evaluation, + Gen5PixelOutputKind.Float, + out var compiledPixel, + out var compileError)) + { + TraceAgcShader( + $"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " + + $"bytes={compiledPixel.Spirv.Length} bindings={evaluation.ImageBindings.Count} " + + $"global_buffers={evaluation.GlobalMemoryBindings.Count}"); + } + else + { + TraceAgcShader( + $"agc.shader_spirv_error ps=0x{pixelShaderAddress:X16} " + + compileError.ReplaceLineEndings(" ")); + } + } + else + { + TraceAgcShader( + $"agc.shader_binding_error ps=0x{pixelShaderAddress:X16} " + + bindingError); + } + } + } + } + + TraceAgcShader( $"agc.shader_translate_miss vertices={vertexCount} " + $"es={(hasExportShader ? $"0x{exportShaderAddress:X16}" : "missing")} " + $"ps={(hasPixelShader ? $"0x{pixelShaderAddress:X16}" : "missing")} " + $"ps_ena={(hasPsInputEna ? $"0x{psInputEna:X8}" : "missing")} " + - $"ps_addr={(hasPsInputAddr ? $"0x{psInputAddr:X8}" : "missing")}"); + $"ps_addr={(hasPsInputAddr ? $"0x{psInputAddr:X8}" : "missing")}" + + (string.IsNullOrEmpty(translationError) ? string.Empty : $" error={translationError}") + + shaderDecode); + } + + private static bool TryMarkMissingPixelShaderBindingsTrace() + { + lock (_submitTraceGate) + { + if (_tracedMissingPixelShaderBindings) + { + return false; + } + + _tracedMissingPixelShaderBindings = true; + return true; + } + } + + private static string DescribeShaderRegisterCandidates( + CpuContext ctx, + IReadOnlyDictionary registers) + { + var candidates = new List<(uint Register, ulong Address, ulong Header)>(); + lock (_submitTraceGate) + { + foreach (var (register, lo) in registers) + { + if (!registers.TryGetValue(register + 1, out var hi)) + { + continue; + } + + var address = ((ulong)hi << 40) | ((ulong)lo << 8); + if (address != 0 && + _shaderHeadersByCode.TryGetValue(address, out var header)) + { + candidates.Add((register, address, header)); + } + } + } + + if (candidates.Count == 0) + { + return "none"; + } + + return string.Join( + ',', + candidates + .OrderBy(candidate => candidate.Register) + .Take(16) + .Select(candidate => + { + var type = TryReadByte( + ctx, + candidate.Header + ShaderTypeOffset, + out var shaderType) + ? shaderType.ToString() + : "?"; + return + $"sh[0x{candidate.Register:X}/0x{candidate.Register + 1:X}]=" + + $"0x{candidate.Address:X16}:type{type}"; + })); } private static bool TryGetShaderAddress( @@ -2403,21 +4639,90 @@ public static class AgcExports } } - var address = ((((ulong)fields[1] << 32) | fields[0]) & 0xFF_FFFF_FFFFUL) << 8; + return TryDecodeTextureDescriptor(fields.ToArray(), out descriptor); + } + + private static bool TryDecodeTextureDescriptor( + IReadOnlyList fields, + out TextureDescriptor descriptor) + { + descriptor = default; + if (fields.Count < 4) + { + return false; + } + + // GFX10/RDNA2 T# layout: WIDTH is split across word1[31:30] (lo 2 bits) + // and word2[11:0] (hi 12 bits); FORMAT is the combined 9-bit field at + // word1[28:20]. Verified against Kyty's decode of the same game + // descriptors (fmt=56=8_8_8_8_UNORM, extent 1280x720, sw_mode 27). + // GNM T# exposes a 38-bit baseaddr256 field, but RPCSX and the + // Demon's Souls descriptors both show that only the low 32 bits are + // part of the guest GPU VA. The upper baseaddr bits carry resource + // metadata and produce bogus addresses such as 0x00003804... if used. + var address = ((ulong)(uint)((((ulong)fields[1] << 32) | fields[0]) & 0x3F_FFFF_FFFFUL)) << 8; var width = (((fields[1] >> 30) & 0x3u) | ((fields[2] & 0xFFFu) << 2)) + 1; var height = ((fields[2] >> 14) & 0x3FFFu) + 1; var format = (fields[1] >> 20) & 0x1FFu; + var numberType = (fields[1] >> 26) & 0xFu; var tileMode = (fields[3] >> 20) & 0x1Fu; var type = (fields[3] >> 28) & 0xFu; + var baseLevel = (fields[3] >> 12) & 0xFu; + var lastLevel = (fields[3] >> 16) & 0xFu; + var pitch = fields.Count >= 5 + ? ((fields[4] >> 13) & 0x3FFFu) + 1 + : width; + var dstSelect = fields[3] & 0xFFFu; if (address == 0 || width == 0 || height == 0) { return false; } - descriptor = new TextureDescriptor(address, width, height, format, tileMode, type); + descriptor = new TextureDescriptor( + address, + width, + height, + format, + numberType, + tileMode, + type, + baseLevel, + lastLevel, + pitch, + dstSelect); return true; } + private static TextureDescriptor CreateFallbackTextureDescriptor(IReadOnlyList fields) + { + var format = Gen5TextureFormatR8G8B8A8Unorm; + var numberType = 0u; + var tileMode = 0u; + if (fields.Count >= 4) + { + format = (fields[1] >> 20) & 0x1FFu; + numberType = (fields[1] >> 26) & 0xFu; + tileMode = (fields[3] >> 20) & 0x1Fu; + if (format == 0) + { + format = Gen5TextureFormatR8G8B8A8Unorm; + } + } + + return new TextureDescriptor( + Address: 0, + Width: 1, + Height: 1, + Format: format, + NumberType: numberType, + TileMode: tileMode, + Type: Gen5TextureType2D, + BaseLevel: 0, + LastLevel: 0, + Pitch: 1, + DstSelect: 0xFAC); + } + private static bool TrySoftwarePresent( CpuContext ctx, TextureDescriptor source, @@ -2518,7 +4823,7 @@ public static class AgcExports VideoOutExports.SubmitHostRgbaFrame(sourceBytes, source.Width, source.Height); TraceAgc( - $"agc.software_presenter src=0x{source.Address:X16} {source.Width}x{source.Height} fmt={source.Format} " + + $"agc.software_presenter src=0x{source.Address:X16} {source.Width}x{source.Height} fmt={source.Format}/num{source.NumberType} " + $"dst=0x{destination.Address:X16} {destination.Width}x{destination.Height} fingerprint=0x{fingerprint:X16}"); return true; } @@ -3058,6 +5363,40 @@ public static class AgcExports return true; } + private static bool TryFillGuestMemory( + CpuContext ctx, + uint value, + ulong destinationAddress, + uint byteCount) + { + var buffer = new byte[Math.Min(byteCount, 64u * 1024u)]; + Span encoded = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(encoded, value); + for (var offset = 0; offset < buffer.Length; offset += sizeof(uint)) + { + var remaining = Math.Min(sizeof(uint), buffer.Length - offset); + encoded[..remaining].CopyTo(buffer.AsSpan(offset, remaining)); + } + + ulong destinationOffset = 0; + while (destinationOffset < byteCount) + { + var chunkLength = (int)Math.Min( + (ulong)buffer.Length, + byteCount - destinationOffset); + if (!ctx.Memory.TryWrite( + destinationAddress + destinationOffset, + buffer.AsSpan(0, chunkLength))) + { + return false; + } + + destinationOffset += (uint)chunkLength; + } + + return true; + } + private static bool ShouldTraceHotPath(ref long counter) { var count = Interlocked.Increment(ref counter); @@ -3074,6 +5413,68 @@ public static class AgcExports Console.Error.WriteLine($"[LOADER][TRACE] {message}"); } + private static void TraceAgcShader(string message) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) && + !string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"), "1", StringComparison.Ordinal)) + { + return; + } + + Console.Error.WriteLine($"[LOADER][TRACE] {message}"); + } + + private static string FormatShaderDwords(IReadOnlyList values) => + values.Count == 0 + ? "none" + : string.Join(',', values.Select(static value => $"{value:X8}")); + + private static string FormatTextureDescriptor(TextureDescriptor descriptor) => + $"addr=0x{descriptor.Address:X16} {descriptor.Width}x{descriptor.Height} " + + $"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " + + $"type={descriptor.Type} levels={descriptor.BaseLevel}-{descriptor.LastLevel} " + + $"pitch={descriptor.Pitch} dst=0x{descriptor.DstSelect:X3}"; + + private static void DumpSpirv( + string stage, + ulong shaderAddress, + ulong stateFingerprint, + byte[] spirv, + Gen5ShaderProgram program) + { + if (spirv.Length == 0 || + !string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_DUMP_SPIRV"), + "1", + StringComparison.Ordinal)) + { + return; + } + + var directory = Path.Combine(AppContext.BaseDirectory, "shader-dumps"); + Directory.CreateDirectory(directory); + var name = $"{shaderAddress:X16}-{stateFingerprint:X16}.{stage}"; + File.WriteAllBytes(Path.Combine(directory, $"{name}.spv"), spirv); + + var lines = new List(program.Instructions.Count + 2) + { + $"address=0x{program.Address:X16}", + "pc words opcode destinations <- sources control", + }; + foreach (var instruction in program.Instructions) + { + lines.Add( + $"0x{instruction.Pc:X4} " + + $"{string.Join('_', instruction.Words.Select(static word => $"{word:X8}"))} " + + $"{instruction.Opcode} " + + $"{string.Join(',', instruction.Destinations)} <- " + + $"{string.Join(',', instruction.Sources)} " + + $"{instruction.Control}"); + } + + File.WriteAllLines(Path.Combine(directory, $"{name}.ir.txt"), lines); + } + private static void TraceCreateShader(ulong destinationAddress, ulong headerAddress, ulong codeAddress, string detail) { var isOk = string.Equals(detail, "ok", StringComparison.Ordinal); diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs new file mode 100644 index 0000000..602eb56 --- /dev/null +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderIr.cs @@ -0,0 +1,313 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.Agc; + +internal enum Gen5ShaderEncoding +{ + Sop1, + Sop2, + Sopc, + Sopp, + Sopk, + Smrd, + Smem, + Mubuf, + Mtbuf, + Vop1, + Vop2, + Vopc, + Vop3, + Vintrp, + Ds, + Flat, + Vop3p, + Mimg, + Exp, +} + +internal enum Gen5OperandKind +{ + ScalarRegister, + VectorRegister, + EncodedConstant, + LiteralConstant, +} + +internal enum Gen5ShaderResourceKind +{ + ReadOnlyTexture, + ReadWriteTexture, + Sampler, + ConstantBuffer, +} + +internal enum Gen5PixelOutputKind +{ + Float, + Uint, + Sint, +} + +internal enum Gen5SpirvStage +{ + Vertex, + Pixel, + Compute, +} + +internal sealed record Gen5SpirvShader( + byte[] Spirv, + IReadOnlyList GlobalMemoryBindings, + IReadOnlyList ImageBindings, + uint AttributeCount, + IReadOnlyList VertexInputs); + +internal readonly record struct Gen5ShaderResourceMapping( + Gen5ShaderResourceKind Kind, + uint Slot, + uint OffsetDwords, + bool SizeFlag); + +internal sealed record Gen5ShaderMetadata( + uint ExtendedUserDataSizeDwords, + uint ShaderResourceTableSizeDwords, + IReadOnlyDictionary DirectResources, + IReadOnlyList Resources); + +internal readonly record struct Gen5ComputeSystemRegisters( + uint? WorkGroupXRegister, + uint? WorkGroupYRegister, + uint? WorkGroupZRegister, + uint? ThreadGroupSizeRegister) +{ + public bool TryGetExpression(uint scalarRegister, out string expression) + { + if (WorkGroupXRegister == scalarRegister) + { + expression = "gl_WorkGroupID.x"; + return true; + } + + if (WorkGroupYRegister == scalarRegister) + { + expression = "gl_WorkGroupID.y"; + return true; + } + + if (WorkGroupZRegister == scalarRegister) + { + expression = "gl_WorkGroupID.z"; + return true; + } + + if (ThreadGroupSizeRegister == scalarRegister) + { + expression = "(gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z)"; + return true; + } + + expression = string.Empty; + return false; + } + + public void ClearStaticValues(Span scalarRegisters) + { + ClearStaticValue(scalarRegisters, WorkGroupXRegister); + ClearStaticValue(scalarRegisters, WorkGroupYRegister); + ClearStaticValue(scalarRegisters, WorkGroupZRegister); + ClearStaticValue(scalarRegisters, ThreadGroupSizeRegister); + } + + private static void ClearStaticValue(Span scalarRegisters, uint? scalarRegister) + { + if (scalarRegister is { } register && register < scalarRegisters.Length) + { + scalarRegisters[(int)register] = 0; + } + } +} + +internal sealed record Gen5ShaderState( + Gen5ShaderProgram Program, + IReadOnlyList UserData, + Gen5ShaderMetadata? Metadata, + Gen5ComputeSystemRegisters? ComputeSystemRegisters = null, + uint UserDataScalarRegisterBase = 0); + +internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value) +{ + public static Gen5Operand Scalar(uint index) => + new(Gen5OperandKind.ScalarRegister, index); + + public static Gen5Operand Vector(uint index) => + new(Gen5OperandKind.VectorRegister, index); + + public static Gen5Operand Source(uint encoded, uint? literal = null) + { + if (encoded >= 256) + { + return Vector(encoded - 256); + } + + if (encoded is 249 or 255 && literal.HasValue) + { + return new(Gen5OperandKind.LiteralConstant, literal.Value); + } + + if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127) + { + return Scalar(encoded); + } + + return new(Gen5OperandKind.EncodedConstant, encoded); + } + + public override string ToString() => Kind switch + { + Gen5OperandKind.ScalarRegister => $"s{Value}", + Gen5OperandKind.VectorRegister => $"v{Value}", + Gen5OperandKind.LiteralConstant => $"0x{Value:X8}", + _ => $"src[{Value}]", + }; +} + +internal abstract record Gen5InstructionControl; + +internal sealed record Gen5ImageControl( + uint Dmask, + uint VectorAddress, + IReadOnlyList AddressRegisters, + uint VectorData, + uint ScalarResource, + uint ScalarSampler, + uint Dimension, + bool IsArray, + bool Glc, + bool Slc) : Gen5InstructionControl +{ + public uint GetAddressRegister(int component) => + component < AddressRegisters.Count + ? AddressRegisters[component] + : VectorAddress + (uint)component; +} + +internal sealed record Gen5GlobalMemoryControl( + uint DwordCount, + uint VectorAddress, + uint VectorData, + uint ScalarAddress, + int OffsetBytes, + bool Glc, + bool Slc) : Gen5InstructionControl; + +internal sealed record Gen5BufferMemoryControl( + uint DwordCount, + uint VectorAddress, + uint VectorData, + uint ScalarResource, + int OffsetBytes, + bool IndexEnabled, + bool OffsetEnabled, + bool Glc, + bool Slc) : Gen5InstructionControl; + +internal sealed record Gen5ExportControl( + uint Target, + uint EnableMask, + bool Compressed, + bool Done, + bool ValidMask) : Gen5InstructionControl; + +internal sealed record Gen5InterpolationControl( + uint Attribute, + uint Channel) : Gen5InstructionControl; + +internal sealed record Gen5Vop3Control( + uint AbsoluteMask, + uint NegateMask, + uint OutputModifier, + bool Clamp, + uint? ScalarDestination) : Gen5InstructionControl; + +internal sealed record Gen5SdwaControl( + uint DestinationSelect, + uint Source0Select, + uint Source1Select, + uint AbsoluteMask, + uint NegateMask, + uint OutputModifier, + bool Clamp) : Gen5InstructionControl; + +internal sealed record Gen5DppControl( + uint Control, + bool FetchInactive, + bool BoundControl, + uint AbsoluteMask, + uint NegateMask, + uint BankMask, + uint RowMask) : Gen5InstructionControl; + +internal sealed record Gen5ScalarMemoryControl( + uint DestinationCount, + int ImmediateOffsetBytes, + uint? DynamicOffsetRegister) : Gen5InstructionControl; + +internal sealed record Gen5DataShareControl( + uint Offset0, + uint Offset1, + bool Gds) : Gen5InstructionControl; + +internal sealed record Gen5ImageBinding( + uint Pc, + string Opcode, + Gen5ImageControl Control, + IReadOnlyList ResourceDescriptor, + IReadOnlyList SamplerDescriptor, + uint? MipLevel); + +internal sealed record Gen5GlobalMemoryBinding( + uint ScalarAddress, + ulong BaseAddress, + IReadOnlyList InstructionPcs, + byte[] Data); + +internal sealed record Gen5VertexInputBinding( + uint Pc, + uint Location, + uint ComponentCount, + uint DataFormat, + uint NumberFormat, + ulong BaseAddress, + uint Stride, + uint OffsetBytes, + byte[] Data); + +internal sealed record Gen5ShaderEvaluation( + IReadOnlyList InitialScalarRegisters, + IReadOnlyList ScalarRegisters, + IReadOnlyDictionary> ScalarRegistersByPc, + IReadOnlyList ImageBindings, + IReadOnlyList GlobalMemoryBindings, + Gen5ComputeSystemRegisters? ComputeSystemRegisters = null, + IReadOnlySet? RuntimeScalarRegisters = null, + IReadOnlyList? VertexInputs = null); + +internal sealed record Gen5ShaderInstruction( + uint Pc, + Gen5ShaderEncoding Encoding, + string Opcode, + IReadOnlyList Words, + IReadOnlyList Sources, + IReadOnlyList Destinations, + Gen5InstructionControl? Control); + +internal sealed record Gen5ShaderProgram( + ulong Address, + IReadOnlyList Instructions) +{ + public IEnumerable ImageResources => + Instructions + .Select(instruction => instruction.Control) + .OfType(); +} diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderMetadataReader.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderMetadataReader.cs new file mode 100644 index 0000000..d51b58c --- /dev/null +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderMetadataReader.cs @@ -0,0 +1,154 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using System.Buffers.Binary; + +namespace SharpEmu.Libs.Agc; + +internal static class Gen5ShaderMetadataReader +{ + private const ulong ShaderUserDataOffset = 0x08; + private const int ResourceClassCount = 4; + private const int MaxMetadataEntries = 4096; + + public static bool TryRead( + CpuContext ctx, + ulong shaderHeaderAddress, + out Gen5ShaderMetadata metadata) + { + metadata = default!; + if (!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) || + userDataAddress == 0 || + !TryReadUInt64(ctx, userDataAddress, out var directResourceOffsetsAddress)) + { + return false; + } + + var resourceOffsets = new ulong[ResourceClassCount]; + for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++) + { + if (!TryReadUInt64( + ctx, + userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)), + out resourceOffsets[resourceClass])) + { + return false; + } + } + + if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) || + !TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) || + !TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) || + directResourceCount > MaxMetadataEntries) + { + return false; + } + + var resourceCounts = new ushort[ResourceClassCount]; + for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++) + { + if (!TryReadUInt16( + ctx, + userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)), + out resourceCounts[resourceClass]) || + resourceCounts[resourceClass] > MaxMetadataEntries) + { + return false; + } + } + + var directResources = new Dictionary(); + if (directResourceCount != 0) + { + if (directResourceOffsetsAddress == 0) + { + return false; + } + + for (uint type = 0; type < directResourceCount; type++) + { + if (!TryReadUInt16(ctx, directResourceOffsetsAddress + type * sizeof(ushort), out var offset)) + { + return false; + } + + if (offset != ushort.MaxValue) + { + directResources[type] = offset; + } + } + } + + var resources = new List(); + for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++) + { + var count = resourceCounts[resourceClass]; + if (count == 0) + { + continue; + } + + if (resourceOffsets[resourceClass] == 0) + { + return false; + } + + for (uint slot = 0; slot < count; slot++) + { + if (!TryReadUInt16( + ctx, + resourceOffsets[resourceClass] + slot * sizeof(ushort), + out var sharp)) + { + return false; + } + + var offset = (uint)(sharp & 0x7FFF); + if (offset == 0x7FFF) + { + continue; + } + + resources.Add(new Gen5ShaderResourceMapping( + (Gen5ShaderResourceKind)resourceClass, + slot, + offset, + (sharp & 0x8000) != 0)); + } + } + + metadata = new Gen5ShaderMetadata( + extendedUserDataSize, + shaderResourceTableSize, + directResources, + resources); + return true; + } + + private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value) + { + Span bytes = stackalloc byte[sizeof(ushort)]; + if (!ctx.Memory.TryRead(address, bytes)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt16LittleEndian(bytes); + return true; + } + + private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value) + { + Span bytes = stackalloc byte[sizeof(ulong)]; + if (!ctx.Memory.TryRead(address, bytes)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt64LittleEndian(bytes); + return true; + } +} diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs new file mode 100644 index 0000000..990591e --- /dev/null +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderScalarEvaluator.cs @@ -0,0 +1,1742 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using System.Buffers.Binary; +using System.Numerics; + +namespace SharpEmu.Libs.Agc; + +internal static class Gen5ShaderScalarEvaluator +{ + private const int ScalarRegisterCount = 256; + private const int ImageDescriptorDwords = 8; + private const int SamplerDescriptorDwords = 4; + private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024; + private const ulong RdnaWaveMask = 0xFFFF_FFFFUL; + + private readonly record struct BufferDescriptor( + ulong BaseAddress, + uint Stride, + uint NumRecords, + ulong SizeBytes, + uint NumberFormat, + uint DataFormat); + + public static bool TryResolveImageBindings( + CpuContext ctx, + Gen5ShaderState state, + out IReadOnlyList bindings, + out string error) + { + if (TryEvaluate(ctx, state, out var evaluation, out error)) + { + bindings = evaluation.ImageBindings; + return true; + } + + bindings = []; + return false; + } + + public static bool TryEvaluate( + CpuContext ctx, + Gen5ShaderState state, + out Gen5ShaderEvaluation evaluation, + out string error, + bool resolveVertexInputs = false) + { + evaluation = default!; + error = string.Empty; + var scalarRegisters = new uint[ScalarRegisterCount]; + for (var index = 0; + index < state.UserData.Count && + state.UserDataScalarRegisterBase + (uint)index < scalarRegisters.Length; + index++) + { + scalarRegisters[state.UserDataScalarRegisterBase + (uint)index] = + state.UserData[index]; + } + + if (state.ComputeSystemRegisters is { } computeSystemRegisters) + { + computeSystemRegisters.ClearStaticValues(scalarRegisters); + } + + var execMask = RdnaWaveMask; + WriteScalarPair(scalarRegisters, 106, 0, ref execMask); + WriteScalarPair(scalarRegisters, 126, execMask, ref execMask); + var initialScalarRegisters = (uint[])scalarRegisters.Clone(); + + var resolved = new List(); + var globalMemoryBindings = new List(); + var globalMemoryByAddress = new Dictionary<(uint ScalarAddress, ulong BaseAddress), Gen5GlobalMemoryBinding>(); + var vertexInputBindings = new List(); + var runtimeScalarRegisters = CollectRuntimeScalarRegisters(state.Program); + var scalarRegisterSnapshots = new Dictionary>(); + var scalarConditionCode = false; + uint? skipUntilPc = null; + + foreach (var instruction in state.Program.Instructions) + { + if (skipUntilPc.HasValue) + { + if (instruction.Pc < skipUntilPc.Value) + { + continue; + } + + skipUntilPc = null; + } + + scalarRegisterSnapshots[instruction.Pc] = (uint[])scalarRegisters.Clone(); + + if (instruction.Opcode == "SEndpgm") + { + break; + } + + if (instruction.Opcode == "SBranch" && + TryGetSoppBranchTargetPc(instruction, out var targetPc) && + targetPc > instruction.Pc) + { + skipUntilPc = targetPc; + continue; + } + + if (instruction.Encoding == Gen5ShaderEncoding.Sopc) + { + if (!TryExecuteScalarCompare( + instruction, + scalarRegisters, + out scalarConditionCode, + out error)) + { + return false; + } + + continue; + } + + if (instruction.Encoding == Gen5ShaderEncoding.Sopk && + instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal)) + { + if (!TryExecuteScalarCompareK( + instruction, + scalarRegisters, + out scalarConditionCode, + out error)) + { + return false; + } + + continue; + } + + if (instruction.Encoding is + Gen5ShaderEncoding.Sop1 or + Gen5ShaderEncoding.Sop2 or + Gen5ShaderEncoding.Sopk) + { + if (instruction.Opcode is "SSetpcB64" or "SSwappcB64") + { + break; + } + + if (!TryExecuteScalarAlu( + instruction, + state.Program.Address, + scalarRegisters, + ref execMask, + ref scalarConditionCode, + out error)) + { + return false; + } + + continue; + } + + if (instruction.Control is Gen5ScalarMemoryControl scalarMemory) + { + foreach (var destination in instruction.Destinations) + { + if (destination.Kind == Gen5OperandKind.ScalarRegister && destination.Value < ScalarRegisterCount) + { + runtimeScalarRegisters.Add(destination.Value); + } + } + + if (!TryExecuteScalarLoad(ctx, state, instruction, scalarMemory, scalarRegisters, globalMemoryBindings, globalMemoryByAddress, runtimeScalarRegisters, out error)) + { + return false; + } + continue; + } + + if (instruction.Control is Gen5GlobalMemoryControl globalMemory) + { + if (globalMemory.ScalarAddress >= ScalarRegisterCount - 1) + { + error = + $"global-address-register-range pc=0x{instruction.Pc:X} " + + $"s{globalMemory.ScalarAddress}"; + return false; + } + + var baseAddress = + scalarRegisters[globalMemory.ScalarAddress] | + ((ulong)scalarRegisters[globalMemory.ScalarAddress + 1] << 32); + if (baseAddress == 0) + { + error = $"global-address-null pc=0x{instruction.Pc:X}"; + return false; + } + + var key = (globalMemory.ScalarAddress, baseAddress); + if (globalMemoryByAddress.TryGetValue(key, out var existingBinding)) + { + if (existingBinding.InstructionPcs is List instructionPcs) + { + instructionPcs.Add(instruction.Pc); + } + } + else + { + byte[] data; + if (!TryReadGlobalMemory(ctx, baseAddress, out data)) + { + error = + $"global-memory-read-failed pc=0x{instruction.Pc:X} " + + $"address=0x{baseAddress:X16}"; + return false; + } + + var binding = new Gen5GlobalMemoryBinding( + globalMemory.ScalarAddress, + baseAddress, + new List { instruction.Pc }, + data); + globalMemoryByAddress.Add(key, binding); + globalMemoryBindings.Add(binding); + } + + continue; + } + + if (instruction.Control is Gen5BufferMemoryControl bufferMemory) + { + if (bufferMemory.ScalarResource >= ScalarRegisterCount - 3) + { + error = + $"buffer-resource-register-range pc=0x{instruction.Pc:X} " + + $"s{bufferMemory.ScalarResource}"; + return false; + } + + if (!TryDecodeBufferDescriptor( + scalarRegisters, + bufferMemory.ScalarResource, + strictType: true, + out var bufferDescriptor)) + { + error = + $"buffer-descriptor-invalid pc=0x{instruction.Pc:X} " + + $"s{bufferMemory.ScalarResource}"; + return false; + } + + if (bufferDescriptor.BaseAddress == 0) + { + error = $"buffer-address-null pc=0x{instruction.Pc:X}"; + return false; + } + + if (resolveVertexInputs && + IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor)) + { + if (!TryReadGlobalMemory( + ctx, + bufferDescriptor.BaseAddress, + bufferDescriptor.SizeBytes, + out var vertexData)) + { + error = + $"vertex-buffer-read-failed pc=0x{instruction.Pc:X} " + + $"address=0x{bufferDescriptor.BaseAddress:X16} " + + $"bytes={bufferDescriptor.SizeBytes} " + + $"stride={bufferDescriptor.Stride} records={bufferDescriptor.NumRecords}"; + return false; + } + + if (!TryCreateVertexInputBinding( + instruction, + bufferMemory, + bufferDescriptor, + vertexData, + (uint)vertexInputBindings.Count, + scalarRegisters, + out var vertexInputBinding)) + { + error = + $"vertex-input-binding-failed pc=0x{instruction.Pc:X} " + + $"s{bufferMemory.ScalarResource}"; + return false; + } + + vertexInputBindings.Add(vertexInputBinding); + continue; + } + + var key = (bufferMemory.ScalarResource, bufferDescriptor.BaseAddress); + if (globalMemoryByAddress.TryGetValue(key, out var existingBinding)) + { + if (existingBinding.InstructionPcs is List instructionPcs) + { + instructionPcs.Add(instruction.Pc); + } + } + else + { + if (!TryReadGlobalMemory( + ctx, + bufferDescriptor.BaseAddress, + bufferDescriptor.SizeBytes, + out var data)) + { + var descriptorWords = string.Join( + ':', + Enumerable.Range(0, 4).Select(index => + $"{scalarRegisters[bufferMemory.ScalarResource + (uint)index]:X8}")); + error = + $"buffer-memory-read-failed pc=0x{instruction.Pc:X} " + + $"address=0x{bufferDescriptor.BaseAddress:X16} " + + $"bytes={bufferDescriptor.SizeBytes} " + + $"stride={bufferDescriptor.Stride} records={bufferDescriptor.NumRecords} " + + $"s{bufferMemory.ScalarResource}=[{descriptorWords}]"; + return false; + } + + var binding = new Gen5GlobalMemoryBinding( + bufferMemory.ScalarResource, + bufferDescriptor.BaseAddress, + new List { instruction.Pc }, + data); + globalMemoryByAddress.Add(key, binding); + globalMemoryBindings.Add(binding); + } + + continue; + } + + if (instruction.Control is not Gen5ImageControl image) + { + continue; + } + + if (!TryCopyRegisters( + scalarRegisters, + image.ScalarResource, + ImageDescriptorDwords, + out var resourceDescriptor)) + { + error = $"resource-register-range pc=0x{instruction.Pc:X} s{image.ScalarResource}"; + return false; + } + + IReadOnlyList samplerDescriptor = []; + if (UsesSampler(instruction.Opcode) && + !TryCopyRegisters( + scalarRegisters, + image.ScalarSampler, + SamplerDescriptorDwords, + out samplerDescriptor)) + { + error = $"sampler-register-range pc=0x{instruction.Pc:X} s{image.ScalarSampler}"; + return false; + } + + resolved.Add(new Gen5ImageBinding( + instruction.Pc, + instruction.Opcode, + image, + resourceDescriptor, + samplerDescriptor, + instruction.Opcode is "ImageLoadMip" or "ImageStoreMip" && + TryResolveVectorConstantBefore( + state.Program, + instruction.Pc, + image.GetAddressRegister(2), + out var mipLevel) + ? mipLevel + : null)); + } + + evaluation = new Gen5ShaderEvaluation( + initialScalarRegisters, + scalarRegisters, + scalarRegisterSnapshots, + resolved, + globalMemoryBindings, + state.ComputeSystemRegisters, + runtimeScalarRegisters, + vertexInputBindings); + return true; + } + + private static bool TryCreateVertexInputBinding( + Gen5ShaderInstruction instruction, + Gen5BufferMemoryControl control, + BufferDescriptor descriptor, + byte[] data, + uint location, + uint[] scalarRegisters, + out Gen5VertexInputBinding binding) + { + binding = default!; + if (!IsVertexFetchCandidate(instruction, control, descriptor) || + instruction.Sources.Count <= 2 || + !TryEvaluateScalarOperand(instruction.Sources[2], scalarRegisters, out var scalarOffset)) + { + return false; + } + + var bindingData = data; + var bindingStride = descriptor.Stride; + var bindingOffset = unchecked((uint)control.OffsetBytes + scalarOffset); + var bindingDataFormat = descriptor.DataFormat; + var bindingNumberFormat = descriptor.NumberFormat; + binding = new Gen5VertexInputBinding( + instruction.Pc, + location, + control.DwordCount, + bindingDataFormat, + bindingNumberFormat, + descriptor.BaseAddress, + bindingStride, + bindingOffset, + bindingData); + return true; + } + + private static bool IsVertexFetchCandidate( + Gen5ShaderInstruction instruction, + Gen5BufferMemoryControl control, + BufferDescriptor descriptor) => + control.IndexEnabled && + !control.OffsetEnabled && + control.DwordCount is >= 1 and <= 4 && + descriptor.BaseAddress != 0 && + descriptor.Stride != 0 && + (instruction.Opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal) || + instruction.Opcode.StartsWith("TBufferLoadFormat", StringComparison.Ordinal)); + + private static HashSet CollectRuntimeScalarRegisters(Gen5ShaderProgram program) + { + var registers = new HashSet(); + foreach (var instruction in program.Instructions) + { + foreach (var operand in instruction.Sources.Concat(instruction.Destinations)) + { + if (operand.Kind == Gen5OperandKind.ScalarRegister && + operand.Value < ScalarRegisterCount) + { + registers.Add(operand.Value); + } + } + + if (instruction.Control is Gen5ScalarMemoryControl + { + DynamicOffsetRegister: { } offsetRegister, + } && + offsetRegister < ScalarRegisterCount) + { + registers.Add(offsetRegister); + } + } + + return registers; + } + + private static bool TryGetSoppBranchTargetPc( + 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 bool TryResolveVectorConstantBefore( + Gen5ShaderProgram program, + uint pc, + uint vectorRegister, + out uint value) + { + for (var index = program.Instructions.Count - 1; index >= 0; index--) + { + var instruction = program.Instructions[index]; + if (instruction.Pc >= pc || + instruction.Destinations.Count != 1 || + instruction.Destinations[0] is not + { + Kind: Gen5OperandKind.VectorRegister, + Value: var destination, + } || + destination != vectorRegister) + { + continue; + } + + if (instruction.Opcode == "VMovB32" && + instruction.Sources.Count == 1 && + TryResolveConstantOperand(instruction.Sources[0], out value)) + { + return true; + } + + break; + } + + value = 0; + return false; + } + + private static bool TryResolveConstantOperand(Gen5Operand operand, out uint value) + { + if (operand.Kind == Gen5OperandKind.LiteralConstant) + { + value = operand.Value; + return true; + } + + if (operand.Kind == Gen5OperandKind.EncodedConstant) + { + return TryDecodeInlineConstant(operand.Value, out value); + } + + value = 0; + return false; + } + + private static bool TryReadGlobalMemory( + CpuContext ctx, + ulong baseAddress, + out byte[] data) + { + for (var size = MaxGlobalMemoryBindingBytes; size >= 4096; size >>= 1) + { + data = GC.AllocateUninitializedArray(size); + if (ctx.Memory.TryRead(baseAddress, data)) + { + return true; + } + } + + data = []; + return false; + } + + private static bool TryReadGlobalMemory( + CpuContext ctx, + ulong baseAddress, + ulong sizeBytes, + out byte[] data) + { + if (sizeBytes == 0) + { + data = []; + return false; + } + + var cappedSize = Math.Min(sizeBytes, MaxGlobalMemoryBindingBytes); + if (cappedSize > int.MaxValue) + { + data = []; + return false; + } + + var candidateSize = (int)cappedSize; + while (candidateSize >= sizeof(uint)) + { + data = GC.AllocateUninitializedArray(candidateSize); + if (ctx.Memory.TryRead(baseAddress, data) || + KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data)) + { + return true; + } + + if (candidateSize == sizeof(uint)) + { + break; + } + + candidateSize = Math.Max(candidateSize / 2, sizeof(uint)); + } + + data = []; + return false; + } + + private static bool TryExecuteScalarAlu( + Gen5ShaderInstruction instruction, + ulong programAddress, + uint[] registers, + ref ulong execMask, + ref bool scalarConditionCode, + out string error) + { + error = string.Empty; + if (instruction.Destinations.Count != 1 || + instruction.Destinations[0] is not + { + Kind: Gen5OperandKind.ScalarRegister, + Value: < ScalarRegisterCount, + } destination) + { + error = $"unsupported-scalar-destination pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + if (instruction.Opcode == "SMovkI32") + { + registers[destination.Value] = unchecked((uint)(short)instruction.Sources[0].Value); + return true; + } + + if (instruction.Opcode is "SAddkI32" or "SMulkI32") + { + var immediate = unchecked((uint)(short)instruction.Sources[0].Value); + registers[destination.Value] = instruction.Opcode == "SAddkI32" + ? registers[destination.Value] + immediate + : unchecked((uint)((int)registers[destination.Value] * (int)immediate)); + return true; + } + + if (instruction.Opcode == "SGetpcB64") + { + var pc = programAddress + instruction.Pc + (ulong)(instruction.Words.Count * sizeof(uint)); + WriteScalarPair(registers, destination.Value, pc, ref execMask); + return true; + } + + if (TryExecuteSaveExecScalarAlu( + instruction, + registers, + ref execMask, + ref scalarConditionCode, + out error)) + { + return true; + } + + if (instruction.Opcode is "SMovB64" or "SWqmB64" or "SNotB64") + { + if (destination.Value >= ScalarRegisterCount - 1 || + !TryEvaluateScalarOperand64( + instruction.Sources[0], + registers, + execMask, + out var value)) + { + error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + if (instruction.Opcode == "SNotB64") + { + value = ~value; + scalarConditionCode = value != 0; + } + + WriteScalarPair(registers, destination.Value, value, ref execMask); + return true; + } + + if (instruction.Opcode is "SLshlB64" or "SLshrB64") + { + if (instruction.Sources.Count < 2 || + destination.Value >= ScalarRegisterCount - 1 || + !TryEvaluateScalarOperand64( + instruction.Sources[0], + registers, + execMask, + out var value) || + !TryEvaluateScalarOperand( + instruction.Sources[1], + registers, + out var shift)) + { + error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + value = instruction.Opcode == "SLshlB64" + ? value << ((int)shift & 63) + : value >> ((int)shift & 63); + WriteScalarPair(registers, destination.Value, value, ref execMask); + scalarConditionCode = value != 0; + return true; + } + + if (instruction.Opcode is "SBfeU64" or "SBfeI64") + { + if (instruction.Sources.Count < 2 || + destination.Value >= ScalarRegisterCount - 1 || + !TryEvaluateScalarOperand64( + instruction.Sources[0], + registers, + execMask, + out var source) || + !TryEvaluateScalarOperand( + instruction.Sources[1], + registers, + out var control)) + { + error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + var offset = (int)control & 63; + var width = Math.Min(((int)control >> 16) & 0x7F, 64 - offset); + ulong value; + if (width == 0) + { + value = 0; + } + else + { + value = source >> offset; + if (width < 64) + { + value &= ulong.MaxValue >> (64 - width); + if (instruction.Opcode == "SBfeI64") + { + value = unchecked((ulong)((long)(value << (64 - width)) >> (64 - width))); + } + } + } + + WriteScalarPair(registers, destination.Value, value, ref execMask); + scalarConditionCode = value != 0; + return true; + } + + if (instruction.Opcode is + "SCselectB64" or + "SAndB64" or + "SOrB64" or + "SXorB64" or + "SAndn2B64" or + "SOrn2B64" or + "SNandB64" or + "SNorB64" or + "SXnorB64") + { + if (instruction.Sources.Count < 2 || + !TryEvaluateScalarOperand64( + instruction.Sources[0], + registers, + execMask, + out var maskLeft) || + !TryEvaluateScalarOperand64( + instruction.Sources[1], + registers, + execMask, + out var maskRight)) + { + error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + var value = instruction.Opcode switch + { + "SCselectB64" => scalarConditionCode ? maskLeft : maskRight, + "SAndB64" => maskLeft & maskRight, + "SOrB64" => maskLeft | maskRight, + "SXorB64" => maskLeft ^ maskRight, + "SAndn2B64" => maskLeft & ~maskRight, + "SOrn2B64" => maskLeft | ~maskRight, + "SNandB64" => ~(maskLeft & maskRight), + "SNorB64" => ~(maskLeft | maskRight), + _ => ~(maskLeft ^ maskRight), + }; + WriteScalarPair(registers, destination.Value, value, ref execMask); + scalarConditionCode = value != 0; + return true; + } + + if (instruction.Sources.Count == 0 || + !TryEvaluateScalarOperand(instruction.Sources[0], registers, out var left)) + { + var source = instruction.Sources.Count == 0 + ? "" + : instruction.Sources[0].ToString(); + error = $"scalar-source0 pc=0x{instruction.Pc:X} op={instruction.Opcode} source={source}"; + return false; + } + + if (instruction.Opcode == "SMovB32") + { + registers[destination.Value] = left; + return true; + } + + if (instruction.Opcode is + "SNotB32" or + "SBrevB32" or + "SBcnt1I32B32" or + "SFF1I32B32" or + "SBitset1B32") + { + registers[destination.Value] = instruction.Opcode switch + { + "SNotB32" => ~left, + "SBrevB32" => ReverseBits(left), + "SBcnt1I32B32" => (uint)BitOperations.PopCount(left), + "SFF1I32B32" => left == 0 ? uint.MaxValue : (uint)BitOperations.TrailingZeroCount(left), + _ => registers[destination.Value] | (1u << ((int)left & 31)), + }; + if (instruction.Opcode != "SBitset1B32") + { + scalarConditionCode = registers[destination.Value] != 0; + } + + return true; + } + + if (instruction.Sources.Count < 2 || + !TryEvaluateScalarOperand(instruction.Sources[1], registers, out var right)) + { + var source = instruction.Sources.Count < 2 + ? "" + : instruction.Sources[1].ToString(); + error = $"scalar-source1 pc=0x{instruction.Pc:X} op={instruction.Opcode} source={source}"; + return false; + } + + uint result; + switch (instruction.Opcode) + { + case "SAddU32": + { + var wide = (ulong)left + right; + result = (uint)wide; + scalarConditionCode = wide > uint.MaxValue; + break; + } + case "SSubU32": + result = left - right; + scalarConditionCode = right > left; + break; + case "SAddI32": + result = unchecked((uint)((int)left + (int)right)); + scalarConditionCode = SignedAddOverflow(left, right, result); + break; + case "SSubI32": + result = unchecked((uint)((int)left - (int)right)); + scalarConditionCode = SignedSubOverflow(left, right, result); + break; + case "SAddcU32": + { + var wide = (ulong)left + right + (scalarConditionCode ? 1UL : 0UL); + result = (uint)wide; + scalarConditionCode = wide > uint.MaxValue; + break; + } + case "SSubbU32": + { + var borrow = scalarConditionCode ? 1UL : 0UL; + var subtrahend = (ulong)right + borrow; + result = unchecked(left - (uint)subtrahend); + scalarConditionCode = subtrahend > left; + break; + } + case "SMinI32": + result = unchecked((uint)Math.Min((int)left, (int)right)); + scalarConditionCode = (int)left < (int)right; + break; + case "SMinU32": + result = Math.Min(left, right); + scalarConditionCode = left < right; + break; + case "SMaxI32": + result = unchecked((uint)Math.Max((int)left, (int)right)); + scalarConditionCode = (int)left > (int)right; + break; + case "SMaxU32": + result = Math.Max(left, right); + scalarConditionCode = left > right; + break; + case "SCselectB32": + result = scalarConditionCode ? left : right; + break; + case "SAndB32": + result = left & right; + scalarConditionCode = result != 0; + break; + case "SOrB32": + result = left | right; + scalarConditionCode = result != 0; + break; + case "SXorB32": + result = left ^ right; + scalarConditionCode = result != 0; + break; + case "SAndn2B32": + result = left & ~right; + scalarConditionCode = result != 0; + break; + case "SOrn2B32": + result = left | ~right; + scalarConditionCode = result != 0; + break; + case "SNandB32": + result = ~(left & right); + scalarConditionCode = result != 0; + break; + case "SNorB32": + result = ~(left | right); + scalarConditionCode = result != 0; + break; + case "SXnorB32": + result = ~(left ^ right); + scalarConditionCode = result != 0; + break; + case "SLshlB32": + result = left << ((int)right & 31); + scalarConditionCode = result != 0; + break; + case "SLshrB32": + result = left >> ((int)right & 31); + scalarConditionCode = result != 0; + break; + case "SAshrI32": + result = unchecked((uint)((int)left >> ((int)right & 31))); + scalarConditionCode = result != 0; + break; + case "SBfmB32": + { + var width = (int)left & 31; + var offset = (int)right & 31; + result = width == 0 ? 0 : ((1u << width) - 1u) << offset; + break; + } + case "SMulI32": + result = unchecked((uint)((int)left * (int)right)); + break; + case "SBfeU32": + { + var offset = (int)right & 31; + var width = Math.Min(((int)right >> 16) & 0x7F, 32 - offset); + result = width == 0 ? 0 : left >> offset & (uint.MaxValue >> (32 - width)); + scalarConditionCode = result != 0; + break; + } + case "SBfeI32": + { + var offset = (int)right & 31; + var width = Math.Min(((int)right >> 16) & 0x7F, 32 - offset); + result = width == 0 + ? 0 + : unchecked((uint)(((int)(left << (32 - width - offset))) >> (32 - width))); + scalarConditionCode = result != 0; + break; + } + case "SAbsdiffI32": + result = unchecked((uint)Math.Abs((long)(int)left - (int)right)); + scalarConditionCode = result != 0; + break; + case "SLshl1AddU32": + { + var wide = ((ulong)left << 1) + right; + result = (uint)wide; + scalarConditionCode = wide > uint.MaxValue; + break; + } + case "SLshl2AddU32": + { + var wide = ((ulong)left << 2) + right; + result = (uint)wide; + scalarConditionCode = wide > uint.MaxValue; + break; + } + case "SLshl3AddU32": + { + var wide = ((ulong)left << 3) + right; + result = (uint)wide; + scalarConditionCode = wide > uint.MaxValue; + break; + } + case "SLshl4AddU32": + { + var wide = ((ulong)left << 4) + right; + result = (uint)wide; + scalarConditionCode = wide > uint.MaxValue; + break; + } + case "SPackLlB32B16": + result = (left & 0xFFFFu) | (right << 16); + break; + case "SPackLhB32B16": + result = (left & 0xFFFFu) | (right & 0xFFFF0000u); + break; + case "SPackHhB32B16": + result = (left >> 16) | (right & 0xFFFF0000u); + break; + case "SMulHiU32": + result = (uint)(((ulong)left * right) >> 32); + break; + case "SMulHiI32": + result = unchecked((uint)(((long)(int)left * (int)right) >> 32)); + break; + default: + error = $"unsupported-scalar-op pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + registers[destination.Value] = result; + return true; + } + + private static bool TryExecuteSaveExecScalarAlu( + Gen5ShaderInstruction instruction, + uint[] registers, + ref ulong execMask, + ref bool scalarConditionCode, + out string error) + { + error = string.Empty; + if (instruction.Opcode is not ( + "SAndSaveexecB64" or + "SOrSaveexecB64" or + "SXorSaveexecB64" or + "SAndn2SaveexecB64" or + "SOrn2SaveexecB64" or + "SNandSaveexecB64" or + "SNorSaveexecB64" or + "SXnorSaveexecB64" or + "SAndn1SaveexecB64" or + "SOrn1SaveexecB64")) + { + return false; + } + + if (instruction.Destinations.Count != 1 || + instruction.Destinations[0] is not + { + Kind: Gen5OperandKind.ScalarRegister, + Value: < ScalarRegisterCount - 1, + } destination || + instruction.Sources.Count == 0 || + !TryEvaluateScalarOperand64( + instruction.Sources[0], + registers, + execMask, + out var source)) + { + error = $"scalar-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + var oldExec = execMask; + var newExec = instruction.Opcode switch + { + "SAndSaveexecB64" => oldExec & source, + "SOrSaveexecB64" => oldExec | source, + "SXorSaveexecB64" => oldExec ^ source, + "SAndn1SaveexecB64" => ~source & oldExec, + "SAndn2SaveexecB64" => source & ~oldExec, + "SOrn1SaveexecB64" => ~source | oldExec, + "SOrn2SaveexecB64" => source | ~oldExec, + "SNandSaveexecB64" => ~(source & oldExec), + "SNorSaveexecB64" => ~(source | oldExec), + _ => ~(oldExec ^ source), + }; + + WriteScalarPair(registers, destination.Value, oldExec, ref execMask); + execMask = MaskWaveValue(newExec); + WriteScalarPair(registers, 126, execMask, ref execMask); + scalarConditionCode = execMask != 0; + return true; + } + + private static bool TryEvaluateScalarOperand64( + Gen5Operand operand, + uint[] registers, + ulong execMask, + out ulong value) + { + if (operand.Kind == Gen5OperandKind.ScalarRegister && + operand.Value == 126) + { + value = execMask; + return true; + } + + if (operand.Kind == Gen5OperandKind.ScalarRegister && + operand.Value < ScalarRegisterCount - 1) + { + value = registers[operand.Value] | ((ulong)registers[operand.Value + 1] << 32); + return true; + } + + if (TryEvaluateScalarOperand(operand, registers, out var low)) + { + value = operand.Kind == Gen5OperandKind.EncodedConstant && + operand.Value is >= 193 and <= 208 + ? ulong.MaxValue << 32 | low + : low; + return true; + } + + value = 0; + return false; + } + + private static void WriteScalarPair( + uint[] registers, + uint destination, + ulong value, + ref ulong execMask) + { + if (destination >= ScalarRegisterCount - 1) + { + return; + } + + if (destination == 126) + { + value = MaskWaveValue(value); + } + + registers[destination] = (uint)value; + registers[destination + 1] = (uint)(value >> 32); + if (destination == 126) + { + execMask = value; + } + } + + private static ulong MaskWaveValue(ulong value) => value & RdnaWaveMask; + + private static bool SignedAddOverflow(uint left, uint right, uint result) => + ((left ^ result) & (right ^ result) & 0x80000000u) != 0; + + private static bool SignedSubOverflow(uint left, uint right, uint result) => + ((left ^ right) & (left ^ result) & 0x80000000u) != 0; + + private static bool TryExecuteScalarCompare( + Gen5ShaderInstruction instruction, + uint[] registers, + out bool scalarConditionCode, + out string error) + { + scalarConditionCode = false; + error = string.Empty; + if (instruction.Sources.Count != 2 || + !TryEvaluateScalarOperand(instruction.Sources[0], registers, out var left) || + !TryEvaluateScalarOperand(instruction.Sources[1], registers, out var right)) + { + error = $"scalar-compare-source pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + if (instruction.Opcode is "SBitcmp0B32" or "SBitcmp1B32") + { + var bit = (int)(right & 31u); + var isSet = ((left >> bit) & 1u) != 0; + scalarConditionCode = instruction.Opcode == "SBitcmp1B32" ? isSet : !isSet; + return true; + } + + if (instruction.Opcode is "SBitcmp0B64" or "SBitcmp1B64") + { + if (!TryEvaluateScalarOperand64(instruction.Sources[0], registers, ulong.MaxValue, out var wide)) + { + error = $"scalar-bitcmp-source64 pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + var bit = (int)(right & 63u); + var isSet = ((wide >> bit) & 1UL) != 0; + scalarConditionCode = instruction.Opcode == "SBitcmp1B64" ? isSet : !isSet; + return true; + } + + scalarConditionCode = instruction.Opcode switch + { + "SCmpEqI32" => (int)left == (int)right, + "SCmpLgI32" => (int)left != (int)right, + "SCmpGtI32" => (int)left > (int)right, + "SCmpGeI32" => (int)left >= (int)right, + "SCmpLtI32" => (int)left < (int)right, + "SCmpLeI32" => (int)left <= (int)right, + "SCmpEqU32" => left == right, + "SCmpLgU32" => left != right, + "SCmpGtU32" => left > right, + "SCmpGeU32" => left >= right, + "SCmpLtU32" => left < right, + "SCmpLeU32" => left <= right, + _ => false, + }; + if (!instruction.Opcode.StartsWith("SCmp", StringComparison.Ordinal)) + { + error = $"unsupported-scalar-compare pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + return true; + } + + private static bool TryExecuteScalarCompareK( + Gen5ShaderInstruction instruction, + uint[] registers, + out bool scalarConditionCode, + out string error) + { + scalarConditionCode = false; + error = string.Empty; + if (instruction.Destinations.Count != 1 || + instruction.Destinations[0] is not + { + Kind: Gen5OperandKind.ScalarRegister, + Value: < ScalarRegisterCount, + } destination) + { + error = $"scalar-comparek-destination pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + var left = registers[destination.Value]; + var right = unchecked((uint)(short)instruction.Sources[0].Value); + scalarConditionCode = instruction.Opcode switch + { + "SCmpkEqI32" => (int)left == (int)right, + "SCmpkLgI32" => (int)left != (int)right, + "SCmpkGtI32" => (int)left > (int)right, + "SCmpkGeI32" => (int)left >= (int)right, + "SCmpkLtI32" => (int)left < (int)right, + "SCmpkLeI32" => (int)left <= (int)right, + "SCmpkEqU32" => left == right, + "SCmpkLgU32" => left != right, + "SCmpkGtU32" => left > right, + "SCmpkGeU32" => left >= right, + "SCmpkLtU32" => left < right, + "SCmpkLeU32" => left <= right, + _ => false, + }; + if (!instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal)) + { + error = $"unsupported-scalar-comparek pc=0x{instruction.Pc:X} op={instruction.Opcode}"; + return false; + } + + return true; + } + + private static bool TryExecuteScalarLoad( + CpuContext ctx, + Gen5ShaderState state, + Gen5ShaderInstruction instruction, + Gen5ScalarMemoryControl control, + uint[] scalarRegisters, + List globalMemoryBindings, + Dictionary<(uint ScalarAddress, ulong BaseAddress), Gen5GlobalMemoryBinding> globalMemoryByAddress, + HashSet runtimeScalarRegisters, + out string error) + { + error = string.Empty; + if (instruction.Sources.Count == 0 || + instruction.Sources[0] is not + { + Kind: Gen5OperandKind.ScalarRegister, + Value: < ScalarRegisterCount - 1, + } scalarBase) + { + error = $"invalid-scalar-base pc=0x{instruction.Pc:X}"; + return false; + } + + var isBufferLoad = + instruction.Opcode.StartsWith("SBufferLoad", StringComparison.Ordinal); + BufferDescriptor bufferDescriptor = default; + var hasBufferDescriptor = + isBufferLoad && + TryDecodeBufferDescriptor( + scalarRegisters, + scalarBase.Value, + strictType: false, + out bufferDescriptor); + var baseAddress = hasBufferDescriptor + ? bufferDescriptor.BaseAddress + : scalarRegisters[scalarBase.Value] | + ((ulong)scalarRegisters[scalarBase.Value + 1] << 32); + var dynamicOffset = control.DynamicOffsetRegister is { } offsetRegister && + offsetRegister < ScalarRegisterCount + ? scalarRegisters[offsetRegister] + : 0; + var immediateOffset = (ulong)(long)control.ImmediateOffsetBytes; + var byteOffset = unchecked(immediateOffset + dynamicOffset); + var address = unchecked( + baseAddress + + byteOffset) & ~3UL; + var bufferUnbound = + isBufferLoad && + (!hasBufferDescriptor || + bufferDescriptor.SizeBytes == 0 || + (scalarRegisters[scalarBase.Value] == 0 && + scalarRegisters[scalarBase.Value + 1] == 0 && + scalarBase.Value + 3 < ScalarRegisterCount && + scalarRegisters[scalarBase.Value + 2] == 0 && + scalarRegisters[scalarBase.Value + 3] == 0)); + var bufferSize = ulong.MaxValue; + if (isBufferLoad) + { + bufferSize = hasBufferDescriptor ? bufferDescriptor.SizeBytes : ulong.MaxValue; + + var key = (scalarBase.Value, bufferDescriptor.BaseAddress); + if (globalMemoryByAddress.TryGetValue(key, out var existingBinding)) + { + if (existingBinding.InstructionPcs is List instructionPcs) instructionPcs.Add(instruction.Pc); + } + else + { + TryReadGlobalMemory(ctx, bufferDescriptor.BaseAddress, bufferDescriptor.SizeBytes, out var data); + var binding = new Gen5GlobalMemoryBinding(scalarBase.Value, bufferDescriptor.BaseAddress, new List { instruction.Pc }, data); + globalMemoryByAddress.Add(key, binding); + globalMemoryBindings.Add(binding); + } + } + else if (baseAddress != 0) + { + var key = (scalarBase.Value, baseAddress); + if (globalMemoryByAddress.TryGetValue(key, out var existingBinding)) + { + if (existingBinding.InstructionPcs is List instructionPcs) + { + instructionPcs.Add(instruction.Pc); + } + } + else + { + var requiredBytes = Math.Max( + 256UL * 1024UL, + checked( + byteOffset + + (ulong)Math.Max(instruction.Destinations.Count, 1) * + sizeof(uint))); + requiredBytes = Math.Min( + (requiredBytes + 4095UL) & ~4095UL, + MaxGlobalMemoryBindingBytes); + if (!TryReadGlobalMemory( + ctx, + baseAddress, + requiredBytes, + out var data)) + { + data = []; + } + + var binding = new Gen5GlobalMemoryBinding( + scalarBase.Value, + baseAddress, + new List { instruction.Pc }, + data); + globalMemoryByAddress.Add(key, binding); + globalMemoryBindings.Add(binding); + } + } + + if (!bufferUnbound && address == 0) + { + error = FormatScalarLoadError( + "invalid-load-address", + instruction, + scalarBase.Value, + scalarRegisters, + control, + baseAddress, + dynamicOffset, + address); + return false; + } + + for (var index = 0; index < instruction.Destinations.Count; index++) + { + var destination = instruction.Destinations[index]; + if (destination.Kind != Gen5OperandKind.ScalarRegister || + destination.Value >= ScalarRegisterCount) + { + error = FormatScalarLoadError( + "invalid-scalar-destination", + instruction, + scalarBase.Value, + scalarRegisters, + control, + baseAddress, + dynamicOffset, + address); + return false; + } + + var componentOffset = unchecked(byteOffset + (ulong)(index * sizeof(uint))); + if (bufferUnbound || + isBufferLoad && + (componentOffset >= bufferSize || + bufferSize - componentOffset < sizeof(uint))) + { + scalarRegisters[destination.Value] = 0; + continue; + } + + if (!TryReadUInt32( + ctx, + address + (ulong)(index * sizeof(uint)), + out var value) && + !TryReadUserDataScalarLoad( + state, + instruction, + control, + byteOffset, + index, + out value)) + { + if (isBufferLoad) + { + scalarRegisters[destination.Value] = 0; + continue; + } + + error = FormatScalarLoadError( + "scalar-load-failed", + instruction, + scalarBase.Value, + scalarRegisters, + control, + baseAddress, + dynamicOffset, + address); + return false; + } + + scalarRegisters[destination.Value] = value; + } + + return true; + } + + private static bool TryDecodeBufferDescriptor( + IReadOnlyList scalarRegisters, + uint scalarBase, + bool strictType, + out BufferDescriptor descriptor) + { + descriptor = default; + if (scalarBase + 3 >= scalarRegisters.Count) + { + return false; + } + + var word0 = scalarRegisters[(int)scalarBase]; + var word1 = scalarRegisters[(int)scalarBase + 1]; + var word2 = scalarRegisters[(int)scalarBase + 2]; + var word3 = scalarRegisters[(int)scalarBase + 3]; + if (word0 == 0 && + word1 == 0 && + word2 == 0 && + word3 == 0) + { + descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0); + return true; + } + + var type = word3 >> 30; + if (type != 0) + { + if (strictType) + { + return false; + } + + descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0); + return true; + } + + var baseAddress = word0 | ((ulong)(word1 & 0xFFFFu) << 32); + var stride = (word1 >> 16) & 0x3FFFu; + var unifiedFormat = (word3 >> 12) & 0x7Fu; + var (dataFormat, numberFormat) = + DecodeGfx10BufferFormat(unifiedFormat); + var sizeBytes = stride == 0 + ? word2 + : (ulong)stride * word2; + descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes, numberFormat, dataFormat); + return true; + } + + private static (uint DataFormat, uint NumberFormat) + DecodeGfx10BufferFormat(uint format) => + format switch + { + 0 => (0, 0), + >= 1 and <= 6 => (1, format - 1), + >= 7 and <= 13 => (2, DecodeUnifiedNumber(format - 7, 7)), + >= 14 and <= 19 => (3, format - 14), + >= 20 and <= 22 => (4, DecodeIntegerOrFloatNumber(format - 20)), + >= 23 and <= 29 => (5, DecodeUnifiedNumber(format - 23, 7)), + >= 30 and <= 36 => (6, DecodeUnifiedNumber(format - 30, 7)), + >= 37 and <= 43 => (7, DecodeUnifiedNumber(format - 37, 7)), + >= 44 and <= 49 => (8, format - 44), + >= 50 and <= 55 => (9, format - 50), + >= 56 and <= 61 => (10, format - 56), + >= 62 and <= 64 => (11, DecodeIntegerOrFloatNumber(format - 62)), + >= 65 and <= 71 => (12, DecodeUnifiedNumber(format - 65, 7)), + >= 72 and <= 74 => (13, DecodeIntegerOrFloatNumber(format - 72)), + >= 75 and <= 77 => (14, DecodeIntegerOrFloatNumber(format - 75)), + 128 => (1, 9), + 129 => (3, 9), + 130 => (10, 9), + 132 => (34, 7), + 133 => (16, 0), + 134 => (17, 0), + 135 => (18, 0), + 136 => (19, 0), + 140 => (4, 7), + _ => (0, 0), + }; + + private static uint DecodeUnifiedNumber(uint offset, uint formatCount) => + offset == formatCount - 1 ? 7u : offset; + + private static uint DecodeIntegerOrFloatNumber(uint offset) => + offset switch + { + 0 => 4, + 1 => 5, + _ => 7, + }; + + private static bool TryReadUserDataScalarLoad( + Gen5ShaderState state, + Gen5ShaderInstruction instruction, + Gen5ScalarMemoryControl control, + ulong byteOffset, + int componentIndex, + out uint value) + { + value = 0; + if (!instruction.Opcode.StartsWith("SLoadDword", StringComparison.Ordinal) || + state.Metadata is not { } metadata || + (byteOffset & 3) != 0) + { + return false; + } + + var baseDwordOffset = byteOffset >> 2; + if (baseDwordOffset > int.MaxValue) + { + return false; + } + + var dwordOffset = (long)baseDwordOffset + componentIndex; + if (dwordOffset < 0 || + dwordOffset >= state.UserData.Count || + !IsShaderUserDataResourceOffset(metadata, (uint)dwordOffset)) + { + return false; + } + + value = state.UserData[(int)dwordOffset]; + return true; + } + + private static bool IsShaderUserDataResourceOffset( + Gen5ShaderMetadata metadata, + uint dwordOffset) + { + if (dwordOffset < metadata.ShaderResourceTableSizeDwords) + { + return true; + } + + foreach (var resource in metadata.Resources) + { + var dwordCount = resource.Kind switch + { + Gen5ShaderResourceKind.ReadOnlyTexture or + Gen5ShaderResourceKind.ReadWriteTexture => 8u, + Gen5ShaderResourceKind.Sampler or + Gen5ShaderResourceKind.ConstantBuffer => 4u, + _ => 1u, + }; + if (dwordOffset >= resource.OffsetDwords && + dwordOffset < resource.OffsetDwords + dwordCount) + { + return true; + } + } + + return metadata.DirectResources.Values.Any(offset => offset == dwordOffset); + } + + private static string FormatScalarLoadError( + string reason, + Gen5ShaderInstruction instruction, + uint scalarBase, + uint[] scalarRegisters, + Gen5ScalarMemoryControl control, + ulong baseAddress, + uint dynamicOffset, + ulong address) + { + var high = scalarBase + 1 < scalarRegisters.Length + ? scalarRegisters[scalarBase + 1] + : 0; + var dynamic = control.DynamicOffsetRegister is { } register + ? $" dyn=s{register}=0x{dynamicOffset:X8}" + : " dyn=none"; + var descriptor = string.Join( + ':', + Enumerable.Range(0, 4).Select(index => + scalarBase + index < scalarRegisters.Length + ? $"{scalarRegisters[scalarBase + index]:X8}" + : "????????")); + var words = string.Join(',', instruction.Words.Select(word => $"{word:X8}")); + return + $"{reason} pc=0x{instruction.Pc:X} op={instruction.Opcode} " + + $"words=[{words}] base=s{scalarBase}[0x{scalarRegisters[scalarBase]:X8}:0x{high:X8}] " + + $"desc=[{descriptor}] " + + $"base_addr=0x{baseAddress:X16} imm={control.ImmediateOffsetBytes}" + + $"{dynamic} address=0x{address:X16}"; + } + + private static bool TryEvaluateScalarOperand( + Gen5Operand operand, + uint[] scalarRegisters, + out uint value) + { + if (operand.Kind == Gen5OperandKind.ScalarRegister && + operand.Value < ScalarRegisterCount) + { + value = scalarRegisters[operand.Value]; + return true; + } + + if (operand.Kind == Gen5OperandKind.LiteralConstant) + { + value = operand.Value; + return true; + } + + if (operand.Kind == Gen5OperandKind.EncodedConstant) + { + return TryDecodeInlineConstant(operand.Value, out value); + } + + value = 0; + return false; + } + + private static bool TryDecodeInlineConstant(uint encoded, out uint value) + { + if (encoded == 125) + { + value = 0; + return true; + } + + if (encoded is >= 128 and <= 192) + { + value = encoded - 128; + return true; + } + + if (encoded is >= 193 and <= 208) + { + value = unchecked((uint)-(int)(encoded - 192)); + return true; + } + + var floatingPoint = encoded switch + { + 240 => 0.5f, + 241 => -0.5f, + 242 => 1.0f, + 243 => -1.0f, + 244 => 2.0f, + 245 => -2.0f, + 246 => 4.0f, + 247 => -4.0f, + 248 => 1.0f / (2.0f * MathF.PI), + _ => float.NaN, + }; + if (float.IsNaN(floatingPoint)) + { + value = 0; + return false; + } + + value = BitConverter.SingleToUInt32Bits(floatingPoint); + return true; + } + + private static uint ReverseBits(uint value) + { + value = (value >> 1 & 0x55555555u) | ((value & 0x55555555u) << 1); + value = (value >> 2 & 0x33333333u) | ((value & 0x33333333u) << 2); + value = (value >> 4 & 0x0F0F0F0Fu) | ((value & 0x0F0F0F0Fu) << 4); + value = (value >> 8 & 0x00FF00FFu) | ((value & 0x00FF00FFu) << 8); + return value >> 16 | value << 16; + } + + private static bool TryCopyRegisters( + uint[] registers, + uint start, + int count, + out IReadOnlyList values) + { + values = []; + if (start > (uint)(registers.Length - count)) + { + return false; + } + + var copy = new uint[count]; + Array.Copy(registers, (int)start, copy, 0, count); + values = copy; + return true; + } + + private static bool UsesSampler(string opcode) => + opcode.StartsWith("ImageSample", StringComparison.Ordinal) || + opcode.StartsWith("ImageGather", StringComparison.Ordinal); + + private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value) + { + Span bytes = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(address, bytes)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32LittleEndian(bytes); + return true; + } +} diff --git a/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs b/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs index 9309306..71ec844 100644 --- a/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs +++ b/src/SharpEmu.Libs/Agc/Gen5ShaderTranslator.cs @@ -4,11 +4,16 @@ using SharpEmu.HLE; using SharpEmu.Libs.VideoOut; using System.Buffers.Binary; +using System.Text; namespace SharpEmu.Libs.Agc; internal static class Gen5ShaderTranslator { + private const int MaxInstructions = 4096; + private const int MinimumUserDataDwords = 16; + private const int MaximumUserDataDwords = 256; + private static readonly uint[] FullscreenBarycentricEs = [ 0xBFA00001, 0x7E000000, 0x7E000000, 0x7E000000, @@ -29,6 +34,31 @@ internal static class Gen5ShaderTranslator 0xBF810000, ]; + private static readonly uint[] Gen5RectListExportEs = + [ + 0xBFA00001, 0x7E000000, 0x7E000000, 0x7E000000, + 0x9380FF03, 0x00080008, 0x8F6A8C00, 0x876BFF03, + 0x000000FF, 0x887C6A6B, 0xBF900009, 0x81EA6BC0, + 0x90FE6AC1, 0x36060A81, 0x2C080A81, 0x7E020280, + 0x7E0402F2, 0xD7460003, 0x03050303, 0xD7460004, + 0x03050304, 0x7E060B03, 0x7E080B04, 0xF80008CF, + 0x02010403, 0x81EA00C0, 0xBF8CFF0F, 0x90FE6AC1, + 0xF8000941, 0x00000000, 0xBF810000, + ]; + + private static readonly uint[] Gen5QuadExportEs = + [ + 0xBEFC03FF, 0x61937B18, 0xBF960000, 0xBFA00002, + 0x93EBFF03, 0x00080008, 0x8F6A8C6B, 0x8700FF03, + 0x000000FF, 0x887C6A00, 0xBF900009, 0x81EA6BC0, + 0x90FE6AC1, 0xF8000941, 0x00000000, 0x81EA00C0, + 0xBF8CFF0F, 0x90FE6AC1, 0x2C080A81, 0x36040A81, + 0x7E0002F2, 0x7E020280, 0xD5690005, 0x000208C2, + 0xD7460003, 0x03050302, 0x7E040B02, 0x7E080B04, + 0x4A0A0A81, 0x7E060B03, 0x7E0A0B05, 0xF80008CF, + 0x00000503, 0xF800020F, 0x01010402, 0xBF810000, + ]; + public static bool TryTranslate( CpuContext ctx, ulong exportShaderAddress, @@ -52,6 +82,137 @@ internal static class Gen5ShaderTranslator return true; } + public static bool IsFullscreenExportShader(CpuContext ctx, ulong exportShaderAddress) => + exportShaderAddress != 0 && + (MatchesProgram(ctx, exportShaderAddress, FullscreenBarycentricEs) || + MatchesProgram(ctx, exportShaderAddress, Gen5RectListExportEs) || + MatchesProgram(ctx, exportShaderAddress, Gen5QuadExportEs)); + + public static string Describe(CpuContext ctx, ulong exportShaderAddress, ulong pixelShaderAddress) + { + var es = TryDecodeProgram(ctx, exportShaderAddress, out var esProgram, out var esError) + ? ShaderDecodeInfo.Create(esProgram).ToString() + : $"error={esError}"; + var ps = TryDecodeProgram(ctx, pixelShaderAddress, out var psProgram, out var psError) + ? ShaderDecodeInfo.Create(psProgram).ToString() + : $"error={psError}"; + return $"es[{es}] ps[{ps}]"; + } + + public static string DescribeWords(CpuContext ctx, ulong shaderAddress) => + TryDecodeProgram(ctx, shaderAddress, out var program, out var error) + ? string.Join(',', program.Instructions.SelectMany(instruction => instruction.Words) + .Select(word => $"{word:X8}")) + : $"error={error}"; + + public static bool TryCreateState( + CpuContext ctx, + ulong shaderAddress, + ulong shaderHeaderAddress, + IReadOnlyDictionary shaderRegisters, + uint userDataBaseRegister, + out Gen5ShaderState state, + out string error, + Gen5ComputeSystemRegisters? computeSystemRegisters = null, + uint userDataScalarRegisterBase = 0) + { + state = default!; + if (!TryDecodeProgram(ctx, shaderAddress, out var program, out error)) + { + return false; + } + + Gen5ShaderMetadata? metadata = null; + if (shaderHeaderAddress != 0 && + Gen5ShaderMetadataReader.TryRead(ctx, shaderHeaderAddress, out var decodedMetadata)) + { + metadata = decodedMetadata; + } + + var userData = new uint[GetUserDataDwordCount(metadata)]; + for (uint index = 0; index < userData.Length; index++) + { + shaderRegisters.TryGetValue(userDataBaseRegister + index, out userData[index]); + } + + state = new Gen5ShaderState( + program, + userData, + metadata, + computeSystemRegisters, + userDataScalarRegisterBase); + return true; + } + + private static int GetUserDataDwordCount(Gen5ShaderMetadata? metadata) + { + var count = MinimumUserDataDwords; + if (metadata is null) + { + return count; + } + + count = Math.Max(count, checked((int)Math.Min(metadata.ExtendedUserDataSizeDwords, MaximumUserDataDwords))); + count = Math.Max(count, checked((int)Math.Min(metadata.ShaderResourceTableSizeDwords, MaximumUserDataDwords))); + + foreach (var offset in metadata.DirectResources.Values) + { + count = Math.Max(count, checked((int)Math.Min(offset + 1u, MaximumUserDataDwords))); + } + + foreach (var resource in metadata.Resources) + { + var size = resource.Kind switch + { + Gen5ShaderResourceKind.ReadOnlyTexture or Gen5ShaderResourceKind.ReadWriteTexture => 8u, + Gen5ShaderResourceKind.Sampler => 4u, + Gen5ShaderResourceKind.ConstantBuffer => 4u, + _ => 1u, + }; + count = Math.Max(count, checked((int)Math.Min(resource.OffsetDwords + size, MaximumUserDataDwords))); + } + + return count; + } + + public static string DescribeState(Gen5ShaderState state) + { + var userData = string.Join( + ',', + state.UserData.Select((value, index) => $"s{index}=0x{value:X8}")); + var systemRegisters = state.ComputeSystemRegisters is { } compute + ? $" compute[{DescribeComputeSystemRegisters(compute)}]" + : string.Empty; + if (state.Metadata is not { } metadata) + { + return + $"ud_base=s{state.UserDataScalarRegisterBase} ud[{userData}]" + + $"{systemRegisters} metadata=missing"; + } + + var direct = string.Join( + ',', + metadata.DirectResources.Select(resource => $"{resource.Key}:{resource.Value}")); + var resources = string.Join( + ',', + metadata.Resources.Select(resource => + $"{resource.Kind}[{resource.Slot}]@{resource.OffsetDwords}" + + (resource.SizeFlag ? "+" : string.Empty))); + return + $"ud_base=s{state.UserDataScalarRegisterBase} ud[{userData}]" + + $"{systemRegisters} metadata[eud={metadata.ExtendedUserDataSizeDwords}," + + $"srt={metadata.ShaderResourceTableSizeDwords},direct={direct},resources={resources}]"; + } + + private static string DescribeComputeSystemRegisters(Gen5ComputeSystemRegisters registers) => + $"x={DescribeRegister(registers.WorkGroupXRegister)}," + + $"y={DescribeRegister(registers.WorkGroupYRegister)}," + + $"z={DescribeRegister(registers.WorkGroupZRegister)}," + + $"size={DescribeRegister(registers.ThreadGroupSizeRegister)}"; + + private static string DescribeRegister(uint? register) => + register.HasValue ? $"s{register.Value}" : "-"; + private static bool MatchesProgram(CpuContext ctx, ulong address, ReadOnlySpan expected) { var bytes = new byte[expected.Length * sizeof(uint)]; @@ -70,4 +231,1627 @@ internal static class Gen5ShaderTranslator return true; } + + private static bool TryDecodeProgram( + CpuContext ctx, + ulong address, + out Gen5ShaderProgram program, + out string error) + { + program = new Gen5ShaderProgram(address, []); + error = string.Empty; + if (address == 0) + { + error = "missing"; + return false; + } + + var instructions = new List(); + var instructionCount = 0; + for (uint pc = 0; instructionCount < MaxInstructions;) + { + if (!TryReadUInt32(ctx, address + pc, out var word)) + { + error = $"read-failed pc=0x{pc:X}"; + return false; + } + + if (!TryDecodeInstruction( + ctx, + address, + pc, + word, + out var encoding, + out var name, + out var sizeDwords, + out error)) + { + return false; + } + + var words = new uint[sizeDwords]; + words[0] = word; + for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++) + { + if (!TryReadUInt32(ctx, address + pc + wordIndex * sizeof(uint), out words[wordIndex])) + { + error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}"; + return false; + } + } + + instructions.Add(CreateInstruction(pc, encoding, name, words)); + instructionCount++; + + pc += sizeDwords * sizeof(uint); + if (string.Equals(name, "SEndpgm", StringComparison.Ordinal)) + { + program = new Gen5ShaderProgram(address, instructions); + return true; + } + } + + error = "unterminated"; + return false; + } + + private static bool TryDecodeInstruction( + CpuContext ctx, + ulong baseAddress, + uint pc, + uint word, + out Gen5ShaderEncoding encoding, + out string name, + out uint sizeDwords, + out string error) + { + encoding = Gen5ShaderEncoding.Vop2; + name = string.Empty; + sizeDwords = 1; + error = string.Empty; + + if ((word & 0x80000000u) == 0) + { + var vopOpcode = (word >> 25) & 0x3F; + encoding = vopOpcode switch + { + 0x3E => Gen5ShaderEncoding.Vopc, + 0x3F => Gen5ShaderEncoding.Vop1, + _ => Gen5ShaderEncoding.Vop2, + }; + return DecodeVop2(word, out name, out sizeDwords, out error); + } + + if ((word & 0xF8000000u) == 0xC0000000u) + { + encoding = Gen5ShaderEncoding.Smrd; + return DecodeSmrd(word, out name, out sizeDwords, out error); + } + + if ((word & 0xC0000000u) == 0x80000000u) + { + var sopOpcode = (word >> 23) & 0x7F; + encoding = sopOpcode switch + { + 0x7D => Gen5ShaderEncoding.Sop1, + 0x7E => Gen5ShaderEncoding.Sopc, + 0x7F => Gen5ShaderEncoding.Sopp, + >= 0x60 => Gen5ShaderEncoding.Sopk, + _ => Gen5ShaderEncoding.Sop2, + }; + return DecodeSop(word, out name, out sizeDwords, out error); + } + + switch (word >> 26) + { + case 0x33: + encoding = Gen5ShaderEncoding.Smem; + return DecodeSmem(word, out name, out sizeDwords, out error); + case 0x32: + encoding = Gen5ShaderEncoding.Vintrp; + return DecodeVintrp(word, out name, out sizeDwords, out error); + case 0x34: + case 0x35: + encoding = Gen5ShaderEncoding.Vop3; + if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var vop3Extra)) + { + error = $"vop3-extra-read-failed pc=0x{pc:X}"; + return false; + } + + return DecodeVop3( + word, + vop3Extra, + IsVop3BOpcode((word >> 16) & 0x3FF), + out name, + out sizeDwords, + out error); + case 0x36: + encoding = Gen5ShaderEncoding.Ds; + return DecodeDs(word, out name, out sizeDwords, out error); + case 0x37: + encoding = Gen5ShaderEncoding.Flat; + return DecodeFlat(word, out name, out sizeDwords, out error); + case 0x38: + encoding = Gen5ShaderEncoding.Mubuf; + if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mubufExtra)) + { + error = $"mubuf-extra-read-failed pc=0x{pc:X}"; + return false; + } + + return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error); + case 0x3A: + encoding = Gen5ShaderEncoding.Mtbuf; + if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mtbufExtra)) + { + error = $"mtbuf-extra-read-failed pc=0x{pc:X}"; + return false; + } + + return DecodeMtbuf(word, mtbufExtra, out name, out sizeDwords, out error); + case 0x3C: + encoding = Gen5ShaderEncoding.Mimg; + return DecodeMimg(word, out name, out sizeDwords, out error); + case 0x3D: + encoding = Gen5ShaderEncoding.Smem; + return DecodeSmem(word, out name, out sizeDwords, out error); + case 0x3E: + encoding = Gen5ShaderEncoding.Exp; + name = "Exp"; + sizeDwords = 2; + return true; + case 0x3F: + encoding = Gen5ShaderEncoding.Vop3p; + return DecodeRaw2(word, "Vop3p", out name, out sizeDwords, out error); + default: + error = $"unknown-top pc=0x{pc:X} word=0x{word:X8}"; + return false; + } + } + + private static bool DecodeSop(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 23) & 0x7F; + return opcode switch + { + 0x7D => DecodeSop1(word, out name, out sizeDwords, out error), + 0x7E => DecodeSopc(word, out name, out sizeDwords, out error), + 0x7F => DecodeSopp(word, out name, out sizeDwords, out error), + >= 0x60 => DecodeSopk(word, out name, out sizeDwords, out error), + _ => DecodeSop2(word, out name, out sizeDwords, out error), + }; + } + + private static bool DecodeSop1(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 8) & 0xFF; + var src0 = word & 0xFF; + sizeDwords = 1 + (src0 == 0xFF ? 1u : 0u); + error = string.Empty; + name = opcode switch + { + 0x03 => "SMovB32", + 0x04 => "SMovB64", + 0x07 => "SNotB32", + 0x08 => "SNotB64", + 0x0A => "SWqmB64", + 0x0B => "SBrevB32", + 0x0F => "SBcnt1I32B32", + 0x13 => "SFF1I32B32", + 0x1D => "SBitset1B32", + 0x1F => "SGetpcB64", + 0x20 => "SSetpcB64", + 0x21 => "SSwappcB64", + 0x24 => "SAndSaveexecB64", + 0x25 => "SOrSaveexecB64", + 0x26 => "SXorSaveexecB64", + 0x27 => "SAndn2SaveexecB64", + 0x28 => "SOrn2SaveexecB64", + 0x29 => "SNandSaveexecB64", + 0x2A => "SNorSaveexecB64", + 0x2B => "SXnorSaveexecB64", + 0x37 => "SAndn1SaveexecB64", + 0x38 => "SOrn1SaveexecB64", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-sop1 op=0x{opcode:X2} word=0x{word:X8}", out error); + } + + private static bool DecodeSop2(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 23) & 0x7F; + var src0 = word & 0xFF; + var src1 = (word >> 8) & 0xFF; + sizeDwords = src0 == 0xFF || src1 == 0xFF ? 2u : 1u; + error = string.Empty; + name = opcode switch + { + 0x00 => "SAddU32", + 0x01 => "SSubU32", + 0x02 => "SAddI32", + 0x03 => "SSubI32", + 0x04 => "SAddcU32", + 0x05 => "SSubbU32", + 0x06 => "SMinI32", + 0x07 => "SMinU32", + 0x08 => "SMaxI32", + 0x09 => "SMaxU32", + 0x0A => "SCselectB32", + 0x0B => "SCselectB64", + 0x0E => "SAndB32", + 0x0F => "SAndB64", + 0x10 => "SOrB32", + 0x11 => "SOrB64", + 0x12 => "SXorB32", + 0x13 => "SXorB64", + 0x14 => "SAndn2B32", + 0x15 => "SAndn2B64", + 0x16 => "SOrn2B32", + 0x17 => "SOrn2B64", + 0x18 => "SNandB32", + 0x19 => "SNandB64", + 0x1A => "SNorB32", + 0x1B => "SNorB64", + 0x1C => "SXnorB32", + 0x1D => "SXnorB64", + 0x1E => "SLshlB32", + 0x1F => "SLshlB64", + 0x20 => "SLshrB32", + 0x21 => "SLshrB64", + 0x22 => "SAshrI32", + 0x23 => "SAshrI64", + 0x24 => "SBfmB32", + 0x25 => "SBfmB64", + 0x26 => "SMulI32", + 0x27 => "SBfeU32", + 0x28 => "SBfeI32", + 0x29 => "SBfeU64", + 0x2A => "SBfeI64", + 0x2D => "SAbsdiffI32", + 0x2E => "SLshl1AddU32", + 0x2F => "SLshl2AddU32", + 0x30 => "SLshl3AddU32", + 0x31 => "SLshl4AddU32", + 0x32 => "SPackLlB32B16", + 0x33 => "SPackLhB32B16", + 0x34 => "SPackHhB32B16", + 0x35 => "SMulHiU32", + 0x36 => "SMulHiI32", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-sop2 op=0x{opcode:X2}", out error); + } + + private static bool DecodeSopc(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 16) & 0x7F; + var src0 = word & 0xFF; + var src1 = (word >> 8) & 0xFF; + sizeDwords = src0 == 0xFF || src1 == 0xFF ? 2u : 1u; + error = string.Empty; + name = opcode switch + { + 0x00 => "SCmpEqI32", + 0x01 => "SCmpLgI32", + 0x02 => "SCmpGtI32", + 0x03 => "SCmpGeI32", + 0x04 => "SCmpLtI32", + 0x05 => "SCmpLeI32", + 0x06 => "SCmpEqU32", + 0x07 => "SCmpLgU32", + 0x08 => "SCmpGtU32", + 0x09 => "SCmpGeU32", + 0x0A => "SCmpLtU32", + 0x0B => "SCmpLeU32", + 0x0C => "SBitcmp0B32", + 0x0D => "SBitcmp1B32", + 0x0E => "SBitcmp0B64", + 0x0F => "SBitcmp1B64", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-sopc op=0x{opcode:X2}", out error); + } + + private static bool DecodeSopp(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 16) & 0x7F; + sizeDwords = 1; + error = string.Empty; + name = opcode switch + { + 0x00 => "SNop", + 0x01 => "SEndpgm", + 0x02 => "SBranch", + 0x04 => "SCbranchScc0", + 0x05 => "SCbranchScc1", + 0x06 => "SCbranchVccz", + 0x07 => "SCbranchVccnz", + 0x08 => "SCbranchExecz", + 0x09 => "SCbranchExecnz", + 0x0A => "SBarrier", + 0x0C => "SWaitcnt", + 0x10 => "SSendmsg", + 0x16 => "STtraceData", + 0x20 => "SInstPrefetch", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-sopp op=0x{opcode:X2}", out error); + } + + private static bool DecodeSopk(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = ((word >> 23) & 0x7F) - 0x60; + sizeDwords = 1; + error = string.Empty; + name = opcode switch + { + 0x00 => "SMovkI32", + 0x03 => "SCmpkEqI32", + 0x04 => "SCmpkLgI32", + 0x05 => "SCmpkGtI32", + 0x06 => "SCmpkGeI32", + 0x07 => "SCmpkLtI32", + 0x08 => "SCmpkLeI32", + 0x09 => "SCmpkEqU32", + 0x0A => "SCmpkLgU32", + 0x0B => "SCmpkGtU32", + 0x0C => "SCmpkGeU32", + 0x0D => "SCmpkLtU32", + 0x0E => "SCmpkLeU32", + 0x0F => "SAddkI32", + 0x10 => "SMulkI32", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-sopk op=0x{opcode:X2}", out error); + } + + private static bool DecodeVop1(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 9) & 0xFF; + var src0 = word & 0x1FF; + sizeDwords = src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u; + error = string.Empty; + name = opcode switch + { + 0x00 => "VNop", + 0x01 => "VMovB32", + 0x02 => "VReadfirstlaneB32", + 0x05 => "VCvtF32I32", + 0x06 => "VCvtF32U32", + 0x07 => "VCvtU32F32", + 0x08 => "VCvtI32F32", + 0x0A => "VCvtF16F32", + 0x0B => "VCvtF32F16", + 0x0C => "VCvtRpiI32F32", + 0x0D => "VCvtFlrI32F32", + 0x0E => "VCvtOffF32I4", + 0x11 => "VCvtF32Ubyte0", + 0x12 => "VCvtF32Ubyte1", + 0x13 => "VCvtF32Ubyte2", + 0x14 => "VCvtF32Ubyte3", + 0x20 => "VFractF32", + 0x21 => "VTruncF32", + 0x22 => "VCeilF32", + 0x23 => "VRndneF32", + 0x24 => "VFloorF32", + 0x25 => "VExpF32", + 0x27 => "VLogF32", + 0x2A => "VRcpF32", + 0x2B => "VRcpIflagF32", + 0x2E => "VRsqF32", + 0x33 => "VSqrtF32", + 0x35 => "VSinF32", + 0x36 => "VCosF32", + 0x37 => "VNotB32", + 0x38 => "VBfrevB32", + 0x3A => "VFfblB32", + 0x42 => "VMovreldB32", + 0x43 => "VMovrelsB32", + 0x44 => "VMovrelsdB32", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-vop1 op=0x{opcode:X2}", out error); + } + + private static bool DecodeVop2(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 25) & 0x3F; + if (opcode == 0x3E) + { + return DecodeVopc(word, out name, out sizeDwords, out error); + } + + if (opcode == 0x3F) + { + return DecodeVop1(word, out name, out sizeDwords, out error); + } + + var src0 = word & 0x1FF; + sizeDwords = opcode is 0x20 or 0x21 || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u; + error = string.Empty; + name = opcode switch + { + 0x01 => "VCndmaskB32", + 0x02 => "VDot2cF32F16", + 0x03 => "VAddF32", + 0x04 => "VSubF32", + 0x05 => "VSubrevF32", + 0x08 => "VMulF32", + 0x0B => "VMulU32U24", + 0x0C => "VMulHiU32U24", + 0x0F => "VMinF32", + 0x10 => "VMaxF32", + 0x11 => "VMinI32", + 0x12 => "VMaxI32", + 0x13 => "VMinU32", + 0x14 => "VMaxU32", + 0x15 => "VLshrB32", + 0x16 => "VLshrrevB32", + 0x17 => "VAshrI32", + 0x18 => "VAshrrevI32", + 0x19 => "VLshlB32", + 0x1A => "VLshlrevB32", + 0x1B => "VAndB32", + 0x1C => "VOrB32", + 0x1D => "VXorB32", + 0x1F => "VMacF32", + 0x20 => "VMadMkF32", + 0x21 => "VMadAkF32", + 0x22 => "VBcntU32B32", + 0x23 => "VMbcntLoU32B32", + 0x24 => "VMbcntHiU32B32", + 0x25 => "VAddI32", + 0x26 => "VSubI32", + 0x27 => "VSubrevI32", + 0x28 => "VAddcU32", + 0x29 => "VSubbU32", + 0x2A => "VSubbrevU32", + 0x2B => "VLdexpF32", + 0x2F => "VCvtPkrtzF16F32", + 0x30 => "VCvtPkU16U32", + 0x31 => "VCvtPkI16I32", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-vop2 op=0x{opcode:X2}", out error); + } + + private static bool DecodeVopc(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 17) & 0xFF; + var src0 = word & 0x1FF; + sizeDwords = src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u; + error = string.Empty; + name = opcode switch + { + 0x00 => "VCmpFF32", + 0x01 => "VCmpLtF32", + 0x02 => "VCmpEqF32", + 0x03 => "VCmpLeF32", + 0x04 => "VCmpGtF32", + 0x05 => "VCmpLgF32", + 0x06 => "VCmpGeF32", + 0x07 => "VCmpOF32", + 0x08 => "VCmpUF32", + 0x09 => "VCmpNgeF32", + 0x0A => "VCmpNlgF32", + 0x0B => "VCmpNgtF32", + 0x0C => "VCmpNleF32", + 0x0D => "VCmpNeqF32", + 0x0E => "VCmpNltF32", + 0x0F => "VCmpTruF32", + 0x10 => "VCmpxFF32", + 0x11 => "VCmpxLtF32", + 0x12 => "VCmpxEqF32", + 0x13 => "VCmpxLeF32", + 0x14 => "VCmpxGtF32", + 0x15 => "VCmpxLgF32", + 0x16 => "VCmpxGeF32", + 0x1D => "VCmpxNeqF32", + 0x1E => "VCmpxNltF32", + 0x80 => "VCmpFI32", + 0x81 => "VCmpLtI32", + 0x82 => "VCmpEqI32", + 0x83 => "VCmpLeI32", + 0x84 => "VCmpGtI32", + 0x85 => "VCmpNeI32", + 0x86 => "VCmpGeI32", + 0x87 => "VCmpTI32", + 0x88 => "VCmpClassF32", + 0x90 => "VCmpxFI32", + 0x91 => "VCmpxLtI32", + 0x92 => "VCmpxEqI32", + 0x93 => "VCmpxLeI32", + 0x94 => "VCmpxGtI32", + 0x95 => "VCmpxNeI32", + 0x96 => "VCmpxGeI32", + 0x97 => "VCmpxTI32", + 0xC0 => "VCmpFU32", + 0xC1 => "VCmpLtU32", + 0xC2 => "VCmpEqU32", + 0xC3 => "VCmpLeU32", + 0xC4 => "VCmpGtU32", + 0xC5 => "VCmpNeU32", + 0xC6 => "VCmpGeU32", + 0xC7 => "VCmpTU32", + 0xD0 => "VCmpxFU32", + 0xD1 => "VCmpxLtU32", + 0xD2 => "VCmpxEqU32", + 0xD3 => "VCmpxLeU32", + 0xD4 => "VCmpxGtU32", + 0xD5 => "VCmpxNeU32", + 0xD6 => "VCmpxGeU32", + 0xD7 => "VCmpxTU32", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-vopc op=0x{opcode:X2}", out error); + } + + private static bool DecodeVop3( + uint word, + uint extra, + bool isVop3B, + out string name, + out uint sizeDwords, + out string error) + { + var opcode = (word >> 16) & 0x3FF; + var src0 = extra & 0x1FF; + var src1 = (extra >> 9) & 0x1FF; + var src2 = (extra >> 18) & 0x1FF; + sizeDwords = src0 == 0xFF || src1 == 0xFF || src2 == 0xFF ? 3u : 2u; + error = string.Empty; + name = isVop3B + ? opcode switch + { + 0x30F => "VAddCoU32", + 0x310 => "VSubCoU32", + 0x319 => "VSubrevCoU32", + _ => $"Vop3bRaw{opcode:X3}", + } + : opcode switch + { + 0x101 => "VCndmaskB32", + 0x103 => "VAddF32", + 0x104 => "VSubF32", + 0x108 => "VMulF32", + 0x10F => "VMinF32", + 0x110 => "VMaxF32", + 0x11F => "VMacF32", + 0x12F => "VCvtPkrtzF16F32", + 0x141 => "VMadF32", + 0x143 => "VMadU32U24", + 0x144 => "VCubeidF32", + 0x145 => "VCubescF32", + 0x146 => "VCubetcF32", + 0x147 => "VCubemaF32", + 0x14A => "VBfiB32", + 0x14B => "VFmaF32", + 0x151 => "VMin3F32", + 0x152 => "VMin3I32", + 0x153 => "VMin3U32", + 0x154 => "VMax3F32", + 0x155 => "VMax3I32", + 0x156 => "VMax3U32", + 0x157 => "VMed3F32", + 0x158 => "VMed3I32", + 0x159 => "VMed3U32", + 0x15A => "VSadU8", + 0x15B => "VSadHiU8", + 0x15C => "VSadU16", + 0x15D => "VSadU32", + 0x15E => "VCvtPkU8F32", + 0x148 => "VBfeU32", + 0x169 => "VMulLoU32", + 0x16A => "VMulHiU32", + 0x16B => "VMulLoI32", + 0x360 => "VMadU32U16", + 0x361 => "VMulLoU32", + 0x362 => "VLdexpF32", + 0x346 => "VLshlAddU32", + 0x347 => "VAddLshlU32", + 0x36D => "VAdd3U32", + 0x36F => "VLshlOrU32", + 0x371 => "VAndOrB32", + 0x372 => "VOr3U32", + 0x377 => "VPermlane16B32", + 0x378 => "VPermlanex16B32", + _ => $"Vop3Raw{opcode:X3}", + }; + + return FinishDecode(name, $"unknown-vop3 op=0x{opcode:X3}", out error); + } + + private static bool IsVop3BOpcode(uint opcode) => + opcode is 0x16D or 0x16E or 0x176 or 0x177 or 0x30F or 0x310 or 0x319; + + private static bool DecodeRaw2( + uint word, + string prefix, + out string name, + out uint sizeDwords, + out string error) + { + name = $"{prefix}Raw{word >> 24:X2}"; + sizeDwords = 2; + error = string.Empty; + return true; + } + + private static bool DecodeDs( + uint word, + out string name, + out uint sizeDwords, + out string error) + { + var opcode = (word >> 18) & 0xFF; + sizeDwords = 2; + error = string.Empty; + name = opcode switch + { + 0x0D => "DsWriteB32", + 0x0E => "DsWrite2B32", + 0x0F => "DsWrite2St64B32", + 0x35 => "DsSwizzleB32", + 0x36 => "DsReadB32", + 0x37 => "DsRead2B32", + 0x38 => "DsRead2St64B32", + 0x4D => "DsWriteB64", + _ => string.Empty, + }; + + return FinishDecode( + name, + $"unknown-ds op=0x{opcode:X2} word=0x{word:X8}", + out error); + } + + private static bool DecodeBuffer( + uint word, + string prefix, + out string name, + out uint sizeDwords, + out string error) + { + var opcode = (word >> 18) & 0x7F; + name = $"{prefix}Raw{opcode:X2}"; + sizeDwords = 2; + error = string.Empty; + return true; + } + + private static bool DecodeMtbuf( + uint word, + uint extra, + out string name, + out uint sizeDwords, + out string error) + { + var opcode = (word >> 16) & 0x7; + name = opcode switch + { + 0x00 => "TBufferLoadFormatX", + 0x01 => "TBufferLoadFormatXy", + 0x02 => "TBufferLoadFormatXyz", + 0x03 => "TBufferLoadFormatXyzw", + 0x04 => "TBufferStoreFormatX", + 0x05 => "TBufferStoreFormatXy", + 0x06 => "TBufferStoreFormatXyz", + 0x07 => "TBufferStoreFormatXyzw", + _ => string.Empty, + }; + sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u; + error = string.Empty; + return true; + } + + private static bool DecodeMubuf( + uint word, + uint extra, + out string name, + out uint sizeDwords, + out string error) + { + var opcode = (word >> 18) & 0x7F; + name = opcode switch + { + 0x00 => "BufferLoadFormatX", + 0x01 => "BufferLoadFormatXy", + 0x02 => "BufferLoadFormatXyz", + 0x03 => "BufferLoadFormatXyzw", + 0x0C => "BufferLoadDword", + 0x0D => "BufferLoadDwordx2", + 0x0E => "BufferLoadDwordx4", + 0x0F => "BufferLoadDwordx3", + 0x1C => "BufferStoreDword", + 0x1D => "BufferStoreDwordx2", + 0x1E => "BufferStoreDwordx4", + 0x1F => "BufferStoreDwordx3", + 0x32 => "BufferAtomicAdd", + _ => $"MubufRaw{opcode:X2}", + }; + sizeDwords = (extra >> 24) == 0xFF ? 3u : 2u; + error = string.Empty; + return true; + } + + private static bool DecodeFlat( + uint word, + out string name, + out uint sizeDwords, + out string error) + { + var segment = (word >> 14) & 0x3; + var opcode = (word >> 18) & 0x7F; + sizeDwords = 2; + error = string.Empty; + name = segment == 0x2 + ? opcode switch + { + 0x0C => "GlobalLoadDword", + 0x0D => "GlobalLoadDwordx2", + 0x0E => "GlobalLoadDwordx4", + 0x0F => "GlobalLoadDwordx3", + _ => string.Empty, + } + : string.Empty; + + return FinishDecode( + name, + $"unknown-flat segment=0x{segment:X1} op=0x{opcode:X2} word=0x{word:X8}", + out error); + } + + private static bool DecodeSmrd(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 22) & 0x1F; + var offset = word & 0xFF; + var immediateOffset = ((word >> 8) & 1) != 0; + sizeDwords = !immediateOffset && offset == 0xFF ? 2u : 1u; + error = string.Empty; + name = opcode switch + { + 0x00 => "SLoadDword", + 0x01 => "SLoadDwordx2", + 0x02 => "SLoadDwordx4", + 0x03 => "SLoadDwordx8", + 0x04 => "SLoadDwordx16", + 0x08 => "SBufferLoadDword", + 0x09 => "SBufferLoadDwordx2", + 0x0A => "SBufferLoadDwordx4", + 0x0B => "SBufferLoadDwordx8", + 0x0C => "SBufferLoadDwordx16", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-smrd op=0x{opcode:X2}", out error); + } + + private static bool DecodeSmem(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 18) & 0xFF; + sizeDwords = 2; + error = string.Empty; + name = opcode switch + { + 0x00 => "SLoadDword", + 0x01 => "SLoadDwordx2", + 0x02 => "SLoadDwordx4", + 0x03 => "SLoadDwordx8", + 0x04 => "SLoadDwordx16", + 0x08 => "SBufferLoadDword", + 0x09 => "SBufferLoadDwordx2", + 0x0A => "SBufferLoadDwordx4", + 0x0B => "SBufferLoadDwordx8", + 0x0C => "SBufferLoadDwordx16", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-smem op=0x{opcode:X2}", out error); + } + + private static bool DecodeMimg(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 18) & 0x7F; + sizeDwords = 2 + ((word >> 1) & 0x3); + error = string.Empty; + name = opcode switch + { + 0x00 => "ImageLoad", + 0x01 => "ImageLoadMip", + 0x08 => "ImageStore", + 0x09 => "ImageStoreMip", + 0x0E => "ImageGetResinfo", + 0x10 => "ImageAtomicCmpswap", + 0x1C => "ImageAtomicDec", + 0x20 => "ImageSample", + 0x24 => "ImageSampleL", + 0x25 => "ImageSampleB", + 0x27 => "ImageSampleLz", + 0x2F => "ImageSampleCLz", + 0x30 => "ImageSampleO", + 0x34 => "ImageSampleLO", + 0x37 => "ImageSampleLzO", + 0x40 => "ImageGather4", + 0x47 => "ImageGather4Lz", + 0x48 => "ImageGather4C", + 0x4E => "ImageGather4CBCl", + 0x5F => "ImageGather4CLzO", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-mimg op=0x{opcode:X2}", out error); + } + + private static bool DecodeVintrp(uint word, out string name, out uint sizeDwords, out string error) + { + var opcode = (word >> 16) & 0x3; + sizeDwords = 1; + error = string.Empty; + name = opcode switch + { + 0x00 => "VInterpP1F32", + 0x01 => "VInterpP2F32", + 0x02 => "VInterpMovF32", + _ => string.Empty, + }; + + return FinishDecode(name, $"unknown-vintrp op=0x{opcode:X1}", out error); + } + + private static bool FinishDecode(string name, string decodeError, out string error) + { + error = string.Empty; + if (name.Length != 0) + { + return true; + } + + error = decodeError; + return false; + } + + private static string ClassifyInstruction(string name) + { + if (name.StartsWith("Image", StringComparison.Ordinal)) + { + return "image"; + } + + if (name.StartsWith("Global", StringComparison.Ordinal)) + { + return "global_memory"; + } + + if (name.StartsWith("VInterp", StringComparison.Ordinal)) + { + return "interp"; + } + + if (string.Equals(name, "Exp", StringComparison.Ordinal)) + { + return "export"; + } + + if (name.StartsWith("SLoad", StringComparison.Ordinal) || + name.StartsWith("SBufferLoad", StringComparison.Ordinal)) + { + return "scalar_load"; + } + + if (name.StartsWith('V')) + { + return "valu"; + } + + if (name.StartsWith('S')) + { + return "salu"; + } + + return "other"; + } + + private static bool IsMimgInstruction(string name) => + name.StartsWith("Image", StringComparison.Ordinal); + + internal static bool IsStorageImageOperation(string name) => + name.StartsWith("ImageStore", StringComparison.Ordinal) || + name.StartsWith("ImageAtomic", StringComparison.Ordinal); + + private static Gen5ShaderInstruction CreateInstruction( + uint pc, + Gen5ShaderEncoding encoding, + string opcode, + uint[] words) + { + var word = words[0]; + var isSdwa = + encoding is Gen5ShaderEncoding.Vop1 or Gen5ShaderEncoding.Vop2 or Gen5ShaderEncoding.Vopc && + (word & 0x1FF) == 0xF9; + var isDpp = + encoding is Gen5ShaderEncoding.Vop1 or Gen5ShaderEncoding.Vop2 or Gen5ShaderEncoding.Vopc && + (word & 0x1FF) == 0xFA; + var literal = !isSdwa && !isDpp && words.Length > MinimumEncodingDwords(encoding) + ? words[^1] + : (uint?)null; + IReadOnlyList sources = []; + IReadOnlyList destinations = []; + Gen5InstructionControl? control = null; + + switch (encoding) + { + case Gen5ShaderEncoding.Sop1: + sources = [Gen5Operand.Source(word & 0xFF, literal)]; + destinations = [Gen5Operand.Scalar((word >> 16) & 0x7F)]; + break; + case Gen5ShaderEncoding.Sop2: + sources = + [ + Gen5Operand.Source(word & 0xFF, literal), + Gen5Operand.Source((word >> 8) & 0xFF, literal), + ]; + destinations = [Gen5Operand.Scalar((word >> 16) & 0x7F)]; + break; + case Gen5ShaderEncoding.Sopc: + sources = + [ + Gen5Operand.Source(word & 0xFF, literal), + Gen5Operand.Source((word >> 8) & 0xFF, literal), + ]; + break; + case Gen5ShaderEncoding.Sopk: + sources = [new Gen5Operand(Gen5OperandKind.EncodedConstant, word & 0xFFFF)]; + destinations = [Gen5Operand.Scalar((word >> 16) & 0x7F)]; + break; + case Gen5ShaderEncoding.Smrd: + { + var scalarBase = ((word >> 9) & 0x3F) * 2; + var scalarDestination = (word >> 15) & 0x7F; + var immediate = ((word >> 8) & 1) != 0; + var offset = word & 0xFF; + var count = ScalarLoadDwordCount(opcode); + uint? dynamicOffsetRegister = null; + var immediateOffsetBytes = 0; + if (immediate) + { + immediateOffsetBytes = checked((int)(offset * sizeof(uint))); + } + else if (offset == 0xFF && literal.HasValue) + { + immediateOffsetBytes = unchecked((int)literal.Value); + } + else + { + dynamicOffsetRegister = offset; + } + + sources = dynamicOffsetRegister.HasValue + ? [Gen5Operand.Scalar(scalarBase), Gen5Operand.Scalar(dynamicOffsetRegister.Value)] + : [Gen5Operand.Scalar(scalarBase)]; + destinations = Enumerable + .Range((int)scalarDestination, checked((int)count)) + .Select(index => Gen5Operand.Scalar((uint)index)) + .ToArray(); + control = new Gen5ScalarMemoryControl( + count, + immediateOffsetBytes, + dynamicOffsetRegister); + break; + } + case Gen5ShaderEncoding.Smem: + { + var extra = words[1]; + var scalarBase = (word & 0x3F) * 2; + var scalarDestination = (word >> 6) & 0x7F; + var scalarOffset = (extra >> 25) & 0x7F; + var offset = SignExtend(extra & 0x1FFFFF, 21); + var count = ScalarLoadDwordCount(opcode); + sources = + [ + Gen5Operand.Scalar(scalarBase), + Gen5Operand.Scalar(scalarOffset), + ]; + destinations = Enumerable + .Range((int)scalarDestination, checked((int)count)) + .Select(index => Gen5Operand.Scalar((uint)index)) + .ToArray(); + control = new Gen5ScalarMemoryControl(count, offset, scalarOffset); + break; + } + case Gen5ShaderEncoding.Vop1: + if (isDpp) + { + var extra = words[1]; + sources = [Gen5Operand.Vector(extra & 0xFF)]; + control = CreateDppControl(extra); + } + else if (isSdwa) + { + var extra = words[1]; + var source0 = (extra & 0xFF) + + ((((extra >> 23) & 1) == 0) ? 256u : 0u); + sources = [Gen5Operand.Source(source0)]; + control = new Gen5SdwaControl( + (extra >> 8) & 0x7, + (extra >> 16) & 0x7, + 6, + (extra >> 21) & 1, + (extra >> 20) & 1, + (extra >> 14) & 0x3, + ((extra >> 13) & 1) != 0); + } + else + { + sources = [Gen5Operand.Source(word & 0x1FF, literal)]; + } + + destinations = [Gen5Operand.Vector((word >> 17) & 0xFF)]; + break; + case Gen5ShaderEncoding.Vop2: + if (isDpp) + { + var extra = words[1]; + sources = + [ + Gen5Operand.Vector(extra & 0xFF), + Gen5Operand.Vector((word >> 9) & 0xFF), + ]; + control = CreateDppControl(extra); + } + else if (isSdwa) + { + var extra = words[1]; + var source0 = (extra & 0xFF) + ((((extra >> 23) & 1) == 0) ? 256u : 0u); + var source1 = + ((word >> 9) & 0xFF) + + ((((extra >> 31) & 1) == 0) ? 256u : 0u); + sources = + [ + Gen5Operand.Source(source0), + Gen5Operand.Source(source1), + ]; + control = new Gen5SdwaControl( + (extra >> 8) & 0x7, + (extra >> 16) & 0x7, + (extra >> 24) & 0x7, + ((extra >> 21) & 1) | (((extra >> 29) & 1) << 1), + ((extra >> 20) & 1) | (((extra >> 28) & 1) << 1), + (extra >> 14) & 0x3, + ((extra >> 13) & 1) != 0); + } + else + { + sources = + [ + Gen5Operand.Source(word & 0x1FF, literal), + Gen5Operand.Vector((word >> 9) & 0xFF), + ]; + if (opcode == "VMadMkF32" && literal.HasValue) + { + sources = + [ + sources[0], + new Gen5Operand(Gen5OperandKind.LiteralConstant, literal.Value), + sources[1], + ]; + } + else if (opcode == "VMadAkF32" && literal.HasValue) + { + sources = + [ + .. sources, + new Gen5Operand(Gen5OperandKind.LiteralConstant, literal.Value), + ]; + } + } + + destinations = [Gen5Operand.Vector((word >> 17) & 0xFF)]; + break; + case Gen5ShaderEncoding.Vopc: + if (isDpp) + { + var extra = words[1]; + sources = + [ + Gen5Operand.Vector(extra & 0xFF), + Gen5Operand.Vector((word >> 9) & 0xFF), + ]; + control = CreateDppControl(extra); + } + else if (isSdwa) + { + var extra = words[1]; + var source0 = (extra & 0xFF) + + ((((extra >> 23) & 1) == 0) ? 256u : 0u); + var source1 = + ((word >> 9) & 0xFF) + + ((((extra >> 31) & 1) == 0) ? 256u : 0u); + sources = + [ + Gen5Operand.Source(source0), + Gen5Operand.Source(source1), + ]; + control = new Gen5SdwaControl( + (extra >> 8) & 0x7, + (extra >> 16) & 0x7, + (extra >> 24) & 0x7, + ((extra >> 21) & 1) | (((extra >> 29) & 1) << 1), + ((extra >> 20) & 1) | (((extra >> 28) & 1) << 1), + (extra >> 14) & 0x3, + ((extra >> 13) & 1) != 0); + } + else + { + sources = + [ + Gen5Operand.Source(word & 0x1FF, literal), + Gen5Operand.Vector((word >> 9) & 0xFF), + ]; + } + break; + case Gen5ShaderEncoding.Vop3: + { + var extra = words[1]; + sources = + [ + Gen5Operand.Source(extra & 0x1FF, literal), + Gen5Operand.Source((extra >> 9) & 0x1FF, literal), + Gen5Operand.Source((extra >> 18) & 0x1FF, literal), + ]; + destinations = [Gen5Operand.Vector(word & 0xFF)]; + var isVop3B = IsVop3BOpcode((word >> 16) & 0x3FF); + control = new Gen5Vop3Control( + isVop3B ? 0 : (word >> 8) & 0x7, + (extra >> 29) & 0x7, + (extra >> 27) & 0x3, + ((word >> 15) & 1) != 0, + isVop3B ? (word >> 8) & 0x7F : null); + break; + } + case Gen5ShaderEncoding.Ds: + { + var extra = words[1]; + var vectorAddress = extra & 0xFF; + var vectorData0 = (extra >> 8) & 0xFF; + var vectorData1 = (extra >> 16) & 0xFF; + var vectorDestination = (extra >> 24) & 0xFF; + control = new Gen5DataShareControl( + word & 0xFF, + (word >> 8) & 0xFF, + ((word >> 17) & 1) != 0); + sources = opcode switch + { + "DsWriteB32" => [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Vector(vectorData0), + ], + "DsWriteB64" => [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Vector(vectorData0), + Gen5Operand.Vector(vectorData0 + 1), + ], + "DsWrite2B32" or "DsWrite2St64B32" => [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Vector(vectorData0), + Gen5Operand.Vector(vectorData1), + ], + "DsSwizzleB32" => [Gen5Operand.Vector(vectorData0)], + _ => [Gen5Operand.Vector(vectorAddress)], + }; + destinations = opcode switch + { + "DsReadB32" or "DsSwizzleB32" => [ + Gen5Operand.Vector(vectorDestination), + ], + "DsRead2B32" or "DsRead2St64B32" => [ + Gen5Operand.Vector(vectorDestination), + Gen5Operand.Vector(vectorDestination + 1), + ], + _ => [], + }; + break; + } + case Gen5ShaderEncoding.Vintrp: + sources = [Gen5Operand.Vector(word & 0xFF)]; + destinations = [Gen5Operand.Vector((word >> 18) & 0xFF)]; + control = new Gen5InterpolationControl( + (word >> 10) & 0x3F, + (word >> 8) & 0x3); + break; + case Gen5ShaderEncoding.Flat: + { + var extra = words[1]; + var vectorAddress = extra & 0xFF; + var vectorData = (extra >> 8) & 0xFF; + var scalarAddress = (extra >> 16) & 0x7F; + var dwordCount = opcode switch + { + "GlobalLoadDword" => 1u, + "GlobalLoadDwordx2" => 2u, + "GlobalLoadDwordx3" => 3u, + "GlobalLoadDwordx4" => 4u, + _ => 0u, + }; + sources = + [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Scalar(scalarAddress), + ]; + destinations = Enumerable + .Range((int)vectorData, checked((int)dwordCount)) + .Select(index => Gen5Operand.Vector((uint)index)) + .ToArray(); + control = new Gen5GlobalMemoryControl( + dwordCount, + vectorAddress, + vectorData, + scalarAddress, + SignExtend(word & 0x1FFF, 13), + ((word >> 16) & 1) != 0, + ((word >> 17) & 1) != 0); + break; + } + case Gen5ShaderEncoding.Mubuf: + { + var extra = words[1]; + var vectorAddress = extra & 0xFF; + var vectorData = (extra >> 8) & 0xFF; + var scalarResource = ((extra >> 16) & 0x1F) * 4; + var scalarOffset = (extra >> 24) & 0xFF; + var dwordCount = opcode switch + { + "BufferLoadFormatX" => 1u, + "BufferLoadFormatXy" => 2u, + "BufferLoadFormatXyz" => 3u, + "BufferLoadFormatXyzw" => 4u, + "BufferLoadDword" => 1u, + "BufferLoadDwordx2" => 2u, + "BufferLoadDwordx3" => 3u, + "BufferLoadDwordx4" => 4u, + "BufferStoreDword" => 1u, + "BufferStoreDwordx2" => 2u, + "BufferStoreDwordx3" => 3u, + "BufferStoreDwordx4" => 4u, + "BufferAtomicAdd" => 1u, + _ => 0u, + }; + sources = + [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Scalar(scalarResource), + Gen5Operand.Source(scalarOffset, literal), + ]; + destinations = Enumerable + .Range((int)vectorData, checked((int)dwordCount)) + .Select(index => Gen5Operand.Vector((uint)index)) + .ToArray(); + control = new Gen5BufferMemoryControl( + dwordCount, + vectorAddress, + vectorData, + scalarResource, + (int)(word & 0xFFF), + ((word >> 13) & 1) != 0, + ((word >> 12) & 1) != 0, + ((word >> 14) & 1) != 0, + ((extra >> 22) & 1) != 0); + break; + } + case Gen5ShaderEncoding.Mtbuf: + { + var extra = words[1]; + var vectorAddress = extra & 0xFF; + var vectorData = (extra >> 8) & 0xFF; + var scalarResource = ((extra >> 16) & 0x1F) * 4; + var scalarOffset = (extra >> 24) & 0xFF; + var dwordCount = opcode switch + { + "TBufferLoadFormatX" => 1u, + "TBufferLoadFormatXy" => 2u, + "TBufferLoadFormatXyz" => 3u, + "TBufferLoadFormatXyzw" => 4u, + _ => 0u, + }; + sources = + [ + Gen5Operand.Vector(vectorAddress), + Gen5Operand.Scalar(scalarResource), + Gen5Operand.Source(scalarOffset, literal), + ]; + destinations = Enumerable + .Range((int)vectorData, checked((int)dwordCount)) + .Select(index => Gen5Operand.Vector((uint)index)) + .ToArray(); + control = new Gen5BufferMemoryControl( + dwordCount, + vectorAddress, + vectorData, + scalarResource, + (int)(word & 0xFFF), + ((word >> 13) & 1) != 0, + ((word >> 12) & 1) != 0, + ((word >> 14) & 1) != 0, + ((extra >> 22) & 1) != 0); + break; + } + case Gen5ShaderEncoding.Mimg: + { + var extra = words[1]; + var vectorAddress = extra & 0xFF; + var vectorData = (extra >> 8) & 0xFF; + var scalarResource = ((extra >> 16) & 0x1F) * 4; + var scalarSampler = ((extra >> 21) & 0x1F) * 4; + var addressRegisters = new List(1 + Math.Max(0, words.Length - 2) * 4) + { + vectorAddress, + }; + for (var wordIndex = 2; wordIndex < words.Length; wordIndex++) + { + for (var shift = 0; shift < 32; shift += 8) + { + addressRegisters.Add((words[wordIndex] >> shift) & 0xFF); + } + } + + var imageSources = new List(addressRegisters.Count + 2); + foreach (var addressRegister in addressRegisters) + { + imageSources.Add(Gen5Operand.Vector(addressRegister)); + } + + imageSources.Add(Gen5Operand.Scalar(scalarResource)); + imageSources.Add(Gen5Operand.Scalar(scalarSampler)); + sources = imageSources; + destinations = opcode.StartsWith("ImageStore", StringComparison.Ordinal) + ? [] + : [Gen5Operand.Vector(vectorData)]; + var dimension = (word >> 3) & 0x7; + control = new Gen5ImageControl( + (word >> 8) & 0xF, + vectorAddress, + addressRegisters, + vectorData, + scalarResource, + scalarSampler, + dimension, + dimension is 4 or 5 or 7, + ((word >> 13) & 1) != 0, + ((word >> 25) & 1) != 0); + break; + } + case Gen5ShaderEncoding.Exp: + { + var extra = words[1]; + sources = + [ + Gen5Operand.Vector(extra & 0xFF), + Gen5Operand.Vector((extra >> 8) & 0xFF), + Gen5Operand.Vector((extra >> 16) & 0xFF), + Gen5Operand.Vector((extra >> 24) & 0xFF), + ]; + control = new Gen5ExportControl( + (word >> 4) & 0x3F, + word & 0xF, + ((word >> 10) & 1) != 0, + ((word >> 11) & 1) != 0, + ((word >> 12) & 1) != 0); + break; + } + } + + return new Gen5ShaderInstruction(pc, encoding, opcode, words, sources, destinations, control); + } + + private static Gen5DppControl CreateDppControl(uint word) => + new( + (word >> 8) & 0x1FF, + ((word >> 18) & 1) != 0, + ((word >> 19) & 1) != 0, + ((word >> 21) & 1) | (((word >> 23) & 1) << 1), + ((word >> 20) & 1) | (((word >> 22) & 1) << 1), + (word >> 24) & 0xF, + (word >> 28) & 0xF); + + private static int MinimumEncodingDwords(Gen5ShaderEncoding encoding) => encoding switch + { + Gen5ShaderEncoding.Vop3 or + Gen5ShaderEncoding.Smem or + Gen5ShaderEncoding.Mubuf or + Gen5ShaderEncoding.Mtbuf or + Gen5ShaderEncoding.Ds or + Gen5ShaderEncoding.Flat or + Gen5ShaderEncoding.Vop3p or + Gen5ShaderEncoding.Mimg or + Gen5ShaderEncoding.Exp => 2, + _ => 1, + }; + + private static uint ScalarLoadDwordCount(string opcode) => opcode switch + { + "SLoadDword" or "SBufferLoadDword" => 1, + "SLoadDwordx2" or "SBufferLoadDwordx2" => 2, + "SLoadDwordx4" or "SBufferLoadDwordx4" => 4, + "SLoadDwordx8" or "SBufferLoadDwordx8" => 8, + "SLoadDwordx16" or "SBufferLoadDwordx16" => 16, + _ => 0, + }; + + private static int SignExtend(uint value, int bits) + { + var shift = 32 - bits; + return (int)(value << shift) >> shift; + } + + private static void AddFeatureCount(Dictionary counts, string key) + { + counts.TryGetValue(key, out var count); + counts[key] = count + 1; + } + + private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value) + { + Span bytes = stackalloc byte[sizeof(uint)]; + if (!ctx.Memory.TryRead(address, bytes)) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32LittleEndian(bytes); + return true; + } + + private readonly record struct ShaderDecodeInfo( + int InstructionCount, + Dictionary Counts, + Dictionary FeatureCounts, + Dictionary MimgCounts, + List Details) + { + public static ShaderDecodeInfo Create(Gen5ShaderProgram program) + { + var counts = new Dictionary(StringComparer.Ordinal); + var featureCounts = new Dictionary(StringComparer.Ordinal); + var mimgCounts = new Dictionary(StringComparer.Ordinal); + var details = new List(); + foreach (var instruction in program.Instructions) + { + AddFeatureCount(counts, instruction.Opcode); + AddFeatureCount(featureCounts, ClassifyInstruction(instruction.Opcode)); + if (instruction.Control is Gen5ImageControl image) + { + AddFeatureCount(mimgCounts, $"{instruction.Opcode}/dmask=0x{image.Dmask:X}"); + } + + if (details.Count < 16 && DescribeInstruction(instruction) is { } detail) + { + details.Add(detail); + } + } + + return new ShaderDecodeInfo( + program.Instructions.Count, + counts, + featureCounts, + mimgCounts, + details); + } + + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append("ins="); + builder.Append(InstructionCount); + AppendCounts(builder, " features=", FeatureCounts, 16); + builder.Append(" ops="); + AppendCounts(builder, string.Empty, Counts, 128); + AppendCounts(builder, " mimg=", MimgCounts, 12); + AppendDetails(builder, Details, 10); + + return builder.ToString(); + } + + private static string? DescribeInstruction(Gen5ShaderInstruction instruction) + { + if (instruction.Control is Gen5ImageControl image) + { + var addressRegisters = string.Join( + '/', + image.AddressRegisters.Select(register => $"v{register}")); + return + $"{instruction.Opcode}@0x{instruction.Pc:X}:dm=0x{image.Dmask:X}," + + $"va={addressRegisters},vd=v{image.VectorData}," + + $"sr=s{image.ScalarResource},ss=s{image.ScalarSampler}," + + $"dim={image.Dimension},da={(image.IsArray ? 1 : 0)}," + + $"glc={(image.Glc ? 1 : 0)}," + + $"slc={(image.Slc ? 1 : 0)}"; + } + + if (instruction.Control is Gen5ExportControl export) + { + return + $"Exp@0x{instruction.Pc:X}:target=0x{export.Target:X}," + + $"en=0x{export.EnableMask:X},compr={(export.Compressed ? 1 : 0)}," + + $"done={(export.Done ? 1 : 0)},vm={(export.ValidMask ? 1 : 0)}," + + $"src={string.Join('/', instruction.Sources)}"; + } + + if (instruction.Control is Gen5InterpolationControl interpolation) + { + return + $"{instruction.Opcode}@0x{instruction.Pc:X}:" + + $"attr={interpolation.Attribute},chan={interpolation.Channel}," + + $"src={instruction.Sources[0]},dst={instruction.Destinations[0]}"; + } + + return null; + } + + private static void AppendCounts( + StringBuilder builder, + string prefix, + Dictionary counts, + int limit) + { + if (counts.Count == 0) + { + return; + } + + builder.Append(prefix); + var written = 0; + foreach (var (name, count) in counts) + { + if (written != 0) + { + builder.Append(','); + } + + builder.Append(name); + builder.Append(':'); + builder.Append(count); + written++; + if (written == limit && counts.Count > written) + { + builder.Append(",..."); + break; + } + } + } + + private static void AppendDetails(StringBuilder builder, List details, int limit) + { + if (details.Count == 0) + { + return; + } + + builder.Append(" detail="); + var written = 0; + foreach (var detail in details) + { + if (written != 0) + { + builder.Append(';'); + } + + builder.Append(detail); + written++; + if (written == limit && details.Count > written) + { + builder.Append(";..."); + break; + } + } + } + } } diff --git a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs new file mode 100644 index 0000000..5c01072 --- /dev/null +++ b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.Alu.cs @@ -0,0 +1,2374 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.Agc; + +internal static partial class Gen5SpirvTranslator +{ + private sealed partial class CompilationContext + { + private bool TryEmitVectorAlu( + Gen5ShaderInstruction instruction, + out string error) + { + error = string.Empty; + if (instruction.Opcode == "VNop") + { + return true; + } + + if (instruction.Opcode.StartsWith("VCmp", StringComparison.Ordinal)) + { + return TryEmitVectorCompare(instruction, out error); + } + + if (!TryGetVectorDestination(instruction, out var destination)) + { + error = "missing vector destination"; + return false; + } + + uint result; + switch (instruction.Opcode) + { + case "VMovB32": + case "VReadlaneB32": + case "VReadfirstlaneB32": + result = GetRawSource(instruction, 0); + break; + case "VCndmaskB32": + { + var condition = instruction.Sources.Count > 2 + ? IsCurrentLaneSet(GetRawSource64(instruction, 2)) + : Load(_boolType, _vcc); + result = _module.AddInstruction( + SpirvOp.Select, + _uintType, + condition, + GetRawSource(instruction, 1), + GetRawSource(instruction, 0)); + break; + } + case "VCvtU32F32": + result = _module.AddInstruction( + SpirvOp.ConvertFToU, + _uintType, + GetFloatSource(instruction, 0)); + break; + case "VCvtI32F32": + case "VCvtRpiI32F32": + case "VCvtFlrI32F32": + { + var source = GetFloatSource(instruction, 0); + if (instruction.Opcode == "VCvtRpiI32F32") + { + source = Ext(9, _floatType, source); + } + else if (instruction.Opcode == "VCvtFlrI32F32") + { + source = Ext(8, _floatType, source); + } + + result = Bitcast( + _uintType, + _module.AddInstruction(SpirvOp.ConvertFToS, _intType, source)); + break; + } + case "VCvtF32I32": + { + var signed = Bitcast(_intType, GetRawSource(instruction, 0)); + result = Bitcast( + _uintType, + _module.AddInstruction(SpirvOp.ConvertSToF, _floatType, signed)); + break; + } + case "VCvtF32U32": + result = Bitcast( + _uintType, + _module.AddInstruction( + SpirvOp.ConvertUToF, + _floatType, + GetRawSource(instruction, 0))); + break; + case "VCvtF32Ubyte0": + case "VCvtF32Ubyte1": + case "VCvtF32Ubyte2": + case "VCvtF32Ubyte3": + { + var shift = (uint)(instruction.Opcode[^1] - '0') * 8; + var raw = ShiftRightLogical(GetRawSource(instruction, 0), UInt(shift)); + raw = BitwiseAnd(raw, UInt(0xFF)); + result = Bitcast( + _uintType, + _module.AddInstruction(SpirvOp.ConvertUToF, _floatType, raw)); + break; + } + case "VCvtF16F32": + { + var vector = _module.AddInstruction( + SpirvOp.CompositeConstruct, + _vec2Type, + GetFloatSource(instruction, 0), + Float(0)); + result = BitwiseAnd(Ext(58, _uintType, vector), UInt(0xFFFF)); + break; + } + case "VCvtF32F16": + { + var unpacked = Ext(62, _vec2Type, GetRawSource(instruction, 0)); + var value = _module.AddInstruction( + SpirvOp.CompositeExtract, + _floatType, + unpacked, + 0); + result = Bitcast(_uintType, value); + break; + } + case "VCvtOffF32I4": + result = EmitCvtOffF32I4(instruction); + break; + case "VCvtPkU8F32": + { + var converted = _module.AddInstruction( + SpirvOp.ConvertFToU, + _uintType, + GetFloatSource(instruction, 0)); + var offset = ShiftLeftLogical( + BitwiseAnd(GetRawSource(instruction, 1), UInt(3)), + UInt(3)); + result = _module.AddInstruction( + SpirvOp.BitFieldInsert, + _uintType, + GetRawSource(instruction, 2), + converted, + offset, + UInt(8)); + break; + } + case "VRcpF32": + case "VRcpIflagF32": + result = EmitFloatResult( + instruction, + _module.AddInstruction( + SpirvOp.FDiv, + _floatType, + Float(1), + GetFloatSource(instruction, 0))); + break; + case "VLogF32": + result = EmitFloatResult( + instruction, + Ext(30, _floatType, GetFloatSource(instruction, 0))); + break; + case "VLdexpF32": + result = EmitFloatResult( + instruction, + Ext( + 53, + _floatType, + GetFloatSource(instruction, 0), + Bitcast(_intType, GetRawSource(instruction, 1)))); + break; + case "VExpF32": + result = EmitFloatResult( + instruction, + Ext(29, _floatType, GetFloatSource(instruction, 0))); + break; + case "VRsqF32": + result = EmitFloatResult( + instruction, + Ext(32, _floatType, GetFloatSource(instruction, 0))); + break; + case "VFractF32": + result = EmitFloatResult( + instruction, + Ext(10, _floatType, GetFloatSource(instruction, 0))); + break; + case "VTruncF32": + result = EmitFloatResult( + instruction, + Ext(3, _floatType, GetFloatSource(instruction, 0))); + break; + case "VCeilF32": + result = EmitFloatResult( + instruction, + Ext(9, _floatType, GetFloatSource(instruction, 0))); + break; + case "VRndneF32": + result = EmitFloatResult( + instruction, + Ext(2, _floatType, GetFloatSource(instruction, 0))); + break; + case "VFloorF32": + result = EmitFloatResult( + instruction, + Ext(8, _floatType, GetFloatSource(instruction, 0))); + break; + case "VSqrtF32": + result = EmitFloatResult( + instruction, + Ext(31, _floatType, GetFloatSource(instruction, 0))); + break; + case "VSinF32": + result = EmitFloatResult( + instruction, + Ext( + 13, + _floatType, + _module.AddInstruction( + SpirvOp.FMul, + _floatType, + GetFloatSource(instruction, 0), + Float(MathF.Tau)))); + break; + case "VCosF32": + result = EmitFloatResult( + instruction, + Ext( + 14, + _floatType, + _module.AddInstruction( + SpirvOp.FMul, + _floatType, + GetFloatSource(instruction, 0), + Float(MathF.Tau)))); + break; + case "VAddF32": + result = EmitFloatBinary(instruction, SpirvOp.FAdd); + break; + case "VSubF32": + result = EmitFloatBinary(instruction, SpirvOp.FSub); + break; + case "VSubrevF32": + result = EmitFloatBinary(instruction, SpirvOp.FSub, reverse: true); + break; + case "VMulF32": + result = EmitFloatBinary(instruction, SpirvOp.FMul); + break; + case "VMinF32": + result = EmitFloatExtBinary(instruction, 37); + break; + case "VMaxF32": + result = EmitFloatExtBinary(instruction, 40); + break; + case "VMadF32": + case "VFmaF32": + case "VMadMkF32": + case "VMadAkF32": + result = EmitFloatResult( + instruction, + Ext( + 50, + _floatType, + GetFloatSource(instruction, 0), + GetFloatSource(instruction, 1), + GetFloatSource(instruction, 2))); + break; + case "VMacF32": + { + var addend = Bitcast(_floatType, LoadV(destination)); + result = EmitFloatResult( + instruction, + Ext( + 50, + _floatType, + GetFloatSource(instruction, 0), + GetFloatSource(instruction, 1), + addend)); + break; + } + case "VMin3F32": + result = EmitFloatTernaryExt(instruction, 37); + break; + case "VMax3F32": + result = EmitFloatTernaryExt(instruction, 40); + break; + case "VAndB32": + result = EmitIntegerBinary(instruction, SpirvOp.BitwiseAnd); + break; + case "VOrB32": + result = EmitIntegerBinary(instruction, SpirvOp.BitwiseOr); + break; + case "VXorB32": + result = EmitIntegerBinary(instruction, SpirvOp.BitwiseXor); + break; + case "VNotB32": + result = _module.AddInstruction( + SpirvOp.Not, + _uintType, + GetRawSource(instruction, 0)); + break; + case "VBfrevB32": + result = _module.AddInstruction( + SpirvOp.BitReverse, + _uintType, + GetRawSource(instruction, 0)); + break; + case "VFfblB32": + result = Bitcast( + _uintType, + Ext( + 73, + _intType, + Bitcast(_intType, GetRawSource(instruction, 0)))); + break; + case "VAddI32": + case "VAddU32": + result = EmitIntegerBinary(instruction, SpirvOp.IAdd); + break; + case "VAddcU32": + result = EmitAddWithCarry(instruction); + break; + case "VSubI32": + case "VSubU32": + result = EmitIntegerBinary(instruction, SpirvOp.ISub); + break; + case "VSubrevI32": + case "VSubrevU32": + result = EmitIntegerBinary(instruction, SpirvOp.ISub, reverse: true); + break; + case "VSubbU32": + result = EmitSubtractWithBorrow(instruction, reverse: false); + break; + case "VSubbrevU32": + result = EmitSubtractWithBorrow(instruction, reverse: true); + break; + case "VMulLoU32": + case "VMulU32U24": + result = EmitIntegerBinary(instruction, SpirvOp.IMul); + break; + case "VMulHiU32": + case "VMulHiU32U24": + { + var left = GetRawSource(instruction, 0); + var right = GetRawSource(instruction, 1); + if (instruction.Opcode == "VMulHiU32U24") + { + left = BitwiseAnd(left, UInt(0x00FF_FFFF)); + right = BitwiseAnd(right, UInt(0x00FF_FFFF)); + } + + var wideLeft = _module.AddInstruction( + SpirvOp.UConvert, + _ulongType, + left); + var wideRight = _module.AddInstruction( + SpirvOp.UConvert, + _ulongType, + right); + var product = _module.AddInstruction( + SpirvOp.IMul, + _ulongType, + wideLeft, + wideRight); + result = _module.AddInstruction( + SpirvOp.UConvert, + _uintType, + ShiftRightLogical64( + product, + _module.Constant64(_ulongType, 32))); + break; + } + case "VMadU32U24": + { + var left = BitwiseAnd( + GetRawSource(instruction, 0), + UInt(0x00FF_FFFF)); + var right = BitwiseAnd( + GetRawSource(instruction, 1), + UInt(0x00FF_FFFF)); + result = IAdd( + _module.AddInstruction( + SpirvOp.IMul, + _uintType, + left, + right), + GetRawSource(instruction, 2)); + break; + } + case "VMadU32U16": + { + var left = BitwiseAnd( + GetRawSource(instruction, 0), + UInt(0xFFFF)); + var right = BitwiseAnd( + GetRawSource(instruction, 1), + UInt(0xFFFF)); + result = IAdd( + _module.AddInstruction( + SpirvOp.IMul, + _uintType, + left, + right), + GetRawSource(instruction, 2)); + break; + } + case "VLshrB32": + result = EmitIntegerBinary(instruction, SpirvOp.ShiftRightLogical); + break; + case "VLshrrevB32": + result = EmitIntegerBinary( + instruction, + SpirvOp.ShiftRightLogical, + reverse: true); + break; + case "VLshlB32": + result = EmitIntegerBinary(instruction, SpirvOp.ShiftLeftLogical); + break; + case "VLshlrevB32": + result = EmitIntegerBinary( + instruction, + SpirvOp.ShiftLeftLogical, + reverse: true); + break; + case "VAshrI32": + case "VAshrrevI32": + { + var reverse = instruction.Opcode == "VAshrrevI32"; + var left = GetRawSource(instruction, reverse ? 1 : 0); + var right = GetRawSource(instruction, reverse ? 0 : 1); + result = ShiftRightArithmetic(left, right); + break; + } + case "VLshlAddU32": + { + var shifted = ShiftLeftLogical( + GetRawSource(instruction, 0), + BitwiseAnd(GetRawSource(instruction, 1), UInt(31))); + result = IAdd(shifted, GetRawSource(instruction, 2)); + break; + } + case "VLshlOrU32": + { + var shifted = ShiftLeftLogical( + GetRawSource(instruction, 0), + BitwiseAnd(GetRawSource(instruction, 1), UInt(31))); + result = BitwiseOr( + shifted, + GetRawSource(instruction, 2)); + break; + } + case "VAndOrB32": + result = BitwiseOr( + BitwiseAnd( + GetRawSource(instruction, 0), + GetRawSource(instruction, 1)), + GetRawSource(instruction, 2)); + break; + case "VOr3U32": + result = BitwiseOr( + BitwiseOr( + GetRawSource(instruction, 0), + GetRawSource(instruction, 1)), + GetRawSource(instruction, 2)); + break; + case "VPermlane16B32": + result = EmitPermlane16(instruction, exchangeRows: false); + break; + case "VPermlanex16B32": + result = EmitPermlane16(instruction, exchangeRows: true); + break; + case "VAddLshlU32": + { + var added = IAdd( + GetRawSource(instruction, 0), + GetRawSource(instruction, 1)); + result = ShiftLeftLogical(added, GetRawSource(instruction, 2)); + break; + } + case "VAdd3U32": + result = IAdd( + IAdd( + GetRawSource(instruction, 0), + GetRawSource(instruction, 1)), + GetRawSource(instruction, 2)); + break; + case "VMinU32": + result = Ext( + 38, + _uintType, + GetRawSource(instruction, 0), + GetRawSource(instruction, 1)); + break; + case "VMaxU32": + result = Ext( + 41, + _uintType, + GetRawSource(instruction, 0), + GetRawSource(instruction, 1)); + break; + case "VMin3U32": + result = Ext( + 38, + _uintType, + Ext( + 38, + _uintType, + GetRawSource(instruction, 0), + GetRawSource(instruction, 1)), + GetRawSource(instruction, 2)); + break; + case "VMax3U32": + result = Ext( + 41, + _uintType, + Ext( + 41, + _uintType, + GetRawSource(instruction, 0), + GetRawSource(instruction, 1)), + GetRawSource(instruction, 2)); + break; + case "VMinI32": + case "VMaxI32": + { + var signedResult = Ext( + instruction.Opcode == "VMinI32" ? 39u : 42u, + _intType, + Bitcast(_intType, GetRawSource(instruction, 0)), + Bitcast(_intType, GetRawSource(instruction, 1))); + result = Bitcast(_uintType, signedResult); + break; + } + case "VMin3I32": + case "VMax3I32": + { + var operation = instruction.Opcode == "VMin3I32" ? 39u : 42u; + var left = Bitcast( + _intType, + GetRawSource(instruction, 0)); + var middle = Bitcast( + _intType, + GetRawSource(instruction, 1)); + var right = Bitcast( + _intType, + GetRawSource(instruction, 2)); + result = Bitcast( + _uintType, + Ext( + operation, + _intType, + Ext(operation, _intType, left, middle), + right)); + break; + } + case "VMed3U32": + { + var left = GetRawSource(instruction, 0); + var middle = GetRawSource(instruction, 1); + var right = GetRawSource(instruction, 2); + var low = Ext(38, _uintType, left, middle); + var high = Ext(41, _uintType, left, middle); + result = Ext( + 41, + _uintType, + low, + Ext(38, _uintType, high, right)); + break; + } + case "VMed3I32": + { + var left = Bitcast(_intType, GetRawSource(instruction, 0)); + var middle = Bitcast(_intType, GetRawSource(instruction, 1)); + var right = Bitcast(_intType, GetRawSource(instruction, 2)); + var low = Ext(39, _intType, left, middle); + var high = Ext(42, _intType, left, middle); + result = Bitcast( + _uintType, + Ext( + 42, + _intType, + low, + Ext(39, _intType, high, right))); + break; + } + case "VMed3F32": + { + var left = GetFloatSource(instruction, 0); + var middle = GetFloatSource(instruction, 1); + var right = GetFloatSource(instruction, 2); + var low = Ext(37, _floatType, left, middle); + var high = Ext(40, _floatType, left, middle); + result = EmitFloatResult( + instruction, + Ext( + 40, + _floatType, + low, + Ext(37, _floatType, high, right))); + break; + } + case "VCubeidF32": + result = EmitCubeCoordinate(instruction, CubeCoordinate.Id); + break; + case "VCubescF32": + result = EmitCubeCoordinate(instruction, CubeCoordinate.Sc); + break; + case "VCubetcF32": + result = EmitCubeCoordinate(instruction, CubeCoordinate.Tc); + break; + case "VCubemaF32": + result = EmitCubeCoordinate(instruction, CubeCoordinate.Ma); + break; + case "VAddCoU32": + { + var left = GetRawSource(instruction, 0); + var right = GetRawSource(instruction, 1); + result = IAdd(left, right); + var carry = _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + result, + left); + StoreCarryOut(instruction, carry); + break; + } + case "VSubCoU32": + case "VSubrevCoU32": + { + var reverse = instruction.Opcode == "VSubrevCoU32"; + var left = GetRawSource(instruction, reverse ? 1 : 0); + var right = GetRawSource(instruction, reverse ? 0 : 1); + result = _module.AddInstruction(SpirvOp.ISub, _uintType, left, right); + var borrow = _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + left, + right); + StoreCarryOut(instruction, borrow); + break; + } + case "VBfeU32": + { + var width = BitwiseAnd(GetRawSource(instruction, 2), UInt(31)); + result = _module.AddInstruction( + SpirvOp.BitFieldUExtract, + _uintType, + GetRawSource(instruction, 0), + BitwiseAnd(GetRawSource(instruction, 1), UInt(31)), + width); + break; + } + case "VBfiB32": + { + var mask = GetRawSource(instruction, 0); + var insert = GetRawSource(instruction, 1); + var source = GetRawSource(instruction, 2); + result = _module.AddInstruction( + SpirvOp.BitwiseOr, + _uintType, + BitwiseAnd(mask, insert), + BitwiseAnd( + _module.AddInstruction(SpirvOp.Not, _uintType, mask), + source)); + break; + } + case "VCvtPkrtzF16F32": + { + var first = TruncateFloat32ForPack(GetFloatSource(instruction, 0)); + var second = TruncateFloat32ForPack(GetFloatSource(instruction, 1)); + var vector = _module.AddInstruction( + SpirvOp.CompositeConstruct, + _vec2Type, + first, + second); + result = Ext(58, _uintType, vector); + break; + } + default: + error = $"unsupported vector opcode {instruction.Opcode}"; + return false; + } + + StoreV(destination, result); + return true; + } + + private bool TryEmitVectorCompare( + Gen5ShaderInstruction instruction, + out string error) + { + error = string.Empty; + uint condition = _module.ConstantBool(false); + var opcode = instruction.Opcode; + if (opcode is "VCmpClassF32" or "VCmpxClassF32") + { + var source = GetFloatSource(instruction, 0); + var raw = GetRawSource(instruction, 0); + var mask = GetRawSource(instruction, 1); + var negative = IsNotZero(BitwiseAnd(raw, UInt(0x8000_0000))); + var positive = _module.AddInstruction( + SpirvOp.LogicalNot, + _boolType, + negative); + var nan = _module.AddInstruction(SpirvOp.IsNan, _boolType, source); + var infinity = + _module.AddInstruction(SpirvOp.IsInf, _boolType, source); + var zero = _module.AddInstruction( + SpirvOp.FOrdEqual, + _boolType, + source, + Float(0)); + var absolute = Ext(4, _floatType, source); + var nonzero = _module.AddInstruction( + SpirvOp.FOrdGreaterThan, + _boolType, + absolute, + Float(0)); + var belowNormal = _module.AddInstruction( + SpirvOp.FOrdLessThan, + _boolType, + absolute, + Bitcast(_floatType, UInt(0x0080_0000))); + var subnormal = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + nonzero, + belowNormal); + var special = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + nan, + _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + infinity, + _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + zero, + subnormal))); + var normal = _module.AddInstruction( + SpirvOp.LogicalNot, + _boolType, + special); + + uint MaskedClass(uint bits, uint value) + { + var enabled = IsNotZero(BitwiseAnd(mask, UInt(bits))); + return _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + enabled, + value); + } + + uint SignedClass(uint negativeBit, uint positiveBit, uint value) + { + var negativeClass = MaskedClass( + negativeBit, + _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + negative, + value)); + var positiveClass = MaskedClass( + positiveBit, + _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + positive, + value)); + return _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + negativeClass, + positiveClass); + } + + condition = MaskedClass(0x003, nan); + condition = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + condition, + SignedClass(0x004, 0x200, infinity)); + condition = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + condition, + SignedClass(0x008, 0x100, normal)); + condition = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + condition, + SignedClass(0x010, 0x080, subnormal)); + condition = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + condition, + SignedClass(0x020, 0x040, zero)); + } + else if (opcode is "VCmpFF32" or "VCmpxFF32" or "VCmpFI32" or "VCmpFU32") + { + condition = _module.ConstantBool(false); + } + else if (opcode is "VCmpTruF32" or "VCmpxTruF32" or "VCmpTI32" or "VCmpTU32") + { + condition = _module.ConstantBool(true); + } + else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") && + opcode.EndsWith("F32", StringComparison.Ordinal)) + { + var left = GetFloatSource(instruction, 0); + var right = GetFloatSource(instruction, 1); + var operation = opcode switch + { + "VCmpLtF32" or "VCmpxLtF32" => SpirvOp.FOrdLessThan, + "VCmpEqF32" or "VCmpxEqF32" => SpirvOp.FOrdEqual, + "VCmpLeF32" or "VCmpxLeF32" => SpirvOp.FOrdLessThanEqual, + "VCmpGtF32" or "VCmpxGtF32" => SpirvOp.FOrdGreaterThan, + "VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual, + "VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual, + "VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual, + "VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual, + "VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan, + "VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual, + "VCmpNgeF32" or "VCmpxNgeF32" => SpirvOp.FUnordLessThan, + _ => SpirvOp.Nop, + }; + if (operation == SpirvOp.Nop) + { + error = $"unsupported float compare {opcode}"; + return false; + } + + condition = _module.AddInstruction(operation, _boolType, left, right); + } + else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32")) + { + var left = GetRawSource(instruction, 0); + var right = GetRawSource(instruction, 1); + var signed = opcode.EndsWith("I32", StringComparison.Ordinal); + if (signed) + { + left = Bitcast(_intType, left); + right = Bitcast(_intType, right); + } + + var operation = opcode switch + { + "VCmpEqI32" or "VCmpxEqI32" or + "VCmpEqU32" or "VCmpxEqU32" => SpirvOp.IEqual, + "VCmpNeI32" or "VCmpxNeI32" or + "VCmpNeU32" or "VCmpxNeU32" => SpirvOp.INotEqual, + "VCmpLtI32" or "VCmpxLtI32" => SpirvOp.SLessThan, + "VCmpLeI32" or "VCmpxLeI32" => SpirvOp.SLessThanEqual, + "VCmpGtI32" or "VCmpxGtI32" => SpirvOp.SGreaterThan, + "VCmpGeI32" or "VCmpxGeI32" => SpirvOp.SGreaterThanEqual, + "VCmpLtU32" or "VCmpxLtU32" => SpirvOp.ULessThan, + "VCmpLeU32" or "VCmpxLeU32" => SpirvOp.ULessThanEqual, + "VCmpGtU32" or "VCmpxGtU32" => SpirvOp.UGreaterThan, + "VCmpGeU32" or "VCmpxGeU32" => SpirvOp.UGreaterThanEqual, + _ => SpirvOp.Nop, + }; + if (operation == SpirvOp.Nop) + { + error = $"unsupported integer compare {opcode}"; + return false; + } + + condition = _module.AddInstruction(operation, _boolType, left, right); + } + + StoreWaveMask(106, condition); + if (opcode.StartsWith("VCmpx", StringComparison.Ordinal)) + { + var active = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + Load(_boolType, _exec), + condition); + StoreWaveMask(126, active); + } + + return true; + } + + private bool TryEmitScalarAlu( + Gen5ShaderInstruction instruction, + out string error) + { + error = string.Empty; + if (instruction.Encoding == Gen5ShaderEncoding.Sopc) + { + return TryEmitScalarCompare(instruction, out error); + } + + if (instruction.Destinations.Count == 0 || + instruction.Destinations[0].Kind != Gen5OperandKind.ScalarRegister) + { + error = "missing scalar destination"; + return false; + } + + var destination = instruction.Destinations[0].Value; + if (instruction.Encoding == Gen5ShaderEncoding.Sopk) + { + var immediate = unchecked((uint)(short)(instruction.Words[0] & 0xFFFF)); + if (instruction.Opcode.StartsWith("SCmpk", StringComparison.Ordinal)) + { + return TryEmitScalarCompareK(instruction, destination, immediate, out error); + } + + var current = LoadS(destination); + var value = instruction.Opcode switch + { + "SMovkI32" => UInt(immediate), + "SAddkI32" => IAdd(current, UInt(immediate)), + "SMulkI32" => _module.AddInstruction( + SpirvOp.IMul, + _uintType, + current, + UInt(immediate)), + _ => 0u, + }; + if (value == 0) + { + error = $"unsupported scalar immediate {instruction.Opcode}"; + return false; + } + + StoreS(destination, value); + return true; + } + + if (instruction.Opcode == "SGetpcB64") + { + var pc = _state.Program.Address + + instruction.Pc + + (ulong)(instruction.Words.Count * sizeof(uint)); + StoreS(destination, UInt((uint)pc)); + StoreS(destination + 1, UInt((uint)(pc >> 32))); + return true; + } + + if (instruction.Opcode.EndsWith("B64", StringComparison.Ordinal) || + instruction.Opcode is "SWqmB64" or "SBfeU64" or "SBfeI64") + { + return TryEmitScalar64(instruction, destination, out error); + } + + var left = GetRawSource(instruction, 0); + uint result; + switch (instruction.Opcode) + { + case "SMovB32": + result = left; + break; + case "SNotB32": + result = _module.AddInstruction(SpirvOp.Not, _uintType, left); + StoreS(destination, result); + Store(_scc, IsNotZero(result)); + return true; + case "SBrevB32": + result = _module.AddInstruction(SpirvOp.BitReverse, _uintType, left); + StoreS(destination, result); + Store(_scc, IsNotZero(result)); + return true; + case "SBcnt1I32B32": + result = _module.AddInstruction(SpirvOp.BitCount, _uintType, left); + StoreS(destination, result); + Store(_scc, IsNotZero(result)); + return true; + case "SFF1I32B32": + result = Ext(73, _uintType, left); + StoreS(destination, result); + Store(_scc, IsNotZero(result)); + return true; + case "SBitset1B32": + result = _module.AddInstruction( + SpirvOp.BitFieldInsert, + _uintType, + LoadS(destination), + UInt(1), + BitwiseAnd(left, UInt(31)), + UInt(1)); + StoreS(destination, result); + return true; + default: + { + if (instruction.Sources.Count < 2) + { + error = $"missing scalar source for {instruction.Opcode}"; + return false; + } + + var right = GetRawSource(instruction, 1); + switch (instruction.Opcode) + { + case "SAddU32": + result = IAdd(left, right); + Store(_scc, _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + result, + left)); + break; + case "SSubU32": + result = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + left, + right); + Store(_scc, _module.AddInstruction( + SpirvOp.UGreaterThan, + _boolType, + right, + left)); + break; + case "SAddI32": + result = IAdd(left, right); + Store(_scc, SignedAddOverflow(left, right, result)); + break; + case "SSubI32": + result = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + left, + right); + Store(_scc, SignedSubOverflow(left, right, result)); + break; + case "SAddcU32": + { + var carryIn = _module.AddInstruction( + SpirvOp.Select, + _uintType, + Load(_boolType, _scc), + UInt(1), + UInt(0)); + var partial = IAdd(left, right); + result = IAdd(partial, carryIn); + var firstCarry = _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + partial, + left); + var secondCarry = _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + result, + partial); + Store( + _scc, + _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + firstCarry, + secondCarry)); + break; + } + case "SSubbU32": + { + var borrow = _module.AddInstruction( + SpirvOp.Select, + _uintType, + Load(_boolType, _scc), + UInt(1), + UInt(0)); + var partial = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + left, + right); + result = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + partial, + borrow); + var firstBorrow = _module.AddInstruction( + SpirvOp.UGreaterThan, + _boolType, + right, + left); + var secondBorrow = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + _module.AddInstruction( + SpirvOp.IEqual, + _boolType, + borrow, + UInt(1)), + _module.AddInstruction( + SpirvOp.IEqual, + _boolType, + right, + left)); + Store( + _scc, + _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + firstBorrow, + secondBorrow)); + break; + } + case "SMulI32": + result = _module.AddInstruction( + SpirvOp.IMul, + _uintType, + left, + right); + break; + case "SAndB32": + result = BitwiseAnd(left, right); + Store(_scc, IsNotZero(result)); + break; + case "SOrB32": + result = _module.AddInstruction( + SpirvOp.BitwiseOr, + _uintType, + left, + right); + Store(_scc, IsNotZero(result)); + break; + case "SXorB32": + result = _module.AddInstruction( + SpirvOp.BitwiseXor, + _uintType, + left, + right); + Store(_scc, IsNotZero(result)); + break; + case "SAndn2B32": + result = BitwiseAnd( + left, + _module.AddInstruction(SpirvOp.Not, _uintType, right)); + Store(_scc, IsNotZero(result)); + break; + case "SOrn2B32": + result = _module.AddInstruction( + SpirvOp.BitwiseOr, + _uintType, + left, + _module.AddInstruction(SpirvOp.Not, _uintType, right)); + Store(_scc, IsNotZero(result)); + break; + case "SNandB32": + result = _module.AddInstruction( + SpirvOp.Not, + _uintType, + BitwiseAnd(left, right)); + Store(_scc, IsNotZero(result)); + break; + case "SNorB32": + result = _module.AddInstruction( + SpirvOp.Not, + _uintType, + _module.AddInstruction( + SpirvOp.BitwiseOr, + _uintType, + left, + right)); + Store(_scc, IsNotZero(result)); + break; + case "SXnorB32": + result = _module.AddInstruction( + SpirvOp.Not, + _uintType, + _module.AddInstruction( + SpirvOp.BitwiseXor, + _uintType, + left, + right)); + Store(_scc, IsNotZero(result)); + break; + case "SLshlB32": + result = ShiftLeftLogical(left, right); + Store(_scc, IsNotZero(result)); + break; + case "SLshrB32": + result = ShiftRightLogical( + left, + BitwiseAnd(right, UInt(31))); + Store(_scc, IsNotZero(result)); + break; + case "SAshrI32": + result = ShiftRightArithmetic(left, right); + Store(_scc, IsNotZero(result)); + break; + case "SBfmB32": + result = _module.AddInstruction( + SpirvOp.BitFieldInsert, + _uintType, + UInt(0), + UInt(uint.MaxValue), + BitwiseAnd(right, UInt(31)), + BitwiseAnd(left, UInt(31))); + break; + case "SBfeU32": + case "SBfeI32": + { + var offset = BitwiseAnd(right, UInt(31)); + var requestedWidth = BitwiseAnd( + ShiftRightLogical(right, UInt(16)), + UInt(0x7F)); + var remaining = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + UInt(32), + offset); + var width = Ext( + 38, + _uintType, + requestedWidth, + remaining); + result = instruction.Opcode == "SBfeI32" + ? Bitcast( + _uintType, + _module.AddInstruction( + SpirvOp.BitFieldSExtract, + _intType, + Bitcast(_intType, left), + offset, + width)) + : _module.AddInstruction( + SpirvOp.BitFieldUExtract, + _uintType, + left, + offset, + width); + Store(_scc, IsNotZero(result)); + break; + } + case "SCselectB32": + result = _module.AddInstruction( + SpirvOp.Select, + _uintType, + Load(_boolType, _scc), + left, + right); + break; + case "SMinU32": + result = Ext(38, _uintType, left, right); + Store( + _scc, + _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + left, + right)); + break; + case "SMinI32": + result = Bitcast( + _uintType, + Ext(39, _intType, Bitcast(_intType, left), Bitcast(_intType, right))); + Store( + _scc, + _module.AddInstruction( + SpirvOp.SLessThan, + _boolType, + Bitcast(_intType, left), + Bitcast(_intType, right))); + break; + case "SMaxU32": + result = Ext(41, _uintType, left, right); + Store( + _scc, + _module.AddInstruction( + SpirvOp.UGreaterThan, + _boolType, + left, + right)); + break; + case "SMaxI32": + result = Bitcast( + _uintType, + Ext(42, _intType, Bitcast(_intType, left), Bitcast(_intType, right))); + Store( + _scc, + _module.AddInstruction( + SpirvOp.SGreaterThan, + _boolType, + Bitcast(_intType, left), + Bitcast(_intType, right))); + break; + case "SLshl1AddU32": + case "SLshl2AddU32": + case "SLshl3AddU32": + case "SLshl4AddU32": + { + var shift = (uint)(instruction.Opcode[5] - '0'); + result = IAdd( + ShiftLeftLogical(left, UInt(shift)), + right); + break; + } + default: + error = $"unsupported scalar opcode {instruction.Opcode}"; + return false; + } + + break; + } + } + + StoreS(destination, result); + return true; + } + + private bool TryEmitScalarCompare( + Gen5ShaderInstruction instruction, + out string error) + { + error = string.Empty; + if (instruction.Sources.Count < 2) + { + error = "missing scalar compare source"; + return false; + } + + var left = GetRawSource(instruction, 0); + var right = GetRawSource(instruction, 1); + if (instruction.Opcode is "SBitcmp0B32" or "SBitcmp1B32") + { + var shifted = ShiftRightLogical( + left, + BitwiseAnd(right, UInt(31))); + var isSet = IsNotZero(BitwiseAnd(shifted, UInt(1))); + Store( + _scc, + instruction.Opcode == "SBitcmp1B32" + ? isSet + : _module.AddInstruction( + SpirvOp.LogicalNot, + _boolType, + isSet)); + return true; + } + + var operation = instruction.Opcode switch + { + "SCmpEqI32" or "SCmpEqU32" => SpirvOp.IEqual, + "SCmpLgI32" or "SCmpLgU32" => SpirvOp.INotEqual, + "SCmpGtI32" => SpirvOp.SGreaterThan, + "SCmpGeI32" => SpirvOp.SGreaterThanEqual, + "SCmpLtI32" => SpirvOp.SLessThan, + "SCmpLeI32" => SpirvOp.SLessThanEqual, + "SCmpGtU32" => SpirvOp.UGreaterThan, + "SCmpGeU32" => SpirvOp.UGreaterThanEqual, + "SCmpLtU32" => SpirvOp.ULessThan, + "SCmpLeU32" => SpirvOp.ULessThanEqual, + _ => SpirvOp.Nop, + }; + if (operation == SpirvOp.Nop) + { + error = $"unsupported scalar compare {instruction.Opcode}"; + return false; + } + + if (instruction.Opcode.EndsWith("I32", StringComparison.Ordinal)) + { + left = Bitcast(_intType, left); + right = Bitcast(_intType, right); + } + + Store(_scc, _module.AddInstruction(operation, _boolType, left, right)); + return true; + } + + private bool TryEmitScalarCompareK( + Gen5ShaderInstruction instruction, + uint destination, + uint immediate, + out string error) + { + error = string.Empty; + var left = LoadS(destination); + var right = UInt(immediate); + var operation = instruction.Opcode switch + { + "SCmpkEqI32" or "SCmpkEqU32" => SpirvOp.IEqual, + "SCmpkLgI32" or "SCmpkLgU32" => SpirvOp.INotEqual, + "SCmpkGtI32" => SpirvOp.SGreaterThan, + "SCmpkGeI32" => SpirvOp.SGreaterThanEqual, + "SCmpkLtI32" => SpirvOp.SLessThan, + "SCmpkLeI32" => SpirvOp.SLessThanEqual, + "SCmpkGtU32" => SpirvOp.UGreaterThan, + "SCmpkGeU32" => SpirvOp.UGreaterThanEqual, + "SCmpkLtU32" => SpirvOp.ULessThan, + "SCmpkLeU32" => SpirvOp.ULessThanEqual, + _ => SpirvOp.Nop, + }; + if (operation == SpirvOp.Nop) + { + error = $"unsupported scalar immediate compare {instruction.Opcode}"; + return false; + } + + if (instruction.Opcode.EndsWith("I32", StringComparison.Ordinal)) + { + left = Bitcast(_intType, left); + right = Bitcast(_intType, right); + } + + Store(_scc, _module.AddInstruction(operation, _boolType, left, right)); + return true; + } + + private bool TryEmitScalar64( + Gen5ShaderInstruction instruction, + uint destination, + out string error) + { + error = string.Empty; + var left = GetRawSource64(instruction, 0); + if (instruction.Opcode.EndsWith("SaveexecB64", StringComparison.Ordinal)) + { + var oldExec = BooleanToLaneMask(Load(_boolType, _exec)); + var notLeft = _module.AddInstruction(SpirvOp.Not, _ulongType, left); + var newExec = instruction.Opcode switch + { + "SAndSaveexecB64" => _module.AddInstruction( + SpirvOp.BitwiseAnd, _ulongType, oldExec, left), + "SOrSaveexecB64" => _module.AddInstruction( + SpirvOp.BitwiseOr, _ulongType, oldExec, left), + "SXorSaveexecB64" => _module.AddInstruction( + SpirvOp.BitwiseXor, _ulongType, oldExec, left), + "SAndn2SaveexecB64" => _module.AddInstruction( + SpirvOp.BitwiseAnd, + _ulongType, + left, + _module.AddInstruction( + SpirvOp.Not, + _ulongType, + oldExec)), + "SAndn1SaveexecB64" => _module.AddInstruction( + SpirvOp.BitwiseAnd, + _ulongType, + notLeft, + oldExec), + "SOrn1SaveexecB64" => _module.AddInstruction( + SpirvOp.BitwiseOr, + _ulongType, + notLeft, + oldExec), + "SOrn2SaveexecB64" => _module.AddInstruction( + SpirvOp.BitwiseOr, + _ulongType, + left, + _module.AddInstruction( + SpirvOp.Not, + _ulongType, + oldExec)), + "SNandSaveexecB64" => _module.AddInstruction( + SpirvOp.Not, + _ulongType, + _module.AddInstruction( + SpirvOp.BitwiseAnd, + _ulongType, + left, + oldExec)), + "SNorSaveexecB64" => _module.AddInstruction( + SpirvOp.Not, + _ulongType, + _module.AddInstruction( + SpirvOp.BitwiseOr, + _ulongType, + left, + oldExec)), + "SXnorSaveexecB64" => _module.AddInstruction( + SpirvOp.Not, + _ulongType, + _module.AddInstruction( + SpirvOp.BitwiseXor, + _ulongType, + left, + oldExec)), + _ => 0u, + }; + if (newExec == 0) + { + error = + $"unsupported scalar 64-bit opcode {instruction.Opcode}"; + return false; + } + + StoreS64(destination, oldExec); + StoreS64(126, newExec); + Store(_scc, IsNotZero64(newExec)); + return true; + } + + if (instruction.Opcode is "SLshlB64" or "SLshrB64") + { + if (instruction.Sources.Count < 2) + { + error = "missing scalar 64-bit shift source"; + return false; + } + + var shift = _module.AddInstruction( + SpirvOp.UConvert, + _ulongType, + GetRawSource(instruction, 1)); + var shiftedValue = instruction.Opcode == "SLshlB64" + ? ShiftLeftLogical64(left, shift) + : ShiftRightLogical64(left, shift); + StoreS64(destination, shiftedValue); + Store(_scc, IsNotZero64(shiftedValue)); + return true; + } + + if (instruction.Opcode is "SBfeU64" or "SBfeI64") + { + if (instruction.Sources.Count < 2) + { + error = "missing scalar 64-bit bitfield source"; + return false; + } + + var control = GetRawSource(instruction, 1); + var offset = BitwiseAnd(control, UInt(63)); + var requestedWidth = BitwiseAnd( + ShiftRightLogical(control, UInt(16)), + UInt(0x7F)); + var remaining = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + UInt(64), + offset); + var width = Ext( + 38, + _uintType, + requestedWidth, + remaining); + var offset64 = _module.AddInstruction( + SpirvOp.UConvert, + _ulongType, + offset); + var width64 = _module.AddInstruction( + SpirvOp.UConvert, + _ulongType, + width); + var one64 = _module.Constant64(_ulongType, 1); + var shifted = ShiftRightLogical64(left, offset64); + var partialMask = _module.AddInstruction( + SpirvOp.ISub, + _ulongType, + ShiftLeftLogical64(one64, width64), + one64); + var fullWidth = _module.AddInstruction( + SpirvOp.IEqual, + _boolType, + width, + UInt(64)); + var mask = _module.AddInstruction( + SpirvOp.Select, + _ulongType, + fullWidth, + _module.Constant64(_ulongType, ulong.MaxValue), + partialMask); + var extracted = _module.AddInstruction( + SpirvOp.BitwiseAnd, + _ulongType, + shifted, + mask); + if (instruction.Opcode == "SBfeI64") + { + var signShift = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + width, + UInt(1)); + var signBit = ShiftLeftLogical64( + one64, + _module.AddInstruction( + SpirvOp.UConvert, + _ulongType, + signShift)); + var signExtended = _module.AddInstruction( + SpirvOp.ISub, + _ulongType, + _module.AddInstruction( + SpirvOp.BitwiseXor, + _ulongType, + extracted, + signBit), + signBit); + extracted = _module.AddInstruction( + SpirvOp.Select, + _ulongType, + _module.AddInstruction( + SpirvOp.IEqual, + _boolType, + width, + UInt(0)), + _module.Constant64(_ulongType, 0), + signExtended); + } + + StoreS64(destination, extracted); + Store(_scc, IsNotZero64(extracted)); + return true; + } + + uint value; + if (instruction.Opcode is "SMovB64" or "SWqmB64") + { + value = left; + } + else if (instruction.Opcode == "SNotB64") + { + value = _module.AddInstruction(SpirvOp.Not, _ulongType, left); + } + else + { + if (instruction.Sources.Count < 2) + { + error = "missing scalar 64-bit source"; + return false; + } + + var right = GetRawSource64(instruction, 1); + value = instruction.Opcode switch + { + "SAndB64" => _module.AddInstruction( + SpirvOp.BitwiseAnd, _ulongType, left, right), + "SOrB64" => _module.AddInstruction( + SpirvOp.BitwiseOr, _ulongType, left, right), + "SXorB64" => _module.AddInstruction( + SpirvOp.BitwiseXor, _ulongType, left, right), + "SNandB64" => _module.AddInstruction( + SpirvOp.Not, + _ulongType, + _module.AddInstruction( + SpirvOp.BitwiseAnd, _ulongType, left, right)), + "SNorB64" => _module.AddInstruction( + SpirvOp.Not, + _ulongType, + _module.AddInstruction( + SpirvOp.BitwiseOr, _ulongType, left, right)), + "SXnorB64" => _module.AddInstruction( + SpirvOp.Not, + _ulongType, + _module.AddInstruction( + SpirvOp.BitwiseXor, _ulongType, left, right)), + "SAndn1B64" => _module.AddInstruction( + SpirvOp.BitwiseAnd, + _ulongType, + _module.AddInstruction(SpirvOp.Not, _ulongType, left), + right), + "SAndn2B64" => _module.AddInstruction( + SpirvOp.BitwiseAnd, + _ulongType, + left, + _module.AddInstruction(SpirvOp.Not, _ulongType, right)), + "SOrn1B64" => _module.AddInstruction( + SpirvOp.BitwiseOr, + _ulongType, + _module.AddInstruction(SpirvOp.Not, _ulongType, left), + right), + "SOrn2B64" => _module.AddInstruction( + SpirvOp.BitwiseOr, + _ulongType, + left, + _module.AddInstruction(SpirvOp.Not, _ulongType, right)), + "SCselectB64" => _module.AddInstruction( + SpirvOp.Select, + _ulongType, + Load(_boolType, _scc), + left, + right), + _ => 0, + }; + if (value == 0) + { + error = $"unsupported scalar 64-bit opcode {instruction.Opcode}"; + return false; + } + } + + StoreS64(destination, value); + if (instruction.Opcode is + "SNotB64" or + "SAndB64" or + "SOrB64" or + "SXorB64" or + "SAndn1B64" or + "SAndn2B64" or + "SOrn1B64" or + "SOrn2B64" or + "SNandB64" or + "SNorB64" or + "SXnorB64") + { + Store(_scc, IsNotZero64(value)); + } + + return true; + } + + private uint GetRawSource( + Gen5ShaderInstruction instruction, + int sourceIndex) + { + if ((uint)sourceIndex >= instruction.Sources.Count) + { + throw new InvalidOperationException($"missing source {sourceIndex}"); + } + + var operand = instruction.Sources[sourceIndex]; + uint value = operand.Kind switch + { + Gen5OperandKind.VectorRegister => LoadV(operand.Value), + Gen5OperandKind.ScalarRegister => LoadS(operand.Value), + Gen5OperandKind.LiteralConstant => UInt(operand.Value), + Gen5OperandKind.EncodedConstant when TryDecodeInlineConstant( + operand.Value, + out var inline) => UInt(inline), + _ => throw new InvalidOperationException($"unsupported source {operand}"), + }; + + if (instruction.Control is Gen5SdwaControl sdwa) + { + var selector = sourceIndex switch + { + 0 => sdwa.Source0Select, + 1 => sdwa.Source1Select, + _ => 6u, + }; + value = selector switch + { + 0 => BitwiseAnd(value, UInt(0xFF)), + 1 => BitwiseAnd(ShiftRightLogical(value, UInt(8)), UInt(0xFF)), + 2 => BitwiseAnd(ShiftRightLogical(value, UInt(16)), UInt(0xFF)), + 3 => BitwiseAnd(ShiftRightLogical(value, UInt(24)), UInt(0xFF)), + 4 => BitwiseAnd(value, UInt(0xFFFF)), + 5 => BitwiseAnd(ShiftRightLogical(value, UInt(16)), UInt(0xFFFF)), + _ => value, + }; + } + + return value; + } + + private uint GetFloatSource( + Gen5ShaderInstruction instruction, + int sourceIndex) + { + var operand = instruction.Sources[sourceIndex]; + uint value; + if (operand.Kind == Gen5OperandKind.EncodedConstant && + operand.Value is >= 128 and <= 192) + { + value = Float(operand.Value - 128); + } + else if (operand.Kind == Gen5OperandKind.EncodedConstant && + operand.Value is >= 193 and <= 208) + { + value = Float(-(operand.Value - 192)); + } + else + { + value = Bitcast(_floatType, GetRawSource(instruction, sourceIndex)); + } + + uint absoluteMask = 0; + uint negateMask = 0; + switch (instruction.Control) + { + case Gen5Vop3Control control: + absoluteMask = control.AbsoluteMask; + negateMask = control.NegateMask; + break; + case Gen5SdwaControl control: + absoluteMask = control.AbsoluteMask; + negateMask = control.NegateMask; + break; + case Gen5DppControl control: + absoluteMask = control.AbsoluteMask; + negateMask = control.NegateMask; + break; + } + + if ((absoluteMask & (1u << sourceIndex)) != 0) + { + value = Ext(4, _floatType, value); + } + + if ((negateMask & (1u << sourceIndex)) != 0) + { + value = _module.AddInstruction(SpirvOp.FNegate, _floatType, value); + } + + return value; + } + + private uint GetRawSource64( + Gen5ShaderInstruction instruction, + int sourceIndex) + { + var operand = instruction.Sources[sourceIndex]; + if (operand.Kind == Gen5OperandKind.ScalarRegister) + { + return LoadS64(operand.Value); + } + + var low = GetRawSource(instruction, sourceIndex); + return _module.AddInstruction(SpirvOp.UConvert, _ulongType, low); + } + + private uint LoadS64(uint register) + { + var low = _module.AddInstruction(SpirvOp.UConvert, _ulongType, LoadS(register)); + var high = _module.AddInstruction( + SpirvOp.UConvert, + _ulongType, + LoadS(register + 1)); + high = ShiftLeftLogical64(high, _module.Constant64(_ulongType, 32)); + return _module.AddInstruction(SpirvOp.BitwiseOr, _ulongType, low, high); + } + + private void StoreS64(uint register, uint value) + { + StoreS( + register, + _module.AddInstruction(SpirvOp.UConvert, _uintType, value)); + var high = ShiftRightLogical64( + value, + _module.Constant64(_ulongType, 32)); + StoreS( + register + 1, + _module.AddInstruction(SpirvOp.UConvert, _uintType, high)); + } + + private uint EmitFloatBinary( + Gen5ShaderInstruction instruction, + SpirvOp operation, + bool reverse = false) + { + var left = GetFloatSource(instruction, reverse ? 1 : 0); + var right = GetFloatSource(instruction, reverse ? 0 : 1); + return EmitFloatResult( + instruction, + _module.AddInstruction(operation, _floatType, left, right)); + } + + private uint EmitFloatExtBinary( + Gen5ShaderInstruction instruction, + uint operation) => + EmitFloatResult( + instruction, + Ext( + operation, + _floatType, + GetFloatSource(instruction, 0), + GetFloatSource(instruction, 1))); + + private uint EmitFloatTernaryExt( + Gen5ShaderInstruction instruction, + uint operation) + { + var first = Ext( + operation, + _floatType, + GetFloatSource(instruction, 0), + GetFloatSource(instruction, 1)); + return EmitFloatResult( + instruction, + Ext(operation, _floatType, first, GetFloatSource(instruction, 2))); + } + + private uint EmitIntegerBinary( + Gen5ShaderInstruction instruction, + SpirvOp operation, + bool reverse = false) + { + var left = GetRawSource(instruction, reverse ? 1 : 0); + var right = GetRawSource(instruction, reverse ? 0 : 1); + if (operation == SpirvOp.ShiftLeftLogical) + { + return ShiftLeftLogical(left, right); + } + + if (operation == SpirvOp.ShiftRightLogical) + { + return ShiftRightLogical(left, right); + } + + if (operation == SpirvOp.ShiftRightArithmetic) + { + return ShiftRightArithmetic(left, right); + } + + return _module.AddInstruction(operation, _uintType, left, right); + } + + private enum CubeCoordinate + { + Id, + Sc, + Tc, + Ma, + } + + private uint EmitCvtOffF32I4(Gen5ShaderInstruction instruction) + { + var index = BitwiseAnd(GetRawSource(instruction, 0), UInt(15)); + ReadOnlySpan table = + [ + 0.0f, + 0.0625f, + 0.1250f, + 0.1875f, + 0.2500f, + 0.3125f, + 0.3750f, + 0.4375f, + -0.5000f, + -0.4375f, + -0.3750f, + -0.3125f, + -0.2500f, + -0.1875f, + -0.1250f, + -0.0625f, + ]; + + var result = UInt(BitConverter.SingleToUInt32Bits(table[^1])); + for (var tableIndex = table.Length - 2; tableIndex >= 0; tableIndex--) + { + var matches = _module.AddInstruction( + SpirvOp.IEqual, + _boolType, + index, + UInt((uint)tableIndex)); + result = _module.AddInstruction( + SpirvOp.Select, + _uintType, + matches, + UInt(BitConverter.SingleToUInt32Bits(table[tableIndex])), + result); + } + + return result; + } + + private uint EmitCubeCoordinate( + Gen5ShaderInstruction instruction, + CubeCoordinate coordinate) + { + var x = GetFloatSource(instruction, 0); + var y = GetFloatSource(instruction, 1); + var z = GetFloatSource(instruction, 2); + var nx = _module.AddInstruction(SpirvOp.FNegate, _floatType, x); + var ny = _module.AddInstruction(SpirvOp.FNegate, _floatType, y); + var nz = _module.AddInstruction(SpirvOp.FNegate, _floatType, z); + var ax = Ext(4, _floatType, x); + var ay = Ext(4, _floatType, y); + var az = Ext(4, _floatType, z); + var amaxXY = Ext(40, _floatType, ax, ay); + var amax = Ext(40, _floatType, az, amaxXY); + var ma = _module.AddInstruction( + SpirvOp.FMul, + _floatType, + Float(2), + amax); + if (coordinate == CubeCoordinate.Ma) + { + return EmitFloatResult(instruction, ma); + } + + var isZMax = _module.AddInstruction( + SpirvOp.FOrdGreaterThanEqual, + _boolType, + az, + amaxXY); + var yGreaterOrEqualX = _module.AddInstruction( + SpirvOp.FOrdGreaterThanEqual, + _boolType, + ay, + ax); + var isYMax = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + _module.AddInstruction(SpirvOp.LogicalNot, _boolType, isZMax), + yGreaterOrEqualX); + if (coordinate == CubeCoordinate.Id) + { + var isZNeg = _module.AddInstruction( + SpirvOp.FOrdLessThan, + _boolType, + z, + Float(0)); + var isYNeg = _module.AddInstruction( + SpirvOp.FOrdLessThan, + _boolType, + y, + Float(0)); + var isXNeg = _module.AddInstruction( + SpirvOp.FOrdLessThan, + _boolType, + x, + Float(0)); + var zCase = _module.AddInstruction( + SpirvOp.Select, + _floatType, + isZNeg, + Float(5), + Float(4)); + var yCase = _module.AddInstruction( + SpirvOp.Select, + _floatType, + isYNeg, + Float(3), + Float(2)); + var xCase = _module.AddInstruction( + SpirvOp.Select, + _floatType, + isXNeg, + Float(1), + Float(0)); + var xyCase = _module.AddInstruction( + SpirvOp.Select, + _floatType, + yGreaterOrEqualX, + yCase, + xCase); + return EmitFloatResult( + instruction, + _module.AddInstruction( + SpirvOp.Select, + _floatType, + isZMax, + zCase, + xyCase)); + } + + if (coordinate == CubeCoordinate.Sc) + { + var isZNeg = _module.AddInstruction( + SpirvOp.FOrdLessThan, + _boolType, + z, + Float(0)); + var isXNeg = _module.AddInstruction( + SpirvOp.FOrdLessThan, + _boolType, + x, + Float(0)); + var zCase = _module.AddInstruction( + SpirvOp.Select, + _floatType, + isZNeg, + nx, + x); + var xCase = _module.AddInstruction( + SpirvOp.Select, + _floatType, + isXNeg, + z, + nz); + var nonZCase = _module.AddInstruction( + SpirvOp.Select, + _floatType, + isYMax, + x, + xCase); + return EmitFloatResult( + instruction, + _module.AddInstruction( + SpirvOp.Select, + _floatType, + isZMax, + zCase, + nonZCase)); + } + + var tcIsYNeg = _module.AddInstruction( + SpirvOp.FOrdLessThan, + _boolType, + y, + Float(0)); + var tcYCase = _module.AddInstruction( + SpirvOp.Select, + _floatType, + tcIsYNeg, + nz, + z); + return EmitFloatResult( + instruction, + _module.AddInstruction( + SpirvOp.Select, + _floatType, + isYMax, + tcYCase, + ny)); + } + + private uint EmitAddWithCarry(Gen5ShaderInstruction instruction) + { + var left = GetRawSource(instruction, 0); + var right = GetRawSource(instruction, 1); + var carryIn = _module.AddInstruction( + SpirvOp.Select, + _uintType, + Load(_boolType, _vcc), + UInt(1), + UInt(0)); + var partial = IAdd(left, right); + var result = IAdd(partial, carryIn); + var carry = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + _module.AddInstruction(SpirvOp.ULessThan, _boolType, partial, left), + _module.AddInstruction(SpirvOp.ULessThan, _boolType, result, partial)); + StoreWaveMask(106, carry); + return result; + } + + private uint EmitSubtractWithBorrow( + Gen5ShaderInstruction instruction, + bool reverse) + { + var left = GetRawSource(instruction, reverse ? 1 : 0); + var right = GetRawSource(instruction, reverse ? 0 : 1); + var borrowIn = _module.AddInstruction( + SpirvOp.Select, + _uintType, + Load(_boolType, _vcc), + UInt(1), + UInt(0)); + var partial = _module.AddInstruction(SpirvOp.ISub, _uintType, left, right); + var result = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + partial, + borrowIn); + var borrow = _module.AddInstruction( + SpirvOp.LogicalOr, + _boolType, + _module.AddInstruction(SpirvOp.ULessThan, _boolType, left, right), + _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + partial, + borrowIn)); + StoreWaveMask(106, borrow); + return result; + } + + private void StoreCarryOut( + Gen5ShaderInstruction instruction, + uint carry) + { + if (instruction.Control is Gen5Vop3Control { ScalarDestination: { } register }) + { + StoreS( + register, + _module.AddInstruction( + SpirvOp.Select, + _uintType, + carry, + UInt(1), + UInt(0))); + if (register == 106) + { + StoreWaveMask(106, carry); + } + + return; + } + + StoreWaveMask(106, carry); + } + + private uint EmitPermlane16( + Gen5ShaderInstruction instruction, + bool exchangeRows) + { + var value = GetRawSource(instruction, 0); + var selectorLow = GetRawSource(instruction, 1); + var selectorHigh = GetRawSource(instruction, 2); + var lane = Load(_uintType, _subgroupInvocationIdInput); + var localLane = BitwiseAnd(lane, UInt(15)); + var lowHalf = _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + localLane, + UInt(8)); + var lowShift = ShiftLeftLogical(localLane, UInt(2)); + var highLane = _module.AddInstruction( + SpirvOp.ISub, + _uintType, + localLane, + UInt(8)); + var highShift = ShiftLeftLogical(highLane, UInt(2)); + var lowSelector = BitwiseAnd( + ShiftRightLogical(selectorLow, lowShift), + UInt(15)); + var highSelector = BitwiseAnd( + ShiftRightLogical(selectorHigh, highShift), + UInt(15)); + var selector = _module.AddInstruction( + SpirvOp.Select, + _uintType, + lowHalf, + lowSelector, + highSelector); + var rowBase = BitwiseAnd(lane, UInt(0xFFFF_FFF0)); + if (exchangeRows) + { + rowBase = BitwiseXor(rowBase, UInt(16)); + } + + var targetLane = IAdd(rowBase, selector); + return _module.AddInstruction( + SpirvOp.GroupNonUniformShuffle, + _uintType, + UInt(3), + value, + targetLane); + } + + private uint EmitFloatResult( + Gen5ShaderInstruction instruction, + uint value) + { + uint outputModifier = 0; + var clamp = false; + switch (instruction.Control) + { + case Gen5Vop3Control control: + outputModifier = control.OutputModifier; + clamp = control.Clamp; + break; + case Gen5SdwaControl control: + outputModifier = control.OutputModifier; + clamp = control.Clamp; + break; + } + + value = outputModifier switch + { + 1 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(2)), + 2 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(4)), + 3 => _module.AddInstruction(SpirvOp.FMul, _floatType, value, Float(0.5f)), + _ => value, + }; + if (clamp) + { + value = Ext(43, _floatType, value, Float(0), Float(1)); + } + + return Bitcast(_uintType, value); + } + + private uint TruncateFloat32ForPack(uint value) + { + var raw = BitwiseAnd( + Bitcast(_uintType, value), + UInt(0xFFFF_E000)); + return Bitcast(_floatType, raw); + } + + private uint Ext(uint operation, uint resultType, params uint[] operands) + { + var values = new uint[2 + operands.Length]; + values[0] = _glsl; + values[1] = operation; + operands.CopyTo(values, 2); + return _module.AddInstruction(SpirvOp.ExtInst, resultType, values); + } + + private uint IsNotZero(uint value) => + _module.AddInstruction(SpirvOp.INotEqual, _boolType, value, UInt(0)); + + private uint IsNotZero64(uint value) => + _module.AddInstruction( + SpirvOp.INotEqual, + _boolType, + value, + _module.Constant64(_ulongType, 0)); + + private uint SignBit(uint value) => + ShiftRightLogical(value, UInt(31)); + + private uint SignedAddOverflow(uint left, uint right, uint result) + { + var leftSign = SignBit(left); + var rightSign = SignBit(right); + var resultSign = SignBit(result); + var sameSourceSign = _module.AddInstruction( + SpirvOp.IEqual, + _boolType, + leftSign, + rightSign); + var resultSignChanged = _module.AddInstruction( + SpirvOp.INotEqual, + _boolType, + leftSign, + resultSign); + return _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + sameSourceSign, + resultSignChanged); + } + + private uint SignedSubOverflow(uint left, uint right, uint result) + { + var leftSign = SignBit(left); + var rightSign = SignBit(right); + var resultSign = SignBit(result); + var differentSourceSign = _module.AddInstruction( + SpirvOp.INotEqual, + _boolType, + leftSign, + rightSign); + var resultSignChanged = _module.AddInstruction( + SpirvOp.INotEqual, + _boolType, + leftSign, + resultSign); + return _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + differentSourceSign, + resultSignChanged); + } + + private static bool TryDecodeInlineConstant(uint encoded, out uint value) + { + if (encoded == 125) + { + value = 0; + return true; + } + + if (encoded is >= 128 and <= 192) + { + value = encoded - 128; + return true; + } + + if (encoded is >= 193 and <= 208) + { + value = unchecked((uint)-(int)(encoded - 192)); + return true; + } + + var floatingPoint = encoded switch + { + 240 => 0.5f, + 241 => -0.5f, + 242 => 1.0f, + 243 => -1.0f, + 244 => 2.0f, + 245 => -2.0f, + 246 => 4.0f, + 247 => -4.0f, + 248 => 1.0f / (2.0f * MathF.PI), + _ => float.NaN, + }; + if (float.IsNaN(floatingPoint)) + { + value = 0; + return false; + } + + value = BitConverter.SingleToUInt32Bits(floatingPoint); + return true; + } + } +} diff --git a/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs new file mode 100644 index 0000000..8a0849d --- /dev/null +++ b/src/SharpEmu.Libs/Agc/Gen5SpirvTranslator.cs @@ -0,0 +1,2513 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.Agc; + +internal static partial class Gen5SpirvTranslator +{ + private const uint ScalarRegisterCount = 256; + private const uint VectorRegisterCount = 512; + private const uint LdsDwordCount = 8192; + private const uint RdnaWaveLaneCount = 32; + + public static bool TryCompilePixelShader( + Gen5ShaderState state, + Gen5ShaderEvaluation evaluation, + Gen5PixelOutputKind outputKind, + out Gen5SpirvShader shader, + out string error, + int globalBufferBase = 0, + int totalGlobalBufferCount = -1, + int imageBindingBase = 0) + { + var context = new CompilationContext( + Gen5SpirvStage.Pixel, + state, + evaluation, + outputKind, + 1, + 1, + 1, + globalBufferBase, + totalGlobalBufferCount, + imageBindingBase); + return context.TryCompile(out shader, out error); + } + + public static bool TryCompileVertexShader( + Gen5ShaderState state, + Gen5ShaderEvaluation evaluation, + out Gen5SpirvShader shader, + out string error, + int globalBufferBase = 0, + int totalGlobalBufferCount = -1, + int imageBindingBase = 0) + { + var context = new CompilationContext( + Gen5SpirvStage.Vertex, + state, + evaluation, + Gen5PixelOutputKind.Float, + 1, + 1, + 1, + globalBufferBase, + totalGlobalBufferCount, + imageBindingBase); + return context.TryCompile(out shader, out error); + } + + public static bool TryCompileComputeShader( + Gen5ShaderState state, + Gen5ShaderEvaluation evaluation, + uint localSizeX, + uint localSizeY, + uint localSizeZ, + out Gen5SpirvShader shader, + out string error) + { + var context = new CompilationContext( + Gen5SpirvStage.Compute, + state, + evaluation, + Gen5PixelOutputKind.Float, + Math.Max(localSizeX, 1), + Math.Max(localSizeY, 1), + Math.Max(localSizeZ, 1), + 0, + -1, + 0); + return context.TryCompile(out shader, out error); + } + + private sealed partial class CompilationContext + { + private readonly SpirvModuleBuilder _module = new(); + private readonly Gen5SpirvStage _stage; + private readonly Gen5ShaderState _state; + private readonly Gen5ShaderEvaluation _evaluation; + private readonly Gen5PixelOutputKind _outputKind; + private readonly uint _localSizeX; + private readonly uint _localSizeY; + private readonly uint _localSizeZ; + private readonly int _globalBufferBase; + private readonly int _totalGlobalBufferCount; + private readonly int _imageBindingBase; + private readonly List _interfaces = []; + private readonly Dictionary _pixelInputs = []; + private readonly Dictionary _vertexOutputs = []; + private readonly Dictionary _vertexInputsByPc = []; + private readonly List _imageResources = []; + private readonly Dictionary _imageBindingByPc = []; + private readonly Dictionary _bufferBindingByPc = []; + 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 _privateBoolPointer; + private uint _scalarRegisters; + private uint _vectorRegisters; + private uint _scc; + private uint _vcc; + private uint _exec; + private uint _programCounter; + private uint _programActive; + private uint _globalBuffers; + private uint _storageUintPointer; + private uint _lds; + private uint _workgroupUintPointer; + private uint _positionOutput; + private uint _pixelOutput; + private uint _vertexIndexInput; + private uint _instanceIndexInput; + private uint _fragCoordInput; + private uint _localInvocationIdInput; + private uint _workGroupIdInput; + private uint _subgroupInvocationIdInput; + private uint _glsl; + + private enum ImageComponentKind + { + Float, + Sint, + Uint, + } + + private readonly record struct SpirvImageResource( + uint Variable, + uint ImageType, + uint ObjectType, + uint ComponentType, + uint VectorType, + ImageComponentKind ComponentKind, + bool IsStorage); + + private readonly record struct SpirvVertexInput( + uint Variable, + uint Type, + uint ComponentCount); + + public CompilationContext( + Gen5SpirvStage stage, + Gen5ShaderState state, + Gen5ShaderEvaluation evaluation, + Gen5PixelOutputKind outputKind, + uint localSizeX, + uint localSizeY, + uint localSizeZ, + int globalBufferBase, + int totalGlobalBufferCount, + int imageBindingBase) + { + _stage = stage; + _state = state; + _evaluation = evaluation; + _outputKind = outputKind; + _localSizeX = localSizeX; + _localSizeY = localSizeY; + _localSizeZ = localSizeZ; + _globalBufferBase = globalBufferBase; + _totalGlobalBufferCount = totalGlobalBufferCount < 0 + ? evaluation.GlobalMemoryBindings.Count + : totalGlobalBufferCount; + _imageBindingBase = imageBindingBase; + } + + public bool TryCompile(out Gen5SpirvShader shader, out string error) + { + shader = default!; + error = string.Empty; + try + { + DeclareModule(); + var blocks = BuildBasicBlocks(_state.Program.Instructions); + 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(); + EmitInitialState(); + + var loopHeader = _module.AllocateId(); + var switchHeader = _module.AllocateId(); + var switchMerge = _module.AllocateId(); + var loopContinue = _module.AllocateId(); + var loopMerge = _module.AllocateId(); + var defaultLabel = _module.AllocateId(); + var caseLabels = new uint[blocks.Count]; + for (var index = 0; index < caseLabels.Length; index++) + { + caseLabels[index] = _module.AllocateId(); + } + + _module.AddStatement(SpirvOp.Branch, loopHeader); + _module.AddLabel(loopHeader); + _module.AddStatement(SpirvOp.LoopMerge, loopMerge, loopContinue, 0); + _module.AddStatement(SpirvOp.Branch, switchHeader); + + _module.AddLabel(switchHeader); + var selector = Load(_uintType, _programCounter); + _module.AddStatement(SpirvOp.SelectionMerge, switchMerge, 0); + var switchOperands = new uint[2 + (blocks.Count * 2)]; + switchOperands[0] = selector; + switchOperands[1] = defaultLabel; + for (var index = 0; index < blocks.Count; index++) + { + switchOperands[2 + (index * 2)] = (uint)index; + switchOperands[3 + (index * 2)] = caseLabels[index]; + } + + _module.AddStatement(SpirvOp.Switch, switchOperands); + for (var index = 0; index < blocks.Count; index++) + { + _module.AddLabel(caseLabels[index]); + if (!TryEmitBlock(blocks, index, out error)) + { + error = $"block=0x{blocks[index].StartPc:X}: {error}"; + return false; + } + + _module.AddStatement(SpirvOp.Branch, switchMerge); + } + + _module.AddLabel(defaultLabel); + Store(_programActive, _module.ConstantBool(false)); + _module.AddStatement(SpirvOp.Branch, switchMerge); + + _module.AddLabel(switchMerge); + _module.AddStatement(SpirvOp.Branch, loopContinue); + _module.AddLabel(loopContinue); + var active = Load(_boolType, _programActive); + _module.AddStatement( + SpirvOp.BranchConditional, + active, + loopHeader, + loopMerge); + _module.AddLabel(loopMerge); + _module.AddStatement(SpirvOp.Return); + _module.EndFunction(); + + var model = _stage switch + { + Gen5SpirvStage.Vertex => SpirvExecutionModel.Vertex, + Gen5SpirvStage.Pixel => SpirvExecutionModel.Fragment, + _ => SpirvExecutionModel.GLCompute, + }; + _module.AddEntryPoint(model, main, "main", _interfaces); + if (_stage == Gen5SpirvStage.Pixel) + { + _module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft); + } + else if (_stage == Gen5SpirvStage.Compute) + { + _module.AddExecutionMode( + main, + SpirvExecutionMode.LocalSize, + _localSizeX, + _localSizeY, + _localSizeZ); + } + + var attributeCount = _stage == Gen5SpirvStage.Vertex + ? (uint)_vertexOutputs.Count + : (uint)_pixelInputs.Count; + shader = new Gen5SpirvShader( + _module.Build(), + _evaluation.GlobalMemoryBindings, + _evaluation.ImageBindings, + attributeCount, + _stage == Gen5SpirvStage.Vertex + ? _evaluation.VertexInputs ?? [] + : []); + return true; + } + catch (Exception exception) + { + error = exception.Message; + return false; + } + } + + private void DeclareModule() + { + _module.AddCapability(SpirvCapability.Shader); + _module.AddCapability(SpirvCapability.Int64); + _module.AddCapability(SpirvCapability.ImageQuery); + if (_evaluation.ImageBindings.Any( + static binding => + (binding.Opcode.StartsWith( + "ImageSample", + StringComparison.Ordinal) || + binding.Opcode.StartsWith( + "ImageGather4", + StringComparison.Ordinal)) && + binding.Opcode.EndsWith("O", StringComparison.Ordinal))) + { + _module.AddCapability(SpirvCapability.ImageGatherExtended); + } + + if (UsesSubgroupOperations()) + { + _module.AddCapability(SpirvCapability.GroupNonUniform); + if (UsesSubgroupShuffle()) + { + _module.AddCapability(SpirvCapability.GroupNonUniformShuffle); + } + + if (UsesWaveControl()) + { + _module.AddCapability(SpirvCapability.GroupNonUniformVote); + } + } + + _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); + _privateBoolPointer = + _module.TypePointer(SpirvStorageClass.Private, _boolType); + + var scalarArrayType = _module.TypeArray(_uintType, ScalarRegisterCount); + var vectorArrayType = _module.TypeArray(_uintType, VectorRegisterCount); + var privateScalarArrayPointer = + _module.TypePointer(SpirvStorageClass.Private, scalarArrayType); + var privateVectorArrayPointer = + _module.TypePointer(SpirvStorageClass.Private, vectorArrayType); + _scalarRegisters = _module.AddGlobalVariable( + privateScalarArrayPointer, + SpirvStorageClass.Private, + _module.ConstantNull(scalarArrayType)); + _vectorRegisters = _module.AddGlobalVariable( + privateVectorArrayPointer, + SpirvStorageClass.Private, + _module.ConstantNull(vectorArrayType)); + _scc = _module.AddGlobalVariable( + _privateBoolPointer, + SpirvStorageClass.Private, + _module.ConstantBool(false)); + _vcc = _module.AddGlobalVariable( + _privateBoolPointer, + SpirvStorageClass.Private, + _module.ConstantBool(false)); + _exec = _module.AddGlobalVariable( + _privateBoolPointer, + SpirvStorageClass.Private, + _module.ConstantBool(true)); + _programCounter = _module.AddGlobalVariable( + _privateUintPointer, + SpirvStorageClass.Private, + _module.Constant(_uintType, 0)); + _programActive = _module.AddGlobalVariable( + _privateBoolPointer, + SpirvStorageClass.Private, + _module.ConstantBool(true)); + _interfaces.Add(_scalarRegisters); + _interfaces.Add(_vectorRegisters); + _interfaces.Add(_scc); + _interfaces.Add(_vcc); + _interfaces.Add(_exec); + _interfaces.Add(_programCounter); + _interfaces.Add(_programActive); + _module.AddName(_scalarRegisters, "sgpr"); + _module.AddName(_vectorRegisters, "vgpr"); + + DeclareBuffers(); + DeclareImages(); + DeclareLds(); + DeclareStageInterface(); + } + + private void DeclareLds() + { + if (_stage != Gen5SpirvStage.Compute || !UsesLds()) + { + return; + } + + var ldsArrayType = _module.TypeArray(_uintType, LdsDwordCount); + var ldsPointer = + _module.TypePointer(SpirvStorageClass.Workgroup, ldsArrayType); + _workgroupUintPointer = + _module.TypePointer(SpirvStorageClass.Workgroup, _uintType); + _lds = _module.AddGlobalVariable( + ldsPointer, + SpirvStorageClass.Workgroup); + _module.AddName(_lds, "lds"); + _interfaces.Add(_lds); + } + + private void DeclareBuffers() + { + for (var index = 0; index < _evaluation.GlobalMemoryBindings.Count; index++) + { + foreach (var pc in _evaluation.GlobalMemoryBindings[index].InstructionPcs) + { + _bufferBindingByPc.TryAdd(pc, _globalBufferBase + index); + } + } + + if (_totalGlobalBufferCount == 0) + { + return; + } + + var runtimeArray = _module.TypeRuntimeArray(_uintType); + _module.AddDecoration(runtimeArray, SpirvDecoration.ArrayStride, sizeof(uint)); + var block = _module.TypeStruct(runtimeArray); + _module.AddDecoration(block, SpirvDecoration.Block); + _module.AddMemberDecoration(block, 0, SpirvDecoration.Offset, 0); + var descriptors = _module.TypeArray( + block, + (uint)_totalGlobalBufferCount); + var descriptorsPointer = + _module.TypePointer(SpirvStorageClass.StorageBuffer, descriptors); + _storageUintPointer = + _module.TypePointer(SpirvStorageClass.StorageBuffer, _uintType); + _globalBuffers = _module.AddGlobalVariable( + descriptorsPointer, + SpirvStorageClass.StorageBuffer); + _module.AddName(_globalBuffers, "guestBuffers"); + _module.AddDecoration(_globalBuffers, SpirvDecoration.DescriptorSet, 0); + _module.AddDecoration(_globalBuffers, SpirvDecoration.Binding, 0); + _interfaces.Add(_globalBuffers); + } + + private void DeclareImages() + { + for (var index = 0; index < _evaluation.ImageBindings.Count; index++) + { + var binding = _evaluation.ImageBindings[index]; + _imageBindingByPc.TryAdd(binding.Pc, index); + var isStorage = + Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode); + var isDepth = + binding.Opcode.Contains("SampleC", StringComparison.Ordinal) || + binding.Opcode.Contains("Gather4C", StringComparison.Ordinal); + var (format, componentKind) = + DecodeImageFormat(binding.ResourceDescriptor); + var componentType = componentKind switch + { + ImageComponentKind.Sint => _intType, + ImageComponentKind.Uint => _uintType, + _ => _floatType, + }; + if (isStorage && format == SpirvImageFormat.Unknown) + { + _module.AddCapability( + SpirvCapability.StorageImageReadWithoutFormat); + _module.AddCapability( + SpirvCapability.StorageImageWriteWithoutFormat); + } + else if (isStorage && RequiresExtendedStorageImageFormat(format)) + { + _module.AddCapability( + SpirvCapability.StorageImageExtendedFormats); + } + + var imageType = _module.TypeImage( + componentType, + SpirvImageDim.Dim2D, + depth: isDepth, + arrayed: false, + multisampled: false, + sampled: isStorage ? 2u : 1u, + isStorage ? format : SpirvImageFormat.Unknown); + var objectType = isStorage + ? imageType + : _module.TypeSampledImage(imageType); + var pointer = _module.TypePointer( + SpirvStorageClass.UniformConstant, + objectType); + var variable = _module.AddGlobalVariable( + pointer, + SpirvStorageClass.UniformConstant); + _module.AddName(variable, isStorage ? $"image{index}" : $"tex{index}"); + _module.AddDecoration(variable, SpirvDecoration.DescriptorSet, 0); + _module.AddDecoration( + variable, + SpirvDecoration.Binding, + (uint)(_imageBindingBase + index + 1)); + _imageResources.Add( + new SpirvImageResource( + variable, + imageType, + objectType, + componentType, + _module.TypeVector(componentType, 4), + componentKind, + isStorage)); + _interfaces.Add(variable); + } + } + + private static bool RequiresExtendedStorageImageFormat( + SpirvImageFormat format) => + format is not SpirvImageFormat.Unknown and + not SpirvImageFormat.Rgba32f and + not SpirvImageFormat.Rgba32i and + not SpirvImageFormat.Rgba32ui; + + private static (SpirvImageFormat Format, ImageComponentKind Kind) + DecodeImageFormat(IReadOnlyList descriptor) + { + if (descriptor.Count < 2) + { + return (SpirvImageFormat.Unknown, ImageComponentKind.Float); + } + + var dataFormat = (descriptor[1] >> 20) & 0x1FFu; + var numberType = (descriptor[1] >> 26) & 0xFu; + return (dataFormat, numberType) switch + { + (1, _) => (SpirvImageFormat.R8, ImageComponentKind.Float), + (2, _) => (SpirvImageFormat.R16f, ImageComponentKind.Float), + (3, _) => (SpirvImageFormat.Rg8, ImageComponentKind.Float), + (4, 4) => (SpirvImageFormat.R32ui, ImageComponentKind.Uint), + (4, 5) => (SpirvImageFormat.R32i, ImageComponentKind.Sint), + (4, _) => (SpirvImageFormat.R32f, ImageComponentKind.Float), + (5, 4) => (SpirvImageFormat.Rg16ui, ImageComponentKind.Uint), + (5, 5) => (SpirvImageFormat.Rg16i, ImageComponentKind.Sint), + (5, 0) => (SpirvImageFormat.Rg16, ImageComponentKind.Float), + (5, _) => (SpirvImageFormat.Rg16f, ImageComponentKind.Float), + (6 or 7, _) => ( + SpirvImageFormat.R11fG11fB10f, + ImageComponentKind.Float), + (9, 4) => (SpirvImageFormat.Rgb10A2ui, ImageComponentKind.Uint), + (9, _) => (SpirvImageFormat.Rgb10A2, ImageComponentKind.Float), + (10, 4) => (SpirvImageFormat.Rgba8ui, ImageComponentKind.Uint), + (10, 5) => (SpirvImageFormat.Rgba8i, ImageComponentKind.Sint), + (10, _) => (SpirvImageFormat.Rgba8, ImageComponentKind.Float), + (11, 4) => (SpirvImageFormat.Rg32ui, ImageComponentKind.Uint), + (11, 5) => (SpirvImageFormat.Rg32i, ImageComponentKind.Sint), + (11, _) => (SpirvImageFormat.Rg32f, ImageComponentKind.Float), + (12, 4) => (SpirvImageFormat.Rgba16ui, ImageComponentKind.Uint), + (12, 5) => (SpirvImageFormat.Rgba16i, ImageComponentKind.Sint), + (12, 0) => (SpirvImageFormat.Rgba16, ImageComponentKind.Float), + (12, _) => (SpirvImageFormat.Rgba16f, ImageComponentKind.Float), + (13 or 14, 4) => ( + SpirvImageFormat.Rgba32ui, + ImageComponentKind.Uint), + (13 or 14, 5) => ( + SpirvImageFormat.Rgba32i, + ImageComponentKind.Sint), + (13 or 14, _) => ( + SpirvImageFormat.Rgba32f, + ImageComponentKind.Float), + (20, _) => (SpirvImageFormat.R32ui, ImageComponentKind.Uint), + (22, _) => (SpirvImageFormat.Rgba16f, ImageComponentKind.Float), + (29, _) => (SpirvImageFormat.R32f, ImageComponentKind.Float), + (36, _) => (SpirvImageFormat.R8, ImageComponentKind.Float), + (49, _) => (SpirvImageFormat.R8ui, ImageComponentKind.Uint), + (56 or 62 or 64, _) => ( + SpirvImageFormat.Rgba8, + ImageComponentKind.Float), + (71, _) => (SpirvImageFormat.Rgba16f, ImageComponentKind.Float), + (75, _) => (SpirvImageFormat.Rg32f, ImageComponentKind.Float), + (_, 4) => (SpirvImageFormat.Unknown, ImageComponentKind.Uint), + (_, 5) => (SpirvImageFormat.Unknown, ImageComponentKind.Sint), + _ => (SpirvImageFormat.Unknown, ImageComponentKind.Float), + }; + } + + private void DeclareStageInterface() + { + if (UsesSubgroupOperations()) + { + var subgroupPointer = + _module.TypePointer(SpirvStorageClass.Input, _uintType); + _subgroupInvocationIdInput = _module.AddGlobalVariable( + subgroupPointer, + SpirvStorageClass.Input); + _module.AddDecoration( + _subgroupInvocationIdInput, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.SubgroupLocalInvocationId); + _interfaces.Add(_subgroupInvocationIdInput); + } + + if (_stage == Gen5SpirvStage.Vertex) + { + DeclareVertexInputs(); + + var inputPointer = + _module.TypePointer(SpirvStorageClass.Input, _uintType); + _vertexIndexInput = _module.AddGlobalVariable( + inputPointer, + SpirvStorageClass.Input); + _module.AddDecoration( + _vertexIndexInput, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.VertexIndex); + _interfaces.Add(_vertexIndexInput); + + _instanceIndexInput = _module.AddGlobalVariable( + inputPointer, + SpirvStorageClass.Input); + _module.AddDecoration( + _instanceIndexInput, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.InstanceIndex); + _interfaces.Add(_instanceIndexInput); + + var outputPointer = + _module.TypePointer(SpirvStorageClass.Output, _vec4Type); + _positionOutput = _module.AddGlobalVariable( + outputPointer, + SpirvStorageClass.Output); + _module.AddDecoration( + _positionOutput, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.Position); + _interfaces.Add(_positionOutput); + + var parameters = _state.Program.Instructions + .Select(instruction => instruction.Control) + .OfType() + .Where(export => export.Target is >= 32 and < 64) + .Select(export => export.Target - 32) + .Distinct() + .Order() + .ToArray(); + foreach (var parameter in parameters) + { + var variable = _module.AddGlobalVariable( + outputPointer, + SpirvStorageClass.Output); + _module.AddDecoration(variable, SpirvDecoration.Location, parameter); + _vertexOutputs.Add(parameter, variable); + _interfaces.Add(variable); + } + } + else if (_stage == Gen5SpirvStage.Pixel) + { + var inputVec4Pointer = + _module.TypePointer(SpirvStorageClass.Input, _vec4Type); + var attributes = _state.Program.Instructions + .Select(instruction => instruction.Control) + .OfType() + .Select(control => control.Attribute) + .Distinct() + .Order() + .ToArray(); + foreach (var attribute in attributes) + { + var variable = _module.AddGlobalVariable( + inputVec4Pointer, + SpirvStorageClass.Input); + _module.AddDecoration(variable, SpirvDecoration.Location, attribute); + _pixelInputs.Add(attribute, variable); + _interfaces.Add(variable); + } + + _fragCoordInput = _module.AddGlobalVariable( + inputVec4Pointer, + SpirvStorageClass.Input); + _module.AddDecoration( + _fragCoordInput, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.FragCoord); + _interfaces.Add(_fragCoordInput); + + var outputType = _outputKind switch + { + Gen5PixelOutputKind.Uint => _uvec4Type, + Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4), + _ => _vec4Type, + }; + var outputPointer = + _module.TypePointer(SpirvStorageClass.Output, outputType); + _pixelOutput = _module.AddGlobalVariable( + outputPointer, + SpirvStorageClass.Output); + _module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0); + _interfaces.Add(_pixelOutput); + } + else + { + var inputPointer = + _module.TypePointer(SpirvStorageClass.Input, _uvec3Type); + _localInvocationIdInput = _module.AddGlobalVariable( + inputPointer, + SpirvStorageClass.Input); + _module.AddDecoration( + _localInvocationIdInput, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.LocalInvocationId); + _workGroupIdInput = _module.AddGlobalVariable( + inputPointer, + SpirvStorageClass.Input); + _module.AddDecoration( + _workGroupIdInput, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.WorkgroupId); + _interfaces.Add(_localInvocationIdInput); + _interfaces.Add(_workGroupIdInput); + } + } + + private void DeclareVertexInputs() + { + foreach (var input in _evaluation.VertexInputs ?? []) + { + var type = input.ComponentCount switch + { + 1u => _floatType, + 2u => _vec2Type, + 3u => _vec3Type, + 4u => _vec4Type, + _ => 0u, + }; + if (type == 0) + { + continue; + } + + var pointer = _module.TypePointer(SpirvStorageClass.Input, type); + var variable = _module.AddGlobalVariable( + pointer, + SpirvStorageClass.Input); + _module.AddName(variable, $"attr{input.Location}"); + _module.AddDecoration( + variable, + SpirvDecoration.Location, + input.Location); + _vertexInputsByPc.TryAdd( + input.Pc, + new SpirvVertexInput( + variable, + type, + input.ComponentCount)); + _interfaces.Add(variable); + } + } + + private void EmitInitialState() + { + for (uint index = 0; + index < _evaluation.InitialScalarRegisters.Count && + index < ScalarRegisterCount; + index++) + { + var value = _evaluation.InitialScalarRegisters[(int)index]; + if (value != 0) + { + StoreS(index, UInt(value)); + } + } + + Store(_scc, _module.ConstantBool(false)); + if (_subgroupInvocationIdInput != 0) + { + StoreWaveMask(106, _module.ConstantBool(false)); + StoreWaveMask(126, _module.ConstantBool(true)); + } + else + { + Store(_vcc, _module.ConstantBool(false)); + Store(_exec, _module.ConstantBool(true)); + } + 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); + } + else if (_stage == Gen5SpirvStage.Pixel) + { + var fragCoord = Load(_vec4Type, _fragCoordInput); + var x = _module.AddInstruction( + SpirvOp.CompositeExtract, + _floatType, + fragCoord, + 0); + var y = _module.AddInstruction( + SpirvOp.CompositeExtract, + _floatType, + fragCoord, + 1); + StoreV(2, Bitcast(_uintType, x), guardWithExec: false); + StoreV(3, Bitcast(_uintType, y), guardWithExec: false); + Store(_pixelOutput, _module.ConstantNull(GetPixelOutputType())); + } + else + { + var localId = Load(_uvec3Type, _localInvocationIdInput); + for (uint component = 0; component < 3; component++) + { + var value = _module.AddInstruction( + SpirvOp.CompositeExtract, + _uintType, + localId, + component); + StoreV(component, value, guardWithExec: false); + } + + if (_state.ComputeSystemRegisters is { } registers) + { + var workGroupId = Load(_uvec3Type, _workGroupIdInput); + StoreComputeSystemRegister( + registers.WorkGroupXRegister, + workGroupId, + 0); + StoreComputeSystemRegister( + registers.WorkGroupYRegister, + workGroupId, + 1); + StoreComputeSystemRegister( + registers.WorkGroupZRegister, + workGroupId, + 2); + if (registers.ThreadGroupSizeRegister is { } sizeRegister) + { + StoreS( + sizeRegister, + UInt(checked(_localSizeX * _localSizeY * _localSizeZ))); + } + } + } + } + + private void StoreComputeSystemRegister( + uint? register, + uint workGroupId, + uint component) + { + if (register is null) + { + return; + } + + var value = _module.AddInstruction( + SpirvOp.CompositeExtract, + _uintType, + workGroupId, + component); + StoreS(register.Value, value); + } + + private bool TryEmitBlock( + IReadOnlyList blocks, + int blockIndex, + out string error) + { + error = string.Empty; + var block = blocks[blockIndex]; + for (var index = block.StartIndex; index < block.EndIndex; index++) + { + var instruction = _state.Program.Instructions[index]; + if (IsBranch(instruction.Opcode) || instruction.Opcode == "SEndpgm") + { + continue; + } + + if (!TryEmitInstruction(instruction, out error)) + { + error = $"pc=0x{instruction.Pc:X} {instruction.Opcode}: {error}"; + return false; + } + } + + var terminator = _state.Program.Instructions[block.EndIndex - 1]; + if (terminator.Opcode == "SEndpgm") + { + Store(_programActive, _module.ConstantBool(false)); + return true; + } + + var fallthrough = blockIndex + 1 < blocks.Count + ? (uint)(blockIndex + 1) + : uint.MaxValue; + if (terminator.Opcode == "SBranch") + { + if (!TryGetBranchTargetPc(terminator, out var targetPc)) + { + error = "invalid scalar branch target"; + return false; + } + + if (IsExitBranchTarget(_state.Program.Instructions, targetPc)) + { + Store(_programActive, _module.ConstantBool(false)); + return true; + } + + if (!TryFindBlock(blocks, targetPc, out var targetBlock)) + { + error = $"invalid scalar branch target pc=0x{terminator.Pc:X} target=0x{targetPc:X} blocks={FormatBlockStarts(blocks)}"; + return false; + } + + Store(_programCounter, UInt((uint)targetBlock)); + return true; + } + + if (terminator.Opcode.StartsWith("SCbranch", StringComparison.Ordinal)) + { + var hasTarget = TryGetBranchTargetPc(terminator, out var targetPc); + var targetBlock = -1; + var hasTargetBlock = hasTarget && TryFindBlock(blocks, targetPc, out targetBlock); + var targetExits = hasTarget && IsExitBranchTarget(_state.Program.Instructions, targetPc); + var hasCondition = TryGetBranchCondition(terminator.Opcode, out var condition); + if (!hasTarget || (!hasTargetBlock && !targetExits) || !hasCondition) + { + error = + $"invalid conditional scalar branch opcode={terminator.Opcode} " + + $"pc=0x{terminator.Pc:X} " + + $"target={(hasTarget ? $"0x{targetPc:X}" : "invalid")} " + + $"target_block={(hasTargetBlock ? targetBlock.ToString() : targetExits ? "exit" : "missing")} " + + $"fallthrough={(fallthrough == uint.MaxValue ? "end" : fallthrough.ToString())} " + + $"condition={hasCondition} " + + $"blocks={FormatBlockStarts(blocks)}"; + return false; + } + + var takenBlock = targetExits ? uint.MaxValue : (uint)targetBlock; + var selected = _module.AddInstruction( + SpirvOp.Select, + _uintType, + condition, + UInt(takenBlock), + UInt(fallthrough)); + Store(_programCounter, selected); + return true; + } + + if (fallthrough == uint.MaxValue) + { + Store(_programActive, _module.ConstantBool(false)); + } + else + { + Store(_programCounter, UInt(fallthrough)); + } + + return true; + } + + private static string FormatBlockStarts(IReadOnlyList blocks) + { + const int maxBlocks = 32; + var count = Math.Min(blocks.Count, maxBlocks); + var starts = new string[count]; + for (var index = 0; index < count; index++) + { + starts[index] = $"0x{blocks[index].StartPc:X}"; + } + + return blocks.Count <= maxBlocks + ? string.Join(",", starts) + : string.Join(",", starts) + $",...({blocks.Count})"; + } + + private static bool IsExitBranchTarget( + IReadOnlyList instructions, + uint targetPc) + { + if (instructions.Count == 0) + { + return false; + } + + var last = instructions[^1]; + var lastEndPc = last.Pc + (uint)(last.Words.Count * sizeof(uint)); + return targetPc >= lastEndPc; + } + + private bool TryGetBranchCondition(string opcode, out uint condition) + { + condition = opcode switch + { + "SCbranchScc0" => LogicalNot(Load(_boolType, _scc)), + "SCbranchScc1" => Load(_boolType, _scc), + "SCbranchVccz" => LogicalNot(SubgroupAny(Load(_boolType, _vcc))), + "SCbranchVccnz" => SubgroupAny(Load(_boolType, _vcc)), + "SCbranchExecz" => LogicalNot(SubgroupAny(Load(_boolType, _exec))), + "SCbranchExecnz" => SubgroupAny(Load(_boolType, _exec)), + _ => 0, + }; + return condition != 0; + } + + private bool TryEmitInstruction( + Gen5ShaderInstruction instruction, + out string error) + { + error = string.Empty; + if (instruction.Opcode is + "SNop" or + "SWaitcnt" or + "SInstPrefetch" or + "STtraceData" or + "VInterpMovF32") + { + return true; + } + + if (instruction.Opcode == "SBarrier") + { + var workgroup = UInt(2); + var semantics = UInt(0x108); + _module.AddStatement( + SpirvOp.ControlBarrier, + workgroup, + workgroup, + semantics); + return true; + } + + if (instruction.Control is Gen5ScalarMemoryControl scalarMemory) + { + return TryEmitScalarMemory(instruction, scalarMemory, out error); + } + + if (instruction.Control is Gen5InterpolationControl interpolation) + { + return TryEmitInterpolation(instruction, interpolation, out error); + } + + if (instruction.Control is Gen5ImageControl image) + { + return TryEmitImage(instruction, image, out error); + } + + if (instruction.Control is Gen5GlobalMemoryControl globalMemory) + { + return TryEmitGlobalMemory(instruction, globalMemory, out error); + } + + if (instruction.Control is Gen5BufferMemoryControl bufferMemory) + { + return TryEmitBufferMemory(instruction, bufferMemory, out error); + } + + if (instruction.Control is Gen5ExportControl export) + { + return TryEmitExport(instruction, export, out error); + } + + if (instruction.Control is Gen5DataShareControl) + { + return TryEmitDataShare(instruction, out error); + } + + if (instruction.Encoding is + Gen5ShaderEncoding.Sop1 or + Gen5ShaderEncoding.Sop2 or + Gen5ShaderEncoding.Sopc or + Gen5ShaderEncoding.Sopk) + { + return TryEmitScalarAlu(instruction, out error); + } + + if (instruction.Encoding is + Gen5ShaderEncoding.Sopp or + Gen5ShaderEncoding.Smrd or + Gen5ShaderEncoding.Smem) + { + return true; + } + + return TryEmitVectorAlu(instruction, out error); + } + + private bool TryEmitDataShare( + Gen5ShaderInstruction instruction, + out string error) + { + error = string.Empty; + if (_stage != Gen5SpirvStage.Compute || + _lds == 0 || + _workgroupUintPointer == 0 || + instruction.Control is not Gen5DataShareControl control) + { + error = "invalid LDS instruction"; + return false; + } + + if (control.Gds) + { + error = "GDS data share is not implemented"; + return false; + } + + switch (instruction.Opcode) + { + case "DsWriteB32": + { + if (instruction.Sources.Count < 2) + { + error = "missing LDS write source"; + return false; + } + + var address = GetRawSource(instruction, 0); + StoreLds( + LdsPointer(address, EffectiveDsOffsetBytes(control.Offset0)), + GetRawSource(instruction, 1)); + return true; + } + case "DsWriteB64": + { + if (instruction.Sources.Count < 3) + { + error = "missing LDS write64 source"; + return false; + } + + var address = GetRawSource(instruction, 0); + var offset = EffectiveDsOffsetBytes(control.Offset0); + StoreLds(LdsPointer(address, offset), GetRawSource(instruction, 1)); + StoreLds( + LdsPointer(address, offset + sizeof(uint)), + GetRawSource(instruction, 2)); + return true; + } + case "DsWrite2B32": + case "DsWrite2St64B32": + { + if (instruction.Sources.Count < 3) + { + error = "missing LDS write2 source"; + return false; + } + + var st64 = instruction.Opcode == "DsWrite2St64B32"; + var address = GetRawSource(instruction, 0); + StoreLds( + LdsPointer( + address, + EffectiveDsOffsetBytes(control.Offset0, st64)), + GetRawSource(instruction, 1)); + StoreLds( + LdsPointer( + address, + EffectiveDsOffsetBytes(control.Offset1, st64)), + GetRawSource(instruction, 2)); + return true; + } + case "DsReadB32": + { + if (instruction.Destinations.Count < 1 || + instruction.Sources.Count < 1) + { + error = "missing LDS read operand"; + return false; + } + + var address = GetRawSource(instruction, 0); + var value = Load( + _uintType, + LdsPointer(address, EffectiveDsOffsetBytes(control.Offset0))); + StoreV(instruction.Destinations[0].Value, value); + return true; + } + case "DsRead2B32": + case "DsRead2St64B32": + { + if (instruction.Destinations.Count < 2 || + instruction.Sources.Count < 1) + { + error = "missing LDS read2 operand"; + return false; + } + + var st64 = instruction.Opcode == "DsRead2St64B32"; + var address = GetRawSource(instruction, 0); + var first = Load( + _uintType, + LdsPointer( + address, + EffectiveDsOffsetBytes(control.Offset0, st64))); + var second = Load( + _uintType, + LdsPointer( + address, + EffectiveDsOffsetBytes(control.Offset1, st64))); + StoreV(instruction.Destinations[0].Value, first); + StoreV(instruction.Destinations[1].Value, second); + return true; + } + default: + error = $"unsupported LDS opcode {instruction.Opcode}"; + return false; + } + } + + private static uint EffectiveDsOffsetBytes(uint offset, bool st64 = false) => + offset * (st64 ? 256u : sizeof(uint)); + + private uint LdsPointer(uint address, uint offsetBytes) + { + var addressWithOffset = offsetBytes == 0 + ? address + : IAdd(address, UInt(offsetBytes)); + var index = ShiftRightLogical(addressWithOffset, UInt(2)); + return _module.AddInstruction( + SpirvOp.AccessChain, + _workgroupUintPointer, + _lds, + index); + } + + private void StoreLds(uint pointer, uint value) + { + var active = Load(_boolType, _exec); + var oldValue = Load(_uintType, pointer); + var selected = _module.AddInstruction( + SpirvOp.Select, + _uintType, + active, + value, + oldValue); + Store(pointer, selected); + } + + private bool TryEmitInterpolation( + Gen5ShaderInstruction instruction, + Gen5InterpolationControl interpolation, + out string error) + { + error = string.Empty; + if (_stage != Gen5SpirvStage.Pixel || + !_pixelInputs.TryGetValue(interpolation.Attribute, out var input) || + !TryGetVectorDestination(instruction, out var destination)) + { + error = "invalid interpolated attribute"; + return false; + } + + var vector = Load(_vec4Type, input); + var component = _module.AddInstruction( + SpirvOp.CompositeExtract, + _floatType, + vector, + interpolation.Channel); + StoreV(destination, Bitcast(_uintType, component)); + return true; + } + + private bool TryEmitScalarMemory( + Gen5ShaderInstruction instruction, + Gen5ScalarMemoryControl control, + out string error) + { + error = string.Empty; + if (!_bufferBindingByPc.TryGetValue(instruction.Pc, out var bindingIndex)) + { + foreach (var destination in instruction.Destinations) + { + if (destination.Kind == Gen5OperandKind.ScalarRegister) + { + StoreS(destination.Value, UInt(0)); + } + } + + return true; + } + + var dynamicOffset = control.DynamicOffsetRegister is { } register + ? LoadS(register) + : UInt(0); + var byteAddress = IAdd( + dynamicOffset, + UInt(unchecked((uint)control.ImmediateOffsetBytes))); + var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); + for (var index = 0; index < instruction.Destinations.Count; index++) + { + var destination = instruction.Destinations[index]; + if (destination.Kind != Gen5OperandKind.ScalarRegister) + { + error = "invalid scalar-memory destination"; + return false; + } + + var address = index == 0 + ? dwordAddress + : IAdd(dwordAddress, UInt((uint)index)); + StoreS(destination.Value, LoadBufferWord(bindingIndex, address)); + } + + return true; + } + + private bool TryEmitGlobalMemory( + Gen5ShaderInstruction instruction, + Gen5GlobalMemoryControl control, + out string error) + { + error = string.Empty; + if (!_bufferBindingByPc.TryGetValue(instruction.Pc, out var bindingIndex)) + { + error = "missing global-memory binding"; + return false; + } + + var byteAddress = IAdd( + LoadV(control.VectorAddress), + UInt(unchecked((uint)control.OffsetBytes))); + var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); + for (uint index = 0; index < control.DwordCount; index++) + { + var address = index == 0 + ? dwordAddress + : IAdd(dwordAddress, UInt(index)); + StoreV( + control.VectorData + index, + LoadBufferWord(bindingIndex, address)); + } + + return true; + } + + private bool TryEmitBufferMemory( + Gen5ShaderInstruction instruction, + Gen5BufferMemoryControl control, + out string error) + { + error = string.Empty; + if (_stage == Gen5SpirvStage.Vertex && + _vertexInputsByPc.TryGetValue(instruction.Pc, out var vertexInput)) + { + return TryEmitVertexInputFetch(control, vertexInput, out error); + } + + if (_stage == Gen5SpirvStage.Vertex && + IsFormatBufferLoad(instruction.Opcode)) + { + error = $"missing vertex input for {instruction.Opcode} pc=0x{instruction.Pc:X}"; + return false; + } + + if (!_bufferBindingByPc.TryGetValue(instruction.Pc, out var bindingIndex)) + { + error = "missing buffer-memory binding"; + return false; + } + + var scalarOffset = instruction.Sources.Count > 2 + ? GetRawSource(instruction, 2) + : UInt(0); + var stride = ShiftRightLogical(LoadS(control.ScalarResource + 1), UInt(16)); + stride = BitwiseAnd(stride, UInt(0x3FFF)); + var vectorIndex = control.IndexEnabled + ? LoadV(control.VectorAddress) + : UInt(0); + var vectorOffset = control.OffsetEnabled + ? LoadV(control.VectorAddress + (control.IndexEnabled ? 1u : 0u)) + : UInt(0); + var byteAddress = IAdd( + UInt(unchecked((uint)control.OffsetBytes)), + scalarOffset); + byteAddress = IAdd(byteAddress, vectorOffset); + byteAddress = IAdd( + byteAddress, + _module.AddInstruction(SpirvOp.IMul, _uintType, vectorIndex, stride)); + var dwordAddress = ShiftRightLogical(byteAddress, UInt(2)); + + if (instruction.Opcode == "BufferAtomicAdd") + { + EmitExecConditional(() => + { + var original = _module.AddInstruction( + SpirvOp.AtomicIAdd, + _uintType, + BufferWordPointer(bindingIndex, dwordAddress), + UInt(1), + UInt(0x48), + LoadV(control.VectorData)); + if (control.Glc) + { + StoreV(control.VectorData, original); + } + }); + + return true; + } + + if (instruction.Opcode.StartsWith("BufferStoreDword", StringComparison.Ordinal)) + { + EmitExecConditional(() => + { + for (uint index = 0; index < control.DwordCount; index++) + { + var address = index == 0 + ? dwordAddress + : IAdd(dwordAddress, UInt(index)); + StoreBufferWord( + bindingIndex, + address, + LoadV(control.VectorData + index)); + } + }); + + return true; + } + + if (!instruction.Opcode.StartsWith("BufferLoad", StringComparison.Ordinal) && + !instruction.Opcode.StartsWith("TBufferLoad", StringComparison.Ordinal)) + { + error = $"unsupported buffer opcode {instruction.Opcode}"; + return false; + } + + for (uint index = 0; index < control.DwordCount; index++) + { + var address = index == 0 + ? dwordAddress + : IAdd(dwordAddress, UInt(index)); + StoreV( + control.VectorData + index, + LoadBufferWord(bindingIndex, address)); + } + + return true; + } + + private static bool IsFormatBufferLoad(string opcode) => + opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal) || + opcode.StartsWith("TBufferLoadFormat", StringComparison.Ordinal); + + private bool TryEmitVertexInputFetch( + Gen5BufferMemoryControl control, + SpirvVertexInput input, + out string error) + { + error = string.Empty; + if (control.DwordCount == 0 || + control.DwordCount > input.ComponentCount) + { + error = + $"invalid vertex input fetch components={control.DwordCount} " + + $"input={input.ComponentCount}"; + return false; + } + + var loaded = Load(input.Type, input.Variable); + for (uint component = 0; component < control.DwordCount; component++) + { + var value = input.ComponentCount == 1 + ? loaded + : _module.AddInstruction( + SpirvOp.CompositeExtract, + _floatType, + loaded, + component); + StoreV(control.VectorData + component, Bitcast(_uintType, value)); + } + + return true; + } + + private bool TryEmitImage( + Gen5ShaderInstruction instruction, + Gen5ImageControl image, + out string error) + { + error = string.Empty; + if (!_imageBindingByPc.TryGetValue(instruction.Pc, out var bindingIndex) || + bindingIndex >= _imageResources.Count) + { + error = "unresolved image binding"; + return false; + } + + var resource = _imageResources[bindingIndex]; + var imageObject = Load(resource.ObjectType, resource.Variable); + if (instruction.Opcode == "ImageGetResinfo") + { + var queryImage = resource.IsStorage + ? imageObject + : _module.AddInstruction( + SpirvOp.Image, + resource.ImageType, + imageObject); + var size = _module.AddInstruction( + resource.IsStorage + ? SpirvOp.ImageQuerySize + : SpirvOp.ImageQuerySizeLod, + _module.TypeVector(_intType, 2), + resource.IsStorage + ? [queryImage] + : [queryImage, UInt(0)]); + uint outputIndex = 0; + for (uint component = 0; component < 4; component++) + { + if ((image.Dmask & (1u << (int)component)) == 0) + { + continue; + } + + uint value; + if (component < 2) + { + var signedValue = _module.AddInstruction( + SpirvOp.CompositeExtract, + _intType, + size, + component); + value = Bitcast(_uintType, signedValue); + } + else + { + value = UInt(1); + } + + StoreV(image.VectorData + outputIndex++, value); + } + + return true; + } + + if (instruction.Opcode is "ImageStore" or "ImageStoreMip") + { + if (!resource.IsStorage) + { + error = "image store is not bound as storage"; + return false; + } + + var coordinates = BuildIntegerCoordinates(image, 0); + var components = new uint[4]; + uint sourceIndex = 0; + for (var component = 0; component < components.Length; component++) + { + if ((image.Dmask & (1u << component)) != 0) + { + var raw = LoadV(image.VectorData + 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); + if (TryGetImageBounds( + _evaluation.ImageBindings[bindingIndex].ResourceDescriptor, + out var width, + out var height)) + { + EmitBoundsCheckedImageWrite( + coordinates, + width, + height, + imageObject, + texel); + } + else + { + EmitExecConditional( + () => _module.AddStatement( + SpirvOp.ImageWrite, + imageObject, + coordinates, + texel)); + } + + return true; + } + + if (resource.IsStorage) + { + error = $"unsupported storage image opcode {instruction.Opcode}"; + return false; + } + + uint sampled; + var writeAllComponents = false; + if (instruction.Opcode is "ImageLoad" or "ImageLoadMip") + { + var coordinates = TryGetImageBounds( + _evaluation.ImageBindings[bindingIndex].ResourceDescriptor, + out var width, + out var height) + ? BuildClampedIntegerCoordinates(image, 0, width, height) + : BuildIntegerCoordinates(image, 0); + var mipLevel = _evaluation.ImageBindings[bindingIndex].MipLevel ?? 0; + var fetchedImage = _module.AddInstruction( + SpirvOp.Image, + resource.ImageType, + imageObject); + sampled = _module.AddInstruction( + SpirvOp.ImageFetch, + resource.VectorType, + fetchedImage, + coordinates, + 2, + UInt(mipLevel)); + } + else if (instruction.Opcode.StartsWith( + "ImageSample", + StringComparison.Ordinal)) + { + var hasOffset = + instruction.Opcode.EndsWith("O", StringComparison.Ordinal); + var hasCompare = + instruction.Opcode.Contains("SampleC", StringComparison.Ordinal); + var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0); + var coordinates = BuildFloatCoordinates(image, start); + var explicitLod = + instruction.Opcode.Contains("Lz", StringComparison.Ordinal) || + instruction.Opcode.Contains("SampleL", StringComparison.Ordinal); + var lod = instruction.Opcode.Contains("Lz", StringComparison.Ordinal) + ? Float(0) + : Bitcast( + _floatType, + LoadV(image.GetAddressRegister(start + 2))); + var offset = hasOffset ? BuildImageOffset(image, 0) : 0u; + var imageOperands = + (explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u); + var operands = new List + { + imageObject, + coordinates, + }; + if (hasCompare) + { + operands.Add( + Bitcast( + _floatType, + LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))); + } + + if (imageOperands != 0) + { + operands.Add(imageOperands); + if (explicitLod) + { + operands.Add(lod); + } + + if (hasOffset) + { + operands.Add(offset); + } + } + + sampled = _module.AddInstruction( + hasCompare + ? explicitLod + ? SpirvOp.ImageSampleDrefExplicitLod + : SpirvOp.ImageSampleDrefImplicitLod + : explicitLod + ? SpirvOp.ImageSampleExplicitLod + : SpirvOp.ImageSampleImplicitLod, + hasCompare ? resource.ComponentType : resource.VectorType, + [.. operands]); + if (hasCompare) + { + var scalar = sampled; + sampled = _module.AddInstruction( + SpirvOp.CompositeConstruct, + resource.VectorType, + scalar, + scalar, + scalar, + resource.ComponentKind == ImageComponentKind.Float + ? Float(1) + : resource.ComponentKind == ImageComponentKind.Uint + ? UInt(1) + : _module.Constant(_intType, 1)); + } + } + 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 start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0); + var coordinates = BuildFloatCoordinates(image, start); + var offset = hasOffset ? BuildImageOffset(image, 0) : 0u; + var operands = new List + { + imageObject, + coordinates, + }; + if (hasCompare) + { + operands.Add( + Bitcast( + _floatType, + LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))); + } + else + { + uint component = 0; + while (component < 3 && + (image.Dmask & (1u << (int)component)) == 0) + { + component++; + } + + operands.Add(UInt(component)); + } + + if (hasOffset) + { + operands.Add(0x10u); + operands.Add(offset); + } + + sampled = _module.AddInstruction( + hasCompare + ? SpirvOp.ImageDrefGather + : SpirvOp.ImageGather, + resource.VectorType, + [.. operands]); + writeAllComponents = true; + } + else + { + error = $"unsupported image opcode {instruction.Opcode}"; + return false; + } + + uint output = 0; + 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), + }; + StoreV(image.VectorData + output++, raw); + } + + return true; + } + + private uint BuildFloatCoordinates(Gen5ImageControl image, int start) + { + var x = Bitcast( + _floatType, + LoadV(image.GetAddressRegister(start))); + var y = Bitcast( + _floatType, + LoadV(image.GetAddressRegister(start + 1))); + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + _vec2Type, + x, + y); + } + + private uint BuildIntegerCoordinates(Gen5ImageControl image, int start) + { + var ivec2 = _module.TypeVector(_intType, 2); + var x = Bitcast( + _intType, + LoadV(image.GetAddressRegister(start))); + var y = Bitcast( + _intType, + LoadV(image.GetAddressRegister(start + 1))); + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + ivec2, + x, + y); + } + + private uint BuildClampedIntegerCoordinates( + Gen5ImageControl image, + int start, + uint width, + uint height) + { + var ivec2 = _module.TypeVector(_intType, 2); + var x = ClampSignedCoordinate( + Bitcast( + _intType, + LoadV(image.GetAddressRegister(start))), + width); + var y = ClampSignedCoordinate( + Bitcast( + _intType, + LoadV(image.GetAddressRegister(start + 1))), + height); + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + ivec2, + x, + y); + } + + private uint ClampSignedCoordinate(uint value, uint extent) + { + var zero = _module.Constant(_intType, 0); + var max = _module.Constant(_intType, Math.Max(extent, 1) - 1); + var belowZero = _module.AddInstruction( + SpirvOp.SLessThan, + _boolType, + value, + zero); + var atLeastZero = _module.AddInstruction( + SpirvOp.Select, + _intType, + belowZero, + zero, + value); + var aboveMax = _module.AddInstruction( + SpirvOp.SGreaterThan, + _boolType, + atLeastZero, + max); + return _module.AddInstruction( + SpirvOp.Select, + _intType, + aboveMax, + max, + atLeastZero); + } + + private void EmitBoundsCheckedImageWrite( + uint coordinates, + uint width, + uint height, + uint imageObject, + uint texel) + { + var x = _module.AddInstruction( + SpirvOp.CompositeExtract, + _intType, + coordinates, + 0); + var y = _module.AddInstruction( + SpirvOp.CompositeExtract, + _intType, + coordinates, + 1); + var zero = _module.Constant(_intType, 0); + var xNonNegative = _module.AddInstruction( + SpirvOp.SGreaterThanEqual, + _boolType, + x, + zero); + var yNonNegative = _module.AddInstruction( + SpirvOp.SGreaterThanEqual, + _boolType, + y, + zero); + var xInRange = _module.AddInstruction( + SpirvOp.SLessThan, + _boolType, + x, + _module.Constant(_intType, width)); + var yInRange = _module.AddInstruction( + SpirvOp.SLessThan, + _boolType, + y, + _module.Constant(_intType, height)); + var lowerInRange = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + xNonNegative, + yNonNegative); + var upperInRange = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + xInRange, + yInRange); + var inRange = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + lowerInRange, + upperInRange); + inRange = _module.AddInstruction( + SpirvOp.LogicalAnd, + _boolType, + Load(_boolType, _exec), + inRange); + var writeLabel = _module.AllocateId(); + var mergeLabel = _module.AllocateId(); + _module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0); + _module.AddStatement( + SpirvOp.BranchConditional, + inRange, + writeLabel, + mergeLabel); + _module.AddLabel(writeLabel); + _module.AddStatement( + SpirvOp.ImageWrite, + imageObject, + coordinates, + texel); + _module.AddStatement(SpirvOp.Branch, mergeLabel); + _module.AddLabel(mergeLabel); + } + + private static bool TryGetImageBounds( + IReadOnlyList descriptor, + out uint width, + out uint height) + { + width = 0; + height = 0; + if (descriptor.Count < 3) + { + return false; + } + + width = (((descriptor[1] >> 30) & 0x3u) | + ((descriptor[2] & 0xFFFu) << 2)) + 1; + height = ((descriptor[2] >> 14) & 0x3FFFu) + 1; + return width != 0 && height != 0 && width <= 16384 && height <= 16384; + } + + private uint BuildImageOffset(Gen5ImageControl image, int component) + { + var ivec2 = _module.TypeVector(_intType, 2); + var packed = Bitcast( + _intType, + LoadV(image.GetAddressRegister(component))); + var x = _module.AddInstruction( + SpirvOp.BitFieldSExtract, + _intType, + packed, + UInt(0), + UInt(6)); + var y = _module.AddInstruction( + SpirvOp.BitFieldSExtract, + _intType, + packed, + UInt(8), + UInt(6)); + return _module.AddInstruction( + SpirvOp.CompositeConstruct, + ivec2, + x, + y); + } + + private bool TryEmitExport( + Gen5ShaderInstruction instruction, + Gen5ExportControl export, + out string error) + { + error = string.Empty; + if (instruction.Sources.Count < 4) + { + error = "missing export sources"; + return false; + } + + if (_stage == Gen5SpirvStage.Pixel) + { + if (export.Target != 0) + { + return true; + } + + var outputType = GetPixelOutputType(); + var values = new uint[4]; + for (var component = 0; component < 4; component++) + { + var enabled = (export.EnableMask & (1u << component)) != 0; + if (!enabled) + { + values[component] = _outputKind == Gen5PixelOutputKind.Sint + ? Bitcast(_intType, UInt(0)) + : UInt(0); + continue; + } + + if (export.Compressed) + { + var value = LoadCompressedExportComponent( + instruction, + component); + values[component] = _outputKind 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] = _outputKind switch + { + Gen5PixelOutputKind.Uint => raw, + Gen5PixelOutputKind.Sint => Bitcast(_intType, raw), + _ => Bitcast(_floatType, raw), + }; + } + + var vector = _module.AddInstruction( + SpirvOp.CompositeConstruct, + outputType, + values); + vector = _module.AddInstruction( + SpirvOp.Select, + outputType, + Load(_boolType, _exec), + vector, + Load(outputType, _pixelOutput)); + Store(_pixelOutput, vector); + return true; + } + + if (_stage != Gen5SpirvStage.Vertex) + { + return true; + } + + uint outputVariable; + if (export.Target is >= 12 and < 16) + { + if (export.Target != 12) + { + return true; + } + + outputVariable = _positionOutput; + } + else if (export.Target is >= 32 and < 64 && + _vertexOutputs.TryGetValue(export.Target - 32, out var parameter)) + { + outputVariable = parameter; + } + else + { + return true; + } + + var components = new uint[4]; + for (var component = 0; component < 4; component++) + { + components[component] = (export.EnableMask & (1u << component)) != 0 + ? export.Compressed + ? LoadCompressedExportComponent(instruction, component) + : Bitcast( + _floatType, + LoadV(instruction.Sources[component].Value)) + : Float(component == 3 ? 1f : 0f); + } + + var outputValue = _module.AddInstruction( + SpirvOp.CompositeConstruct, + _vec4Type, + components); + outputValue = _module.AddInstruction( + SpirvOp.Select, + _vec4Type, + Load(_boolType, _exec), + outputValue, + Load(_vec4Type, outputVariable)); + Store(outputVariable, outputValue); + return true; + } + + private uint LoadCompressedExportComponent( + Gen5ShaderInstruction instruction, + int component) + { + 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 uint GetPixelOutputType() => + _outputKind switch + { + Gen5PixelOutputKind.Uint => _uvec4Type, + Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4), + _ => _vec4Type, + }; + + private uint LoadBufferWord(int binding, uint dwordAddress) + { + var pointer = BufferWordPointer(binding, dwordAddress); + return Load(_uintType, pointer); + } + + private void StoreBufferWord(int binding, uint dwordAddress, uint value) + { + var pointer = BufferWordPointer(binding, dwordAddress); + Store(pointer, value); + } + + private uint BufferWordPointer(int binding, uint dwordAddress) => + _module.AddInstruction( + SpirvOp.AccessChain, + _storageUintPointer, + _globalBuffers, + UInt((uint)binding), + UInt(0), + dwordAddress); + + private uint ScalarPointer(uint register) => + _module.AddInstruction( + SpirvOp.AccessChain, + _privateUintPointer, + _scalarRegisters, + UInt(register)); + + private uint VectorPointer(uint register) => + _module.AddInstruction( + SpirvOp.AccessChain, + _privateUintPointer, + _vectorRegisters, + UInt(register)); + + private uint LoadS(uint register) => Load(_uintType, ScalarPointer(register)); + + private uint LoadV(uint register) => Load(_uintType, VectorPointer(register)); + + private void StoreS(uint register, uint value) + { + Store(ScalarPointer(register), value); + if (register is 106 or 107) + { + Store(_vcc, IsWaveMaskActive(LoadS64(106))); + } + else if (register is 126 or 127) + { + Store(_exec, IsWaveMaskActive(LoadS64(126))); + } + } + + private void StoreV(uint register, uint value, bool guardWithExec = true) + { + if (guardWithExec) + { + var active = Load(_boolType, _exec); + var oldValue = LoadV(register); + value = _module.AddInstruction( + SpirvOp.Select, + _uintType, + active, + value, + oldValue); + } + + Store(VectorPointer(register), value); + } + + private uint Load(uint type, uint pointer) => + _module.AddInstruction(SpirvOp.Load, type, pointer); + + private void Store(uint pointer, uint value) => + _module.AddStatement(SpirvOp.Store, pointer, value); + + private uint UInt(uint value) => _module.Constant(_uintType, value); + + private uint Float(float value) => _module.ConstantFloat(_floatType, value); + + private uint Bitcast(uint type, uint value) => + _module.AddInstruction(SpirvOp.Bitcast, type, value); + + private uint IAdd(uint left, uint right) => + _module.AddInstruction(SpirvOp.IAdd, _uintType, left, right); + + private uint ShiftLeftLogical(uint left, uint right) => + _module.AddInstruction( + SpirvOp.ShiftLeftLogical, + _uintType, + left, + BitwiseAnd(right, UInt(31))); + + private uint ShiftRightLogical(uint left, uint right) => + _module.AddInstruction( + SpirvOp.ShiftRightLogical, + _uintType, + left, + BitwiseAnd(right, UInt(31))); + + private uint ShiftRightArithmetic(uint left, uint right) => + Bitcast( + _uintType, + _module.AddInstruction( + SpirvOp.ShiftRightArithmetic, + _intType, + Bitcast(_intType, left), + BitwiseAnd(right, UInt(31)))); + + private uint ShiftLeftLogical64(uint left, uint right) => + _module.AddInstruction( + SpirvOp.ShiftLeftLogical, + _ulongType, + left, + BitwiseAnd64(right, _module.Constant64(_ulongType, 63))); + + private uint ShiftRightLogical64(uint left, uint right) => + _module.AddInstruction( + SpirvOp.ShiftRightLogical, + _ulongType, + left, + BitwiseAnd64(right, _module.Constant64(_ulongType, 63))); + + private uint BitwiseAnd(uint left, uint right) => + _module.AddInstruction(SpirvOp.BitwiseAnd, _uintType, left, right); + + private uint BitwiseAnd64(uint left, uint right) => + _module.AddInstruction(SpirvOp.BitwiseAnd, _ulongType, left, right); + + private uint 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 + : _module.AddInstruction( + SpirvOp.GroupNonUniformAny, + _boolType, + UInt(3), + condition); + + private uint CurrentLaneBit() + { + if (_subgroupInvocationIdInput == 0) + { + return _module.Constant64(_ulongType, 1); + } + + var lane = Load(_uintType, _subgroupInvocationIdInput); + var maskedLane = BitwiseAnd(lane, UInt(RdnaWaveLaneCount - 1)); + var shifted = ShiftLeftLogical64( + _module.Constant64(_ulongType, 1), + _module.AddInstruction( + SpirvOp.UConvert, + _ulongType, + maskedLane)); + return _module.AddInstruction( + SpirvOp.Select, + _ulongType, + IsCurrentLaneInRdnaWave(), + shifted, + _module.Constant64(_ulongType, 0)); + } + + private uint IsCurrentLaneInRdnaWave() => + _module.AddInstruction( + SpirvOp.ULessThan, + _boolType, + Load(_uintType, _subgroupInvocationIdInput), + UInt(RdnaWaveLaneCount)); + + private uint BooleanToLaneMask(uint condition) => + _module.AddInstruction( + SpirvOp.Select, + _ulongType, + condition, + CurrentLaneBit(), + _module.Constant64(_ulongType, 0)); + + private uint IsWaveMaskActive(uint mask) => + _subgroupInvocationIdInput == 0 + ? IsNotZero64(mask) + : IsCurrentLaneSet(mask); + + private uint IsCurrentLaneSet(uint mask) => + IsNotZero64( + _module.AddInstruction( + SpirvOp.BitwiseAnd, + _ulongType, + mask, + CurrentLaneBit())); + + private void StoreWaveMask(uint register, uint condition) => + StoreS64(register, BooleanToLaneMask(condition)); + + private void EmitExecConditional(Action emit) + { + var activeLabel = _module.AllocateId(); + var mergeLabel = _module.AllocateId(); + var active = Load(_boolType, _exec); + _module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0); + _module.AddStatement( + SpirvOp.BranchConditional, + active, + activeLabel, + mergeLabel); + _module.AddLabel(activeLabel); + emit(); + _module.AddStatement(SpirvOp.Branch, mergeLabel); + _module.AddLabel(mergeLabel); + } + + private bool UsesLds() => + _state.Program.Instructions.Any(instruction => + instruction.Control is Gen5DataShareControl); + + private bool UsesSubgroupShuffle() => + _state.Program.Instructions.Any(instruction => + instruction.Opcode is "VPermlane16B32" or "VPermlanex16B32"); + + private bool UsesWaveControl() => + _state.Program.Instructions.Any(instruction => + instruction.Opcode.Contains("Saveexec", StringComparison.Ordinal) || + instruction.Opcode.StartsWith("SCbranchExec", StringComparison.Ordinal) || + instruction.Opcode.StartsWith("SCbranchVcc", StringComparison.Ordinal) || + instruction.Opcode.StartsWith("VCmpx", StringComparison.Ordinal) || + instruction.Sources.Any(IsWaveMaskOperand) || + instruction.Destinations.Any(IsWaveMaskOperand)); + + private bool UsesSubgroupOperations() => + _stage == Gen5SpirvStage.Compute && + (UsesSubgroupShuffle() || UsesWaveControl()); + + private static bool IsWaveMaskOperand(Gen5Operand operand) => + operand.Kind == Gen5OperandKind.ScalarRegister && + operand.Value is 106 or 107 or 126 or 127; + + private static bool TryGetVectorDestination( + Gen5ShaderInstruction instruction, + out uint destination) + { + if (instruction.Destinations.Count != 0 && + instruction.Destinations[0].Kind == Gen5OperandKind.VectorRegister) + { + destination = instruction.Destinations[0].Value; + return true; + } + + destination = 0; + return false; + } + + private static bool IsBranch(string opcode) => + opcode == "SBranch" || + opcode.StartsWith("SCbranch", StringComparison.Ordinal); + + private static bool TryGetBranchTargetPc( + Gen5ShaderInstruction instruction, + out uint targetPc) + { + targetPc = 0; + if (instruction.Encoding != Gen5ShaderEncoding.Sopp || + instruction.Words.Count == 0) + { + return false; + } + + var offset = unchecked((short)(instruction.Words[0] & 0xFFFF)); + var nextPc = (long)instruction.Pc + + (instruction.Words.Count * sizeof(uint)); + var target = nextPc + (offset * sizeof(uint)); + if (target < 0 || target > uint.MaxValue) + { + return false; + } + + targetPc = (uint)target; + return true; + } + + private static IReadOnlyList BuildBasicBlocks( + IReadOnlyList instructions) + { + if (instructions.Count == 0) + { + return []; + } + + var leaders = new SortedSet { instructions[0].Pc }; + for (var index = 0; index < instructions.Count; index++) + { + var instruction = instructions[index]; + if (IsBranch(instruction.Opcode) && + TryGetBranchTargetPc(instruction, out var targetPc)) + { + leaders.Add(targetPc); + } + + if ((IsBranch(instruction.Opcode) || instruction.Opcode == "SEndpgm") && + index + 1 < instructions.Count) + { + leaders.Add(instructions[index + 1].Pc); + } + } + + var starts = leaders + .Where(pc => instructions.Any(instruction => instruction.Pc == pc)) + .ToArray(); + var blocks = new List(starts.Length); + for (var index = 0; index < starts.Length; index++) + { + var startIndex = FindInstructionIndex(instructions, starts[index]); + var endIndex = index + 1 < starts.Length + ? FindInstructionIndex(instructions, starts[index + 1]) + : instructions.Count; + if (startIndex >= 0 && endIndex > startIndex) + { + blocks.Add(new ShaderBlock(starts[index], startIndex, endIndex)); + } + } + + return blocks; + } + + private static int FindInstructionIndex( + IReadOnlyList instructions, + uint pc) + { + for (var index = 0; index < instructions.Count; index++) + { + if (instructions[index].Pc == pc) + { + return index; + } + } + + return -1; + } + + private static bool TryFindBlock( + IReadOnlyList blocks, + uint pc, + out int block) + { + for (var index = 0; index < blocks.Count; index++) + { + if (blocks[index].StartPc == pc) + { + block = index; + return true; + } + } + + block = -1; + return false; + } + + private readonly record struct ShaderBlock( + uint StartPc, + int StartIndex, + int EndIndex); + } +} diff --git a/src/SharpEmu.Libs/Agc/SpirvFixedShaders.cs b/src/SharpEmu.Libs/Agc/SpirvFixedShaders.cs new file mode 100644 index 0000000..a2cae91 --- /dev/null +++ b/src/SharpEmu.Libs/Agc/SpirvFixedShaders.cs @@ -0,0 +1,167 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +namespace SharpEmu.Libs.Agc; + +internal static class SpirvFixedShaders +{ + public static byte[] CreateFullscreenVertex(uint attributeCount) + { + var module = new SpirvModuleBuilder(); + module.AddCapability(SpirvCapability.Shader); + + var voidType = module.TypeVoid(); + var boolType = module.TypeBool(); + var uintType = module.TypeInt(32, signed: false); + var floatType = module.TypeFloat(32); + var vec4Type = module.TypeVector(floatType, 4); + var inputUintPointer = module.TypePointer(SpirvStorageClass.Input, uintType); + var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type); + + var vertexIndex = module.AddGlobalVariable(inputUintPointer, SpirvStorageClass.Input); + module.AddName(vertexIndex, "vertexIndex"); + module.AddDecoration( + vertexIndex, + SpirvDecoration.BuiltIn, + (uint)SpirvBuiltIn.VertexIndex); + + var position = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output); + module.AddName(position, "position"); + module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position); + + var attributes = new uint[attributeCount]; + for (uint index = 0; index < attributeCount; index++) + { + attributes[index] = + module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output); + module.AddName(attributes[index], $"attr{index}"); + module.AddDecoration(attributes[index], SpirvDecoration.Location, index); + module.AddDecoration(attributes[index], SpirvDecoration.NoPerspective); + } + + var functionType = module.TypeFunction(voidType); + var main = module.BeginFunction(voidType, functionType); + module.AddName(main, "main"); + module.AddLabel(); + + var indexValue = module.AddInstruction(SpirvOp.Load, uintType, vertexIndex); + var one = module.Constant(uintType, 1); + var two = module.Constant(uintType, 2); + var shifted = module.AddInstruction(SpirvOp.ShiftLeftLogical, uintType, indexValue, one); + var xBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, shifted, two); + var yBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, indexValue, two); + var x = module.AddInstruction(SpirvOp.ConvertUToF, floatType, xBits); + var y = module.AddInstruction(SpirvOp.ConvertUToF, floatType, yBits); + var zero = module.ConstantFloat(floatType, 0f); + var oneFloat = module.ConstantFloat(floatType, 1f); + var twoFloat = module.ConstantFloat(floatType, 2f); + var xPosition = module.AddInstruction(SpirvOp.FMul, floatType, x, twoFloat); + xPosition = module.AddInstruction(SpirvOp.FSub, floatType, xPosition, oneFloat); + var yPosition = module.AddInstruction(SpirvOp.FMul, floatType, y, twoFloat); + yPosition = module.AddInstruction(SpirvOp.FSub, floatType, yPosition, oneFloat); + var positionValue = module.AddInstruction( + SpirvOp.CompositeConstruct, + vec4Type, + xPosition, + yPosition, + zero, + oneFloat); + module.AddStatement(SpirvOp.Store, position, positionValue); + + var attributeValue = module.AddInstruction( + SpirvOp.CompositeConstruct, + vec4Type, + x, + y, + zero, + oneFloat); + foreach (var attribute in attributes) + { + module.AddStatement(SpirvOp.Store, attribute, attributeValue); + } + + module.AddStatement(SpirvOp.Return); + module.EndFunction(); + + var interfaces = new uint[2 + attributes.Length]; + interfaces[0] = vertexIndex; + interfaces[1] = position; + attributes.CopyTo(interfaces, 2); + module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", interfaces); + _ = boolType; + return module.Build(); + } + + public static byte[] CreateCopyFragment() + { + var module = new SpirvModuleBuilder(); + module.AddCapability(SpirvCapability.Shader); + + var voidType = module.TypeVoid(); + var floatType = module.TypeFloat(32); + var vec2Type = module.TypeVector(floatType, 2); + var vec4Type = module.TypeVector(floatType, 4); + var inputVec4Pointer = module.TypePointer(SpirvStorageClass.Input, vec4Type); + var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type); + var imageType = module.TypeImage( + floatType, + SpirvImageDim.Dim2D, + depth: false, + arrayed: false, + multisampled: false, + sampled: 1, + SpirvImageFormat.Unknown); + var sampledImageType = module.TypeSampledImage(imageType); + var sampledImagePointer = + module.TypePointer(SpirvStorageClass.UniformConstant, sampledImageType); + + var attribute = module.AddGlobalVariable(inputVec4Pointer, SpirvStorageClass.Input); + module.AddName(attribute, "attr0"); + module.AddDecoration(attribute, SpirvDecoration.Location, 0); + + var texture = module.AddGlobalVariable( + sampledImagePointer, + SpirvStorageClass.UniformConstant); + module.AddName(texture, "tex0"); + module.AddDecoration(texture, SpirvDecoration.DescriptorSet, 0); + module.AddDecoration(texture, SpirvDecoration.Binding, 1); + + var output = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output); + module.AddName(output, "outColor"); + module.AddDecoration(output, SpirvDecoration.Location, 0); + + var functionType = module.TypeFunction(voidType); + var main = module.BeginFunction(voidType, functionType); + module.AddName(main, "main"); + module.AddLabel(); + + var attributeValue = module.AddInstruction(SpirvOp.Load, vec4Type, attribute); + var coordinates = module.AddInstruction( + SpirvOp.VectorShuffle, + vec2Type, + attributeValue, + attributeValue, + 0, + 1); + var sampledImage = module.AddInstruction(SpirvOp.Load, sampledImageType, texture); + var lod = module.ConstantFloat(floatType, 0f); + var color = module.AddInstruction( + SpirvOp.ImageSampleExplicitLod, + vec4Type, + sampledImage, + coordinates, + 2, + lod); + module.AddStatement(SpirvOp.Store, output, color); + module.AddStatement(SpirvOp.Return); + module.EndFunction(); + + module.AddEntryPoint( + SpirvExecutionModel.Fragment, + main, + "main", + [attribute, texture, output]); + module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft); + return module.Build(); + } +} diff --git a/src/SharpEmu.Libs/Agc/SpirvModuleBuilder.cs b/src/SharpEmu.Libs/Agc/SpirvModuleBuilder.cs new file mode 100644 index 0000000..c6394a9 --- /dev/null +++ b/src/SharpEmu.Libs/Agc/SpirvModuleBuilder.cs @@ -0,0 +1,885 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using System.Text; + +namespace SharpEmu.Libs.Agc; + +internal enum SpirvOp : ushort +{ + Nop = 0, + Name = 5, + Extension = 10, + ExtInstImport = 11, + ExtInst = 12, + MemoryModel = 14, + EntryPoint = 15, + ExecutionMode = 16, + Capability = 17, + TypeVoid = 19, + TypeBool = 20, + TypeInt = 21, + TypeFloat = 22, + TypeVector = 23, + TypeImage = 25, + TypeSampler = 26, + TypeSampledImage = 27, + TypeArray = 28, + TypeRuntimeArray = 29, + TypeStruct = 30, + TypePointer = 32, + TypeFunction = 33, + ConstantTrue = 41, + ConstantFalse = 42, + Constant = 43, + ConstantComposite = 44, + ConstantNull = 46, + Function = 54, + FunctionParameter = 55, + FunctionEnd = 56, + FunctionCall = 57, + Variable = 59, + Load = 61, + Store = 62, + AccessChain = 65, + ArrayLength = 68, + Decorate = 71, + VectorExtractDynamic = 77, + VectorInsertDynamic = 78, + VectorShuffle = 79, + CompositeConstruct = 80, + CompositeExtract = 81, + CompositeInsert = 82, + CopyObject = 83, + SampledImage = 86, + ImageSampleImplicitLod = 87, + ImageSampleExplicitLod = 88, + ImageSampleDrefImplicitLod = 89, + ImageSampleDrefExplicitLod = 90, + ImageFetch = 95, + ImageGather = 96, + ImageDrefGather = 97, + ImageRead = 98, + ImageWrite = 99, + Image = 100, + ImageQuerySizeLod = 103, + ImageQuerySize = 104, + ImageQueryLod = 105, + ImageQueryLevels = 106, + ImageQuerySamples = 107, + ConvertFToU = 109, + ConvertFToS = 110, + ConvertSToF = 111, + ConvertUToF = 112, + UConvert = 113, + SConvert = 114, + FConvert = 115, + Bitcast = 124, + SNegate = 126, + FNegate = 127, + IAdd = 128, + FAdd = 129, + ISub = 130, + FSub = 131, + IMul = 132, + FMul = 133, + UDiv = 134, + SDiv = 135, + FDiv = 136, + UMod = 137, + SRem = 138, + SMod = 139, + FRem = 140, + FMod = 141, + IAddCarry = 149, + ISubBorrow = 150, + UMulExtended = 151, + SMulExtended = 152, + Any = 154, + All = 155, + IsNan = 156, + IsInf = 157, + LogicalEqual = 164, + LogicalNotEqual = 165, + LogicalOr = 166, + LogicalAnd = 167, + LogicalNot = 168, + Select = 169, + IEqual = 170, + INotEqual = 171, + UGreaterThan = 172, + SGreaterThan = 173, + UGreaterThanEqual = 174, + SGreaterThanEqual = 175, + ULessThan = 176, + SLessThan = 177, + ULessThanEqual = 178, + SLessThanEqual = 179, + FOrdEqual = 180, + FUnordEqual = 181, + FOrdNotEqual = 182, + FUnordNotEqual = 183, + FOrdLessThan = 184, + FUnordLessThan = 185, + FOrdGreaterThan = 186, + FUnordGreaterThan = 187, + FOrdLessThanEqual = 188, + FUnordLessThanEqual = 189, + FOrdGreaterThanEqual = 190, + FUnordGreaterThanEqual = 191, + ShiftRightLogical = 194, + ShiftRightArithmetic = 195, + ShiftLeftLogical = 196, + BitwiseOr = 197, + BitwiseXor = 198, + BitwiseAnd = 199, + Not = 200, + BitFieldInsert = 201, + BitFieldSExtract = 202, + BitFieldUExtract = 203, + BitReverse = 204, + BitCount = 205, + ControlBarrier = 224, + MemoryBarrier = 225, + AtomicIAdd = 234, + Phi = 245, + LoopMerge = 246, + SelectionMerge = 247, + Label = 248, + Branch = 249, + BranchConditional = 250, + Switch = 251, + Kill = 252, + Return = 253, + ReturnValue = 254, + Unreachable = 255, + GroupNonUniformElect = 333, + GroupNonUniformAll = 334, + GroupNonUniformAny = 335, + GroupNonUniformAllEqual = 336, + GroupNonUniformBroadcast = 337, + GroupNonUniformBroadcastFirst = 338, + GroupNonUniformBallot = 339, + GroupNonUniformShuffle = 345, + GroupNonUniformShuffleXor = 346, + GroupNonUniformShuffleUp = 347, + GroupNonUniformShuffleDown = 348, +} + +internal enum SpirvCapability : uint +{ + Shader = 1, + Float16 = 9, + Float64 = 10, + Int64 = 11, + Int16 = 22, + ImageGatherExtended = 25, + StorageImageExtendedFormats = 49, + ImageQuery = 50, + StorageImageReadWithoutFormat = 55, + StorageImageWriteWithoutFormat = 56, + GroupNonUniform = 61, + GroupNonUniformVote = 62, + GroupNonUniformBallot = 64, + GroupNonUniformShuffle = 65, + RuntimeDescriptorArray = 5302, +} + +internal enum SpirvStorageClass : uint +{ + UniformConstant = 0, + Input = 1, + Uniform = 2, + Output = 3, + Workgroup = 4, + Private = 6, + Function = 7, + PushConstant = 9, + Image = 11, + StorageBuffer = 12, +} + +internal enum SpirvExecutionModel : uint +{ + Vertex = 0, + Fragment = 4, + GLCompute = 5, +} + +internal enum SpirvExecutionMode : uint +{ + OriginUpperLeft = 7, + DepthReplacing = 12, + LocalSize = 17, +} + +internal enum SpirvDecoration : uint +{ + Block = 2, + ArrayStride = 6, + BuiltIn = 11, + NoPerspective = 13, + Flat = 14, + Location = 30, + Binding = 33, + DescriptorSet = 34, + Offset = 35, +} + +internal enum SpirvBuiltIn : uint +{ + Position = 0, + VertexIndex = 42, + InstanceIndex = 43, + FragCoord = 15, + FrontFacing = 17, + WorkgroupId = 26, + LocalInvocationId = 27, + GlobalInvocationId = 28, + LocalInvocationIndex = 29, + SubgroupLocalInvocationId = 41, +} + +internal enum SpirvImageDim : uint +{ + Dim1D = 0, + Dim2D = 1, + Dim3D = 2, + Cube = 3, + Buffer = 5, +} + +internal enum SpirvImageFormat : uint +{ + Unknown = 0, + Rgba32f = 1, + Rgba16f = 2, + R32f = 3, + Rgba8 = 4, + Rgba8Snorm = 5, + Rg32f = 6, + Rg16f = 7, + R11fG11fB10f = 8, + R16f = 9, + Rgba16 = 10, + Rgb10A2 = 11, + Rg16 = 12, + Rg8 = 13, + R16 = 14, + R8 = 15, + Rgba16Snorm = 16, + Rg16Snorm = 17, + Rg8Snorm = 18, + R16Snorm = 19, + R8Snorm = 20, + Rgba32i = 21, + Rgba16i = 22, + Rgba8i = 23, + R32i = 24, + Rg32i = 25, + Rg16i = 26, + Rg8i = 27, + R16i = 28, + R8i = 29, + Rgba32ui = 30, + Rgba16ui = 31, + Rgba8ui = 32, + R32ui = 33, + Rgb10A2ui = 34, + Rg32ui = 35, + Rg16ui = 36, + Rg8ui = 37, + R16ui = 38, + R8ui = 39, +} + +internal sealed class SpirvModuleBuilder +{ + private const uint Magic = 0x07230203; + private const uint Version15 = 0x00010500; + private const uint Generator = 0x53504500; // "SPE" + + private readonly List _capabilities = []; + private readonly List _extensions = []; + private readonly List _imports = []; + private readonly List _memoryModel = []; + private readonly List _entryPoints = []; + private readonly List _executionModes = []; + private readonly List _debug = []; + private readonly List _annotations = []; + private readonly List _typesConstantsGlobals = []; + private readonly List _functions = []; + private readonly Dictionary<(uint Width, bool Signed), uint> _integerTypes = []; + private readonly Dictionary _floatTypes = []; + private readonly Dictionary<(uint Component, uint Count), uint> _vectorTypes = []; + private readonly Dictionary< + ( + uint SampledType, + SpirvImageDim Dimension, + bool Depth, + bool Arrayed, + bool Multisampled, + uint Sampled, + SpirvImageFormat Format + ), + uint> _imageTypes = []; + private readonly Dictionary _sampledImageTypes = []; + private readonly Dictionary<(SpirvStorageClass Storage, uint Type), uint> _pointerTypes = []; + private readonly Dictionary<(uint Element, uint Count), uint> _arrayTypes = []; + private readonly Dictionary _runtimeArrayTypes = []; + private readonly Dictionary _functionTypes = []; + private readonly Dictionary<(uint Type, ulong Value), uint> _constants = []; + private readonly HashSet _declaredCapabilities = []; + private readonly Dictionary _extInstImports = []; + private uint _nextId = 1; + private uint? _voidType; + private uint? _boolType; + + public uint AllocateId() => _nextId++; + + public void AddCapability(SpirvCapability capability) + { + if (_declaredCapabilities.Add(capability)) + { + Emit(_capabilities, SpirvOp.Capability, (uint)capability); + } + } + + public void AddExtension(string extension) => + EmitWithString(_extensions, SpirvOp.Extension, [], extension); + + public uint ImportExtInst(string name) + { + if (_extInstImports.TryGetValue(name, out var existing)) + { + return existing; + } + + var id = AllocateId(); + EmitWithString(_imports, SpirvOp.ExtInstImport, [id], name); + _extInstImports.Add(name, id); + return id; + } + + public void SetLogicalGlsl450MemoryModel() => + Emit(_memoryModel, SpirvOp.MemoryModel, 0, 1); + + public void AddEntryPoint( + SpirvExecutionModel model, + uint function, + string name, + IReadOnlyList interfaces) + { + var prefix = new uint[2 + interfaces.Count]; + prefix[0] = (uint)model; + prefix[1] = function; + for (var index = 0; index < interfaces.Count; index++) + { + prefix[index + 2] = interfaces[index]; + } + + EmitWithString(_entryPoints, SpirvOp.EntryPoint, prefix, name, stringBeforeTailCount: 2); + } + + public void AddExecutionMode(uint function, SpirvExecutionMode mode, params uint[] operands) + { + var values = new uint[2 + operands.Length]; + values[0] = function; + values[1] = (uint)mode; + operands.CopyTo(values, 2); + Emit(_executionModes, SpirvOp.ExecutionMode, values); + } + + public void AddName(uint target, string name) => + EmitWithString(_debug, SpirvOp.Name, [target], name); + + public void AddDecoration(uint target, SpirvDecoration decoration, params uint[] operands) + { + var values = new uint[2 + operands.Length]; + values[0] = target; + values[1] = (uint)decoration; + operands.CopyTo(values, 2); + Emit(_annotations, SpirvOp.Decorate, values); + } + + public void AddMemberDecoration( + uint target, + uint member, + SpirvDecoration decoration, + params uint[] operands) + { + var values = new uint[3 + operands.Length]; + values[0] = target; + values[1] = member; + values[2] = (uint)decoration; + operands.CopyTo(values, 3); + EmitRaw(_annotations, 72, values); + } + + public uint TypeVoid() + { + if (_voidType is { } existing) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypeVoid, id); + _voidType = id; + return id; + } + + public uint TypeBool() + { + if (_boolType is { } existing) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypeBool, id); + _boolType = id; + return id; + } + + public uint TypeInt(uint width, bool signed) + { + var key = (width, signed); + if (_integerTypes.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypeInt, id, width, signed ? 1u : 0u); + _integerTypes.Add(key, id); + return id; + } + + public uint TypeFloat(uint width) + { + if (_floatTypes.TryGetValue(width, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypeFloat, id, width); + _floatTypes.Add(width, id); + return id; + } + + public uint TypeVector(uint componentType, uint count) + { + var key = (componentType, count); + if (_vectorTypes.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypeVector, id, componentType, count); + _vectorTypes.Add(key, id); + return id; + } + + public uint TypeImage( + uint sampledType, + SpirvImageDim dimension, + bool depth, + bool arrayed, + bool multisampled, + uint sampled, + SpirvImageFormat format) + { + var key = ( + sampledType, + dimension, + depth, + arrayed, + multisampled, + sampled, + format); + if (_imageTypes.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit( + _typesConstantsGlobals, + SpirvOp.TypeImage, + id, + sampledType, + (uint)dimension, + depth ? 1u : 0u, + arrayed ? 1u : 0u, + multisampled ? 1u : 0u, + sampled, + (uint)format); + _imageTypes.Add(key, id); + return id; + } + + public uint TypeSampledImage(uint imageType) + { + if (_sampledImageTypes.TryGetValue(imageType, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypeSampledImage, id, imageType); + _sampledImageTypes.Add(imageType, id); + return id; + } + + public uint TypeArray(uint elementType, uint count) + { + var key = (elementType, count); + if (_arrayTypes.TryGetValue(key, out var existing)) + { + return existing; + } + + var length = Constant(TypeInt(32, false), count); + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypeArray, id, elementType, length); + _arrayTypes.Add(key, id); + return id; + } + + public uint TypeRuntimeArray(uint elementType) + { + if (_runtimeArrayTypes.TryGetValue(elementType, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypeRuntimeArray, id, elementType); + _runtimeArrayTypes.Add(elementType, id); + return id; + } + + public uint TypeStruct(params uint[] memberTypes) + { + var id = AllocateId(); + var operands = new uint[memberTypes.Length + 1]; + operands[0] = id; + memberTypes.CopyTo(operands, 1); + Emit(_typesConstantsGlobals, SpirvOp.TypeStruct, operands); + return id; + } + + public uint TypePointer(SpirvStorageClass storageClass, uint type) + { + var key = (storageClass, type); + if (_pointerTypes.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.TypePointer, id, (uint)storageClass, type); + _pointerTypes.Add(key, id); + return id; + } + + public uint TypeFunction(uint returnType, params uint[] parameterTypes) + { + var key = returnType + ":" + string.Join(',', parameterTypes); + if (_functionTypes.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + var operands = new uint[parameterTypes.Length + 2]; + operands[0] = id; + operands[1] = returnType; + parameterTypes.CopyTo(operands, 2); + Emit(_typesConstantsGlobals, SpirvOp.TypeFunction, operands); + _functionTypes.Add(key, id); + return id; + } + + public uint ConstantBool(bool value) + { + var type = TypeBool(); + var key = (type, value ? 1UL : 0UL); + if (_constants.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit( + _typesConstantsGlobals, + value ? SpirvOp.ConstantTrue : SpirvOp.ConstantFalse, + type, + id); + _constants.Add(key, id); + return id; + } + + public uint Constant(uint type, uint value) + { + var key = (type, (ulong)value); + if (_constants.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.Constant, type, id, value); + _constants.Add(key, id); + return id; + } + + public uint Constant64(uint type, ulong value) + { + var key = (type, value); + if (_constants.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit( + _typesConstantsGlobals, + SpirvOp.Constant, + type, + id, + (uint)value, + (uint)(value >> 32)); + _constants.Add(key, id); + return id; + } + + public uint ConstantFloat(uint type, float value) => + Constant(type, BitConverter.SingleToUInt32Bits(value)); + + public uint ConstantComposite(uint type, params uint[] constituents) + { + var id = AllocateId(); + var operands = new uint[constituents.Length + 2]; + operands[0] = type; + operands[1] = id; + constituents.CopyTo(operands, 2); + Emit(_typesConstantsGlobals, SpirvOp.ConstantComposite, operands); + return id; + } + + public uint ConstantNull(uint type) + { + var key = (type, ulong.MaxValue); + if (_constants.TryGetValue(key, out var existing)) + { + return existing; + } + + var id = AllocateId(); + Emit(_typesConstantsGlobals, SpirvOp.ConstantNull, type, id); + _constants.Add(key, id); + return id; + } + + public uint AddGlobalVariable( + uint pointerType, + SpirvStorageClass storageClass, + uint? initializer = null) + { + var id = AllocateId(); + if (initializer.HasValue) + { + Emit( + _typesConstantsGlobals, + SpirvOp.Variable, + pointerType, + id, + (uint)storageClass, + initializer.Value); + } + else + { + Emit(_typesConstantsGlobals, SpirvOp.Variable, pointerType, id, (uint)storageClass); + } + + return id; + } + + public uint BeginFunction(uint returnType, uint functionType) + { + var id = AllocateId(); + Emit(_functions, SpirvOp.Function, returnType, id, 0, functionType); + return id; + } + + public uint AddFunctionParameter(uint type) => + EmitResult(_functions, SpirvOp.FunctionParameter, type); + + public uint AddLabel(uint? id = null) + { + var result = id ?? AllocateId(); + Emit(_functions, SpirvOp.Label, result); + return result; + } + + public uint AddFunctionVariable(uint pointerType, uint? initializer = null) + { + var id = AllocateId(); + if (initializer.HasValue) + { + Emit( + _functions, + SpirvOp.Variable, + pointerType, + id, + (uint)SpirvStorageClass.Function, + initializer.Value); + } + else + { + Emit( + _functions, + SpirvOp.Variable, + pointerType, + id, + (uint)SpirvStorageClass.Function); + } + + return id; + } + + public uint AddInstruction(SpirvOp opcode, uint resultType, params uint[] operands) => + EmitResult(_functions, opcode, resultType, operands); + + public void AddStatement(SpirvOp opcode, params uint[] operands) => + Emit(_functions, opcode, operands); + + public void EndFunction() => Emit(_functions, SpirvOp.FunctionEnd); + + public byte[] Build() + { + if (_memoryModel.Count == 0) + { + SetLogicalGlsl450MemoryModel(); + } + + var wordCount = + 5 + + _capabilities.Count + + _extensions.Count + + _imports.Count + + _memoryModel.Count + + _entryPoints.Count + + _executionModes.Count + + _debug.Count + + _annotations.Count + + _typesConstantsGlobals.Count + + _functions.Count; + var words = new uint[wordCount]; + var offset = 0; + WriteWord(Magic); + WriteWord(Version15); + WriteWord(Generator); + WriteWord(_nextId); + WriteWord(0); + WriteSection(_capabilities); + WriteSection(_extensions); + WriteSection(_imports); + WriteSection(_memoryModel); + WriteSection(_entryPoints); + WriteSection(_executionModes); + WriteSection(_debug); + WriteSection(_annotations); + WriteSection(_typesConstantsGlobals); + WriteSection(_functions); + var bytes = new byte[wordCount * sizeof(uint)]; + Buffer.BlockCopy(words, 0, bytes, 0, bytes.Length); + return bytes; + + void WriteWord(uint value) + { + words[offset++] = value; + } + + void WriteSection(List section) + { + foreach (var value in section) + { + WriteWord(value); + } + } + } + + private uint EmitResult( + List section, + SpirvOp opcode, + uint resultType, + params uint[] operands) + { + var result = AllocateId(); + var values = new uint[operands.Length + 2]; + values[0] = resultType; + values[1] = result; + operands.CopyTo(values, 2); + Emit(section, opcode, values); + return result; + } + + private static void Emit(List section, SpirvOp opcode, params uint[] operands) => + EmitRaw(section, (ushort)opcode, operands); + + private static void EmitRaw(List section, ushort opcode, params uint[] operands) + { + section.Add(((uint)(operands.Length + 1) << 16) | opcode); + section.AddRange(operands); + } + + private static void EmitWithString( + List section, + SpirvOp opcode, + IReadOnlyList prefix, + string value, + int stringBeforeTailCount = -1) + { + var encoded = EncodeString(value); + if (stringBeforeTailCount < 0) + { + var operands = new uint[prefix.Count + encoded.Length]; + for (var index = 0; index < prefix.Count; index++) + { + operands[index] = prefix[index]; + } + + encoded.CopyTo(operands, prefix.Count); + Emit(section, opcode, operands); + return; + } + + var result = new uint[prefix.Count + encoded.Length]; + for (var index = 0; index < stringBeforeTailCount; index++) + { + result[index] = prefix[index]; + } + + encoded.CopyTo(result, stringBeforeTailCount); + for (var index = stringBeforeTailCount; index < prefix.Count; index++) + { + result[index + encoded.Length] = prefix[index]; + } + + Emit(section, opcode, result); + } + + private static uint[] EncodeString(string value) + { + var byteCount = Encoding.UTF8.GetByteCount(value) + 1; + var words = new uint[(byteCount + 3) / 4]; + Encoding.UTF8.GetBytes(value, System.Runtime.InteropServices.MemoryMarshal.AsBytes(words.AsSpan())); + return words; + } +} diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 9982448..124af76 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -5388,6 +5388,37 @@ public static class KernelMemoryCompatExports } } + internal static bool TryReadTrackedLibcHeap( + ulong address, + Span destination) + { + if (destination.IsEmpty) + { + return true; + } + + var length = (ulong)destination.Length; + lock (_libcAllocGate) + { + foreach (var (allocationAddress, allocation) in _libcAllocations) + { + var allocationSize = (ulong)allocation.Size; + var offset = address >= allocationAddress + ? address - allocationAddress + : ulong.MaxValue; + if (offset > allocationSize || + length > allocationSize - offset) + { + continue; + } + + return TryReadHostMemory(address, destination); + } + } + + return false; + } + private static bool TryAllocateLibcHeap(ulong requestedSize, nuint alignment, bool zeroFill, out ulong address) { address = 0; diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index da394ea..de82410 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -360,7 +360,7 @@ public static class VideoOutExports var bufferIndex = unchecked((int)ctx[CpuRegister.Rsi]); var flipMode = unchecked((int)ctx[CpuRegister.Rdx]); var flipArg = unchecked((long)ctx[CpuRegister.Rcx]); - return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg); + return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: true); } [SysAbiExport( @@ -440,7 +440,7 @@ public static class VideoOutExports } public static int SubmitFlipFromAgc(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) => - SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg); + SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: false); internal static void SubmitHostRgbaFrame(ReadOnlySpan rgbaFrame, uint width, uint height) { @@ -748,7 +748,13 @@ public static class VideoOutExports return groupIndex < 0 ? groupIndex : setIndex; } - private static int SubmitFlip(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) + private static int SubmitFlip( + CpuContext ctx, + int handle, + int bufferIndex, + int flipMode, + long flipArg, + bool submitGpuImage) { if (!TryGetPort(handle, out var port)) { @@ -776,6 +782,17 @@ public static class VideoOutExports flipEvents = new List(port.FlipEvents); } + if (submitGpuImage && + bufferIndex >= 0 && + TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer)) + { + _ = VulkanVideoPresenter.TrySubmitGuestImage( + displayBuffer.Address, + displayBuffer.Width, + displayBuffer.Height, + displayBuffer.PitchInPixel); + } + if (string.Equals( Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"), "1", diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 5d1fac1..dc7da05 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -4,9 +4,12 @@ using Silk.NET.Core; using Silk.NET.Core.Native; using Silk.NET.Maths; +using SharpEmu.Libs.Agc; using Silk.NET.Vulkan; using Silk.NET.Vulkan.Extensions.KHR; using Silk.NET.Windowing; +using System.Runtime.CompilerServices; +using System.Text; using VkBuffer = Silk.NET.Vulkan.Buffer; using VkSemaphore = Silk.NET.Vulkan.Semaphore; @@ -18,15 +21,162 @@ internal enum GuestDrawKind FullscreenBarycentric, } +internal sealed record VulkanGuestDrawTexture( + ulong Address, + uint Width, + uint Height, + uint Format, + uint NumberType, + byte[] RgbaPixels, + bool IsFallback, + bool IsStorage, + uint MipLevels = 1, + uint MipLevel = 0, + uint Pitch = 0, + uint TileMode = 0, + uint DstSelect = 0xFAC, + VulkanGuestSampler Sampler = default); + +internal readonly record struct VulkanGuestSampler( + uint Word0, + uint Word1, + uint Word2, + uint Word3); + +internal sealed record VulkanGuestMemoryBuffer( + ulong BaseAddress, + byte[] Data); + +internal sealed record VulkanGuestVertexBuffer( + uint Location, + uint ComponentCount, + uint DataFormat, + uint NumberFormat, + ulong BaseAddress, + uint Stride, + uint OffsetBytes, + byte[] Data); + +internal sealed record VulkanGuestIndexBuffer( + byte[] Data, + bool Is32Bit); + +internal readonly record struct VulkanGuestRect( + int X, + int Y, + uint Width, + uint Height); + +internal readonly record struct VulkanGuestViewport( + float X, + float Y, + float Width, + float Height, + float MinDepth, + float MaxDepth); + +internal readonly record struct VulkanGuestBlendState( + bool Enable, + uint ColorSrcFactor, + uint ColorDstFactor, + uint ColorFunc, + uint AlphaSrcFactor, + uint AlphaDstFactor, + uint AlphaFunc, + bool SeparateAlphaBlend, + uint WriteMask) +{ + public static VulkanGuestBlendState Default { get; } = new( + Enable: false, + ColorSrcFactor: 1, + ColorDstFactor: 0, + ColorFunc: 0, + AlphaSrcFactor: 1, + AlphaDstFactor: 0, + AlphaFunc: 0, + SeparateAlphaBlend: false, + WriteMask: 0xFu); +} + +internal sealed record VulkanGuestRenderState( + VulkanGuestBlendState Blend, + VulkanGuestRect? Scissor, + VulkanGuestViewport? Viewport) +{ + public static VulkanGuestRenderState Default { get; } = new( + VulkanGuestBlendState.Default, + Scissor: null, + Viewport: null); +} + +internal sealed record VulkanGuestRenderTarget( + ulong Address, + uint Width, + uint Height, + uint Format, + uint NumberType, + uint MipLevels = 1); + +internal sealed record VulkanTranslatedGuestDraw( + byte[] VertexSpirv, + byte[] PixelSpirv, + IReadOnlyList Textures, + IReadOnlyList GlobalMemoryBuffers, + IReadOnlyList VertexBuffers, + uint AttributeCount, + uint VertexCount, + uint InstanceCount, + uint PrimitiveType, + VulkanGuestIndexBuffer? IndexBuffer, + VulkanGuestRenderState RenderState); + +internal sealed record VulkanOffscreenGuestDraw( + VulkanTranslatedGuestDraw Draw, + VulkanGuestRenderTarget Target, + bool PublishTarget); + +internal sealed record VulkanComputeGuestDispatch( + ulong ShaderAddress, + byte[] ComputeSpirv, + IReadOnlyList Textures, + IReadOnlyList GlobalMemoryBuffers, + uint GroupCountX, + uint GroupCountY, + uint GroupCountZ); + internal static unsafe class VulkanVideoPresenter { + private const uint DefaultWindowWidth = 1280; + private const uint DefaultWindowHeight = 720; + private const int MaxPendingGuestWork = 16; + private const int MaxGuestWorkPerRender = 16; + private const uint GuestPrimitiveRectList = 0x11; + private const uint GuestFormatR32Uint = 0x10004; + private const uint GuestFormatR32Sint = 0x20004; + private const uint GuestFormatR32Sfloat = 0x30004; + private const uint GuestFormatR16G16Uint = 0x10005; + private const uint GuestFormatR16G16Sint = 0x20005; + private const uint GuestFormatR16G16Sfloat = 0x30005; + private const uint GuestFormatR8G8B8A8Uint = 0x1000A; + private const uint GuestFormatR8G8B8A8Sint = 0x2000A; + private const uint GuestFormatR16G16B16A16Uint = 0x1000C; + private const uint GuestFormatR16G16B16A16Sint = 0x2000C; + private static readonly object _gate = new(); + private static readonly Queue _pendingGuestWork = new(); + private static readonly Dictionary _availableGuestImages = new(); + private static readonly HashSet<(ulong Address, uint Width, uint Height)> + _tracedGuestImageSubmissions = []; private static Thread? _thread; private static Presentation? _latestPresentation; + private static byte[]? _copyFragmentSpirv; private static uint _windowWidth; private static uint _windowHeight; private static bool _closed; + private const string DebugUtilsExtensionName = "VK_EXT_debug_utils"; private static bool _splashHidden; + private static long _enqueuedGuestWorkSequence; + private static long _completedGuestWorkSequence; public static void EnsureStarted(uint width, uint height) { @@ -63,6 +213,8 @@ internal static unsafe class VulkanVideoPresenter height, 1, GuestDrawKind.None, + TranslatedDraw: null, + RequiredGuestWorkSequence: _enqueuedGuestWorkSequence, IsSplash: false) : hasSplash ? new Presentation( @@ -71,6 +223,8 @@ internal static unsafe class VulkanVideoPresenter splashHeight, 1, GuestDrawKind.None, + TranslatedDraw: null, + RequiredGuestWorkSequence: _enqueuedGuestWorkSequence, IsSplash: true) : new Presentation( null, @@ -78,6 +232,8 @@ internal static unsafe class VulkanVideoPresenter height, 0, GuestDrawKind.None, + TranslatedDraw: null, + RequiredGuestWorkSequence: _enqueuedGuestWorkSequence, IsSplash: false); _thread = new Thread(Run) { @@ -105,6 +261,8 @@ internal static unsafe class VulkanVideoPresenter latest.Height, sequence, GuestDrawKind.None, + TranslatedDraw: null, + RequiredGuestWorkSequence: _enqueuedGuestWorkSequence, IsSplash: false); Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash"); } @@ -131,6 +289,8 @@ internal static unsafe class VulkanVideoPresenter height, sequence, GuestDrawKind.None, + TranslatedDraw: null, + RequiredGuestWorkSequence: _enqueuedGuestWorkSequence, IsSplash: false); if (_thread is not null) { @@ -173,6 +333,8 @@ internal static unsafe class VulkanVideoPresenter height, sequence, drawKind, + TranslatedDraw: null, + RequiredGuestWorkSequence: _enqueuedGuestWorkSequence, IsSplash: false); if (_thread is not null) { @@ -190,6 +352,351 @@ internal static unsafe class VulkanVideoPresenter } } + public static void SubmitTranslatedDraw( + byte[] pixelSpirv, + IReadOnlyList textures, + IReadOnlyList globalMemoryBuffers, + uint width, + uint height, + uint attributeCount, + byte[]? vertexSpirv = null, + uint vertexCount = 3, + uint instanceCount = 1, + uint primitiveType = 4, + VulkanGuestIndexBuffer? indexBuffer = null, + IReadOnlyList? vertexBuffers = null, + VulkanGuestRenderState? renderState = null) + { + if (pixelSpirv.Length == 0 || width == 0 || height == 0) + { + return; + } + + lock (_gate) + { + if (_closed) + { + return; + } + + var sequence = (_latestPresentation?.Sequence ?? 0) + 1; + _latestPresentation = new Presentation( + null, + width, + height, + sequence, + GuestDrawKind.None, + new VulkanTranslatedGuestDraw( + vertexSpirv ?? [], + pixelSpirv, + textures.ToArray(), + globalMemoryBuffers.ToArray(), + vertexBuffers?.ToArray() ?? [], + attributeCount, + vertexCount, + instanceCount, + primitiveType, + indexBuffer, + renderState ?? VulkanGuestRenderState.Default), + RequiredGuestWorkSequence: _enqueuedGuestWorkSequence, + IsSplash: false); + if (_thread is not null) + { + return; + } + + _windowWidth = width; + _windowHeight = height; + _thread = new Thread(Run) + { + IsBackground = true, + Name = "SharpEmu Vulkan VideoOut", + }; + _thread.Start(); + } + } + + public static void SubmitOffscreenTranslatedDraw( + byte[] pixelSpirv, + IReadOnlyList textures, + IReadOnlyList globalMemoryBuffers, + uint attributeCount, + VulkanGuestRenderTarget target, + byte[]? vertexSpirv = null, + uint vertexCount = 3, + uint instanceCount = 1, + uint primitiveType = 4, + VulkanGuestIndexBuffer? indexBuffer = null, + IReadOnlyList? vertexBuffers = null, + VulkanGuestRenderState? renderState = null) + { + if (pixelSpirv.Length == 0 || + target.Address == 0 || + target.Width == 0 || + target.Height == 0) + { + return; + } + + lock (_gate) + { + if (_closed) + { + return; + } + + var guestTextureFormat = GetGuestTextureFormat( + target.Format, + target.NumberType); + if (guestTextureFormat != 0) + { + _availableGuestImages[target.Address] = guestTextureFormat; + } + + EnqueueGuestWorkLocked( + new VulkanOffscreenGuestDraw( + new VulkanTranslatedGuestDraw( + vertexSpirv ?? [], + pixelSpirv, + textures.ToArray(), + globalMemoryBuffers.ToArray(), + vertexBuffers?.ToArray() ?? [], + attributeCount, + vertexCount, + instanceCount, + primitiveType, + indexBuffer, + renderState ?? VulkanGuestRenderState.Default), + target, + PublishTarget: true)); + } + } + + public static void SubmitStorageTranslatedDraw( + byte[] pixelSpirv, + IReadOnlyList textures, + IReadOnlyList globalMemoryBuffers, + uint attributeCount, + uint width, + uint height) + { + if (pixelSpirv.Length == 0 || + width == 0 || + height == 0 || + textures.All(texture => !texture.IsStorage)) + { + return; + } + + lock (_gate) + { + if (_closed) + { + return; + } + + EnqueueGuestWorkLocked( + new VulkanOffscreenGuestDraw( + new VulkanTranslatedGuestDraw( + [], + pixelSpirv, + textures.ToArray(), + globalMemoryBuffers.ToArray(), + [], + attributeCount, + 3, + 1, + 4, + null, + VulkanGuestRenderState.Default), + new VulkanGuestRenderTarget( + Address: 0, + width, + height, + Format: 12, + NumberType: 7), + PublishTarget: false)); + } + } + + public static void SubmitComputeDispatch( + ulong shaderAddress, + byte[] computeSpirv, + IReadOnlyList textures, + IReadOnlyList globalMemoryBuffers, + uint groupCountX, + uint groupCountY, + uint groupCountZ) + { + if (computeSpirv.Length == 0 || + groupCountX == 0 || + groupCountY == 0 || + groupCountZ == 0 || + textures.All(texture => !texture.IsStorage)) + { + return; + } + + lock (_gate) + { + if (_closed) + { + return; + } + + EnqueueGuestWorkLocked( + new VulkanComputeGuestDispatch( + shaderAddress, + computeSpirv, + textures.ToArray(), + globalMemoryBuffers.ToArray(), + groupCountX, + groupCountY, + groupCountZ)); + } + } + + public static bool TrySubmitGuestImage( + ulong address, + uint width, + uint height, + uint pitchInPixel) + { + var traceSubmission = false; + lock (_gate) + { + if (_closed || + !_availableGuestImages.ContainsKey(address)) + { + return false; + } + + traceSubmission = + _tracedGuestImageSubmissions.Add((address, width, height)); + var sequence = (_latestPresentation?.Sequence ?? 0) + 1; + _latestPresentation = new Presentation( + null, + width, + height, + sequence, + GuestDrawKind.None, + TranslatedDraw: null, + RequiredGuestWorkSequence: 0, + IsSplash: false, + GuestImageAddress: address); + } + + if (traceSubmission) + { + var effectivePitch = pitchInPixel == 0 ? width : pitchInPixel; + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.submit_guest_image addr=0x{address:X16} " + + $"size={width}x{height} pitch={effectivePitch}"); + } + + return true; + } + + public static bool TrySubmitGuestImageBlit( + ulong sourceAddress, + uint sourceWidth, + uint sourceHeight, + uint sourceFormat, + ulong destinationAddress, + uint destinationWidth, + uint destinationHeight, + uint destinationFormat) + { + if (sourceAddress == 0 || + destinationAddress == 0 || + sourceWidth == 0 || + sourceHeight == 0 || + destinationWidth == 0 || + destinationHeight == 0 || + !TryGetCopyFragmentShader(out var fragmentSpirv)) + { + return false; + } + + lock (_gate) + { + if (_closed || + !_availableGuestImages.ContainsKey(sourceAddress) || + GetGuestTextureFormat(destinationFormat, 0) == 0) + { + return false; + } + } + + SubmitOffscreenTranslatedDraw( + fragmentSpirv, + [ + new VulkanGuestDrawTexture( + sourceAddress, + sourceWidth, + sourceHeight, + sourceFormat, + NumberType: 0, + [], + IsFallback: false, + IsStorage: false), + ], + [], + attributeCount: 1, + new VulkanGuestRenderTarget( + destinationAddress, + destinationWidth, + destinationHeight, + destinationFormat, + NumberType: 0)); + return true; + } + + private static bool TryGetCopyFragmentShader(out byte[] spirv) + { + lock (_gate) + { + if (_copyFragmentSpirv is not null) + { + spirv = _copyFragmentSpirv; + return true; + } + } + + spirv = SpirvFixedShaders.CreateCopyFragment(); + + lock (_gate) + { + _copyFragmentSpirv ??= spirv; + spirv = _copyFragmentSpirv; + } + + return true; + } + + private static uint GetGuestTextureFormat(uint format, uint numberType) => + (format, numberType) switch + { + (9, _) => 9, + (4, 4) => GuestFormatR32Uint, + (4, 5) => GuestFormatR32Sint, + (4, 7) => GuestFormatR32Sfloat, + (5, 4) => GuestFormatR16G16Uint, + (5, 5) => GuestFormatR16G16Sint, + (5, 7) => GuestFormatR16G16Sfloat, + (10, 4) => GuestFormatR8G8B8A8Uint, + (10, 5) => GuestFormatR8G8B8A8Sint, + (10, _) => 56, + (12, 4) => GuestFormatR16G16B16A16Uint, + (12, 5) => GuestFormatR16G16B16A16Sint, + (12, 7) => 71, + (_, 0) when IsKnownGuestTextureFormat(format) => format, + _ => 0, + }; + + private static bool IsKnownGuestTextureFormat(uint format) => + format is 4 or 5 or 7 or 9 or 13 or 14 or 22 or 29 or 36 or 56 or 62 or 64 or 71; + private static byte[] CreateBlackFrame(uint width, uint height) { if (width == 0 || height == 0 || width > 8192 || height > 8192) @@ -225,7 +732,7 @@ internal static unsafe class VulkanVideoPresenter } catch (Exception exception) { - Console.Error.WriteLine($"[LOADER][ERROR] Vulkan VideoOut presenter failed: {exception.Message}"); + Console.Error.WriteLine($"[LOADER][ERROR] Vulkan VideoOut presenter failed: {exception}"); } finally { @@ -233,6 +740,7 @@ internal static unsafe class VulkanVideoPresenter { _closed = true; _thread = null; + System.Threading.Monitor.PulseAll(_gate); } } } @@ -241,7 +749,9 @@ internal static unsafe class VulkanVideoPresenter { lock (_gate) { - if (_latestPresentation is not { } latest || latest.Sequence == presentedSequence) + if (_latestPresentation is not { } latest || + latest.Sequence == presentedSequence || + latest.RequiredGuestWorkSequence > _completedGuestWorkSequence) { presentation = default; return false; @@ -252,13 +762,51 @@ internal static unsafe class VulkanVideoPresenter } } + private static void EnqueueGuestWorkLocked(object work) + { + while (!_closed && + _thread is not null && + _pendingGuestWork.Count >= MaxPendingGuestWork) + { + System.Threading.Monitor.Wait(_gate); + } + + if (_closed) + { + return; + } + + _pendingGuestWork.Enqueue(work); + _enqueuedGuestWorkSequence++; + } + + private static bool TryTakeGuestWork(out object work) + { + lock (_gate) + { + return _pendingGuestWork.TryDequeue(out work!); + } + } + + private static void CompleteGuestWork() + { + lock (_gate) + { + _completedGuestWorkSequence++; + System.Threading.Monitor.PulseAll(_gate); + } + } + private readonly record struct Presentation( byte[]? Pixels, uint Width, uint Height, long Sequence, GuestDrawKind DrawKind, - bool IsSplash); + VulkanTranslatedGuestDraw? TranslatedDraw, + long RequiredGuestWorkSequence, + bool IsSplash, + ulong GuestImageAddress = 0); private sealed class Presenter : IDisposable { @@ -269,9 +817,13 @@ internal static unsafe class VulkanVideoPresenter "AwIjBwAAAQALAAgAEgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ABwAEAAAABAAAAG1haW4AAAAACQAAAAwAAAAQAAMABAAAAAcAAAADAAMAAgAAAMIBAAAFAAQABAAAAG1haW4AAAAABQAFAAkAAABvdXRDb2xvcgAAAAAFAAUADAAAAGJhcnljZW50cmljAEcABAAJAAAAHgAAAAAAAABHAAQADAAAAB4AAAAAAAAAEwACAAIAAAAhAAMAAwAAAAIAAAAWAAMABgAAACAAAAAXAAQABwAAAAYAAAAEAAAAIAAEAAgAAAADAAAABwAAADsABAAIAAAACQAAAAMAAAAXAAQACgAAAAYAAAACAAAAIAAEAAsAAAABAAAACgAAADsABAALAAAADAAAAAEAAAArAAQABgAAAA4AAAAAAAAANgAFAAIAAAAEAAAAAAAAAAMAAAD4AAIABQAAAD0ABAAKAAAADQAAAAwAAABRAAUABgAAAA8AAAANAAAAAAAAAFEABQAGAAAAEAAAAA0AAAABAAAAUAAHAAcAAAARAAAADwAAABAAAAAOAAAADgAAAD4AAwAJAAAAEQAAAP0AAQA4AAEA"; private readonly IWindow _window; + private const int MaxInFlightGuestSubmissions = 8; private Vk _vk = null!; private KhrSurface _surfaceApi = null!; private KhrSwapchain _swapchainApi = null!; + private delegate* unmanaged _setDebugUtilsObjectName; + private delegate* unmanaged _cmdBeginDebugUtilsLabel; + private delegate* unmanaged _cmdEndDebugUtilsLabel; private Instance _instance; private SurfaceKHR _surface; private PhysicalDevice _physicalDevice; @@ -290,6 +842,7 @@ internal static unsafe class VulkanVideoPresenter private Pipeline _barycentricPipeline; private CommandPool _commandPool; private CommandBuffer _commandBuffer; + private CommandBuffer _presentationCommandBuffer; private VkSemaphore _imageAvailable; private VkSemaphore _renderFinished; private VkBuffer _stagingBuffer; @@ -300,11 +853,114 @@ internal static unsafe class VulkanVideoPresenter private bool _firstFramePresented; private bool _firstGuestDrawPresented; private bool _splashPresented; + private bool _swapchainRecreateDeferred; + private bool _tracedPresentedSwapchain; + private bool _swapchainReadbackPending; + private int _directPresentationCount; + private readonly Dictionary _guestImages = new(); + private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new(); + private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureUploads = new(); + private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new(); + private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new(); + private readonly HashSet _tracedGuestImageContents = new(); + private readonly Dictionary _tracedGuestWriteCounts = new(); + private int _tracedVertexBufferCount; + private readonly Dictionary _computePipelines = + new(ReferenceEqualityComparer.Instance); + private readonly Queue _pendingGuestSubmissions = new(); + + private sealed class TranslatedDrawResources + { + public string DebugName = "SharpEmu translated"; + public PipelineLayout PipelineLayout; + public Pipeline Pipeline; + public bool PipelineCached; + public DescriptorSetLayout DescriptorSetLayout; + public DescriptorPool DescriptorPool; + public DescriptorSet DescriptorSet; + public TextureResource[] Textures = []; + public GlobalBufferResource[] GlobalMemoryBuffers = []; + public VertexBufferResource[] VertexBuffers = []; + public VkBuffer IndexBuffer; + public DeviceMemory IndexMemory; + public bool Index32Bit; + public uint VertexCount = 3; + public uint InstanceCount = 1; + public PrimitiveTopology Topology = PrimitiveTopology.TriangleList; + public VulkanGuestBlendState Blend = VulkanGuestBlendState.Default; + public VulkanGuestRect? Scissor; + public VulkanGuestViewport? Viewport; + } + + private sealed class TextureResource + { + public ulong Address; + public VkBuffer StagingBuffer; + public DeviceMemory StagingMemory; + public Image Image; + public DeviceMemory ImageMemory; + public ImageView View; + public uint Width; + public uint Height; + public uint RowLength; + public uint DstSelect; + public bool NeedsUpload; + public bool OwnsStorage; + public bool IsStorage; + public VulkanGuestSampler SamplerState; + public Sampler Sampler; + public GuestImageResource? GuestImage; + } + + private sealed class GlobalBufferResource + { + public VkBuffer Buffer; + public DeviceMemory Memory; + public ulong Size; + } + + private sealed class VertexBufferResource + { + public VkBuffer Buffer; + public DeviceMemory Memory; + public ulong Size; + public uint Location; + public uint ComponentCount; + public uint DataFormat; + public uint NumberFormat; + public uint Stride; + public uint OffsetBytes; + } + + private sealed class GuestImageResource + { + public ulong Address; + public uint Width; + public uint Height; + public uint MipLevels; + public Format Format; + public Image Image; + public DeviceMemory Memory; + public ImageView View; + public ImageView[] MipViews = []; + public Dictionary<(Format Format, uint MipLevel, uint LevelCount, uint DstSelect), ImageView> FormatViews { get; } = new(); + public RenderPass RenderPass; + public Framebuffer Framebuffer; + public bool Initialized; + public bool InitialUploadPending; + } + + private sealed record PendingGuestSubmission( + Fence Fence, + CommandBuffer CommandBuffer, + TranslatedDrawResources Resources, + IReadOnlyList TraceImages, + string DebugName); public Presenter(uint width, uint height) { var options = WindowOptions.DefaultVulkan; - options.Size = new Vector2D((int)width, (int)height); + options.Size = new Vector2D((int)DefaultWindowWidth, (int)DefaultWindowHeight); options.Title = VideoOutExports.GetWindowTitle(); options.WindowBorder = WindowBorder.Fixed; options.VSync = true; @@ -321,11 +977,21 @@ internal static unsafe class VulkanVideoPresenter public void Dispose() { DisposeVulkan(); - _window.Dispose(); + try + { + _window.Dispose(); + } + catch (InvalidOperationException exception) + when (exception.Message.Contains("render loop", StringComparison.OrdinalIgnoreCase)) + { + Console.Error.WriteLine( + $"[LOADER][WARN] Vulkan VideoOut window dispose skipped during render loop: {exception.Message}"); + } } private void Initialize() { + WaitForRenderDocAttachIfRequested(); _vk = Vk.GetApi(); CreateInstance(); CreateSurface(); @@ -339,6 +1005,183 @@ internal static unsafe class VulkanVideoPresenter $"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}"); } + private static void WaitForRenderDocAttachIfRequested() + { + var value = Environment.GetEnvironmentVariable("SHARPEMU_RENDERDOC_WAIT"); + if (string.IsNullOrWhiteSpace(value)) + { + return; + } + + if (string.Equals(value, "enter", StringComparison.OrdinalIgnoreCase)) + { + Console.Error.WriteLine( + $"[LOADER][INFO] Waiting for RenderDoc attach before Vulkan init. pid={Environment.ProcessId}. Press Enter to continue."); + _ = Console.ReadLine(); + return; + } + + var seconds = 15; + if (int.TryParse(value, out var parsedSeconds)) + { + seconds = Math.Clamp(parsedSeconds, 1, 300); + } + + Console.Error.WriteLine( + $"[LOADER][INFO] Waiting {seconds}s for RenderDoc attach before Vulkan init. pid={Environment.ProcessId}"); + Thread.Sleep(TimeSpan.FromSeconds(seconds)); + } + + private bool IsInstanceExtensionAvailable(string extensionName) + { + uint extensionCount = 0; + if (_vk.EnumerateInstanceExtensionProperties((byte*)null, &extensionCount, null) != Result.Success || + extensionCount == 0) + { + return false; + } + + var properties = new ExtensionProperties[extensionCount]; + fixed (ExtensionProperties* propertyPointer = properties) + { + if (_vk.EnumerateInstanceExtensionProperties( + (byte*)null, + &extensionCount, + propertyPointer) != Result.Success) + { + return false; + } + + var expected = Encoding.UTF8.GetBytes(extensionName); + for (var index = 0; index < extensionCount; index++) + { + if (Utf8NullTerminatedEquals(propertyPointer[index].ExtensionName, expected)) + { + return true; + } + } + } + + return false; + } + + private static bool Utf8NullTerminatedEquals(byte* actual, ReadOnlySpan expected) + { + for (var index = 0; index < expected.Length; index++) + { + if (actual[index] != expected[index]) + { + return false; + } + } + + return actual[expected.Length] == 0; + } + + private void LoadDebugUtilsCommands() + { + var setObjectName = _vk.GetDeviceProcAddr(_device, "vkSetDebugUtilsObjectNameEXT"); + var beginLabel = _vk.GetDeviceProcAddr(_device, "vkCmdBeginDebugUtilsLabelEXT"); + var endLabel = _vk.GetDeviceProcAddr(_device, "vkCmdEndDebugUtilsLabelEXT"); + _setDebugUtilsObjectName = + (delegate* unmanaged) + setObjectName.Handle; + _cmdBeginDebugUtilsLabel = + (delegate* unmanaged) + beginLabel.Handle; + _cmdEndDebugUtilsLabel = + (delegate* unmanaged) + endLabel.Handle; + + if (_setDebugUtilsObjectName is not null) + { + Console.Error.WriteLine("[LOADER][INFO] Vulkan debug labels enabled."); + } + } + + private void SetDebugName(ObjectType objectType, ulong objectHandle, string name) + { + if (_setDebugUtilsObjectName is null || + _device.Handle == 0 || + objectHandle == 0) + { + return; + } + + var bytes = NullTerminatedUtf8(name); + fixed (byte* namePointer = bytes) + { + var info = new DebugUtilsObjectNameInfoEXT + { + SType = StructureType.DebugUtilsObjectNameInfoExt, + ObjectType = objectType, + ObjectHandle = objectHandle, + PObjectName = namePointer, + }; + _ = _setDebugUtilsObjectName(_device, &info); + } + } + + private void BeginDebugLabel(CommandBuffer commandBuffer, string name) + { + if (_cmdBeginDebugUtilsLabel is null || + commandBuffer.Handle == 0) + { + return; + } + + var bytes = NullTerminatedUtf8(name); + fixed (byte* namePointer = bytes) + { + var label = new DebugUtilsLabelEXT + { + SType = StructureType.DebugUtilsLabelExt, + PLabelName = namePointer, + }; + label.Color[0] = 0.20f; + label.Color[1] = 0.60f; + label.Color[2] = 1.00f; + label.Color[3] = 1.00f; + _cmdBeginDebugUtilsLabel(commandBuffer, &label); + } + } + + private void EndDebugLabel(CommandBuffer commandBuffer) + { + if (_cmdEndDebugUtilsLabel is not null && + commandBuffer.Handle != 0) + { + _cmdEndDebugUtilsLabel(commandBuffer); + } + } + + private static byte[] NullTerminatedUtf8(string value) + { + var bytes = Encoding.UTF8.GetBytes(value); + Array.Resize(ref bytes, bytes.Length + 1); + return bytes; + } + + private static string BuildComputeDebugName(VulkanComputeGuestDispatch dispatch) + { + var storage = dispatch.Textures.FirstOrDefault(texture => texture.IsStorage && texture.Address != 0); + return storage is null + ? $"SharpEmu compute cs=0x{dispatch.ShaderAddress:X16} " + + $"{dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ}" + : $"SharpEmu compute cs=0x{dispatch.ShaderAddress:X16} " + + $"storage=0x{storage.Address:X16} " + + $"{storage.Width}x{storage.Height} fmt{storage.Format} " + + $"{dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ}"; + } + + private static string GuestImageDebugName(VulkanGuestRenderTarget target, Format format) => + $"SharpEmu guest 0x{target.Address:X16} {target.Width}x{target.Height} " + + $"fmt{target.Format}/{format}"; + + private static string TextureDebugName(VulkanGuestDrawTexture texture, Format format) => + $"SharpEmu texture 0x{texture.Address:X16} {texture.Width}x{texture.Height} " + + $"fmt{texture.Format}/{format}"; + private void CreateInstance() { var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu"); @@ -355,18 +1198,42 @@ internal static unsafe class VulkanVideoPresenter }; var extensions = _window.VkSurface!.GetRequiredExtensions(out var extensionCount); + byte* debugUtilsExtension = null; + var enabledExtensionCount = (int)extensionCount; + var enabledExtensions = stackalloc byte*[(int)extensionCount + 1]; + for (var index = 0; index < (int)extensionCount; index++) + { + enabledExtensions[index] = extensions[index]; + } + + if (IsInstanceExtensionAvailable(DebugUtilsExtensionName)) + { + debugUtilsExtension = (byte*)SilkMarshal.StringToPtr(DebugUtilsExtensionName); + enabledExtensions[enabledExtensionCount++] = debugUtilsExtension; + } + var createInfo = new InstanceCreateInfo { SType = StructureType.InstanceCreateInfo, PApplicationInfo = &applicationInfo, - EnabledExtensionCount = extensionCount, - PpEnabledExtensionNames = extensions, + EnabledExtensionCount = (uint)enabledExtensionCount, + PpEnabledExtensionNames = enabledExtensions, }; - Check(_vk.CreateInstance(&createInfo, null, out _instance), "vkCreateInstance"); - if (!_vk.TryGetInstanceExtension(_instance, out _surfaceApi)) + try { - throw new InvalidOperationException("VK_KHR_surface is unavailable."); + Check(_vk.CreateInstance(&createInfo, null, out _instance), "vkCreateInstance"); + if (!_vk.TryGetInstanceExtension(_instance, out _surfaceApi)) + { + throw new InvalidOperationException("VK_KHR_surface is unavailable."); + } + } + finally + { + if (debugUtilsExtension is not null) + { + SilkMarshal.Free((nint)debugUtilsExtension); + } } } finally @@ -456,6 +1323,7 @@ internal static unsafe class VulkanVideoPresenter } _vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue); + LoadDebugUtilsCommands(); if (!_vk.TryGetDeviceExtension(_instance, _device, out _swapchainApi)) { throw new InvalidOperationException("VK_KHR_swapchain is unavailable."); @@ -499,7 +1367,10 @@ internal static unsafe class VulkanVideoPresenter ImageColorSpace = surfaceFormat.ColorSpace, ImageExtent = _extent, ImageArrayLayers = 1, - ImageUsage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.ColorAttachmentBit, + ImageUsage = + ImageUsageFlags.TransferDstBit | + ImageUsageFlags.TransferSrcBit | + ImageUsageFlags.ColorAttachmentBit, ImageSharingMode = SharingMode.Exclusive, PreTransform = capabilities.CurrentTransform, CompositeAlpha = compositeAlpha, @@ -542,6 +1413,7 @@ internal static unsafe class VulkanVideoPresenter CommandBufferCount = 1, }; Check(_vk.AllocateCommandBuffers(_device, &allocateInfo, out _commandBuffer), "vkAllocateCommandBuffers"); + _presentationCommandBuffer = _commandBuffer; var semaphoreInfo = new SemaphoreCreateInfo { @@ -553,6 +1425,139 @@ internal static unsafe class VulkanVideoPresenter CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4); } + private CommandBuffer AllocateGuestCommandBuffer() + { + var allocateInfo = new CommandBufferAllocateInfo + { + SType = StructureType.CommandBufferAllocateInfo, + CommandPool = _commandPool, + Level = CommandBufferLevel.Primary, + CommandBufferCount = 1, + }; + CommandBuffer commandBuffer; + Check( + _vk.AllocateCommandBuffers( + _device, + &allocateInfo, + out commandBuffer), + "vkAllocateCommandBuffers(guest)"); + return commandBuffer; + } + + private void SubmitGuestCommandBuffer( + CommandBuffer commandBuffer, + TranslatedDrawResources resources, + IReadOnlyList traceImages) + { + var fenceInfo = new FenceCreateInfo + { + SType = StructureType.FenceCreateInfo, + }; + Fence fence; + Check( + _vk.CreateFence(_device, &fenceInfo, null, out fence), + "vkCreateFence(guest)"); + try + { + var submitInfo = new SubmitInfo + { + SType = StructureType.SubmitInfo, + CommandBufferCount = 1, + PCommandBuffers = &commandBuffer, + }; + Check( + _vk.QueueSubmit(_queue, 1, &submitInfo, fence), + "vkQueueSubmit(guest)"); + } + catch + { + _vk.DestroyFence(_device, fence, null); + throw; + } + + _pendingGuestSubmissions.Enqueue( + new PendingGuestSubmission( + fence, + commandBuffer, + resources, + traceImages, + resources.DebugName)); + } + + private void EnsureGuestSubmissionCapacity() + { + CollectCompletedGuestSubmissions(waitForOldest: false); + if (_pendingGuestSubmissions.Count >= MaxInFlightGuestSubmissions) + { + CollectCompletedGuestSubmissions(waitForOldest: true); + } + } + + private void CollectCompletedGuestSubmissions(bool waitForOldest) + { + if (waitForOldest && _pendingGuestSubmissions.TryPeek(out var oldest)) + { + var fence = oldest.Fence; + var result = _vk.WaitForFences( + _device, + 1, + &fence, + true, + ulong.MaxValue); + Check(result, $"vkWaitForFences(guest: {oldest.DebugName})"); + } + + while (_pendingGuestSubmissions.TryPeek(out var submission)) + { + var status = _vk.GetFenceStatus(_device, submission.Fence); + if (status == Result.NotReady) + { + break; + } + + Check(status, $"vkGetFenceStatus(guest: {submission.DebugName})"); + _pendingGuestSubmissions.Dequeue(); + + foreach (var image in submission.TraceImages) + { + TraceGuestImageContents(image); + } + + DestroyTranslatedDrawResources(submission.Resources); + var commandBuffer = submission.CommandBuffer; + _vk.FreeCommandBuffers( + _device, + _commandPool, + 1, + &commandBuffer); + _vk.DestroyFence(_device, submission.Fence, null); + } + } + + private IReadOnlyList GetTraceImages( + TranslatedDrawResources resources, + GuestImageResource? renderTarget = null) + { + var images = new HashSet(); + if (renderTarget is not null && + ShouldTraceGuestImageContents(renderTarget)) + { + images.Add(renderTarget); + } + + foreach (var texture in resources.Textures) + { + if (texture.IsStorage && + texture.GuestImage is { } image && + ShouldTraceGuestImageContents(image)) + { + images.Add(image); + } + } + + return images.ToArray(); + } + private void CreateGuestDrawResources() { var colorAttachment = new AttachmentDescription @@ -765,28 +1770,1774 @@ internal static unsafe class VulkanVideoPresenter } } - private void CreateStagingBuffer(ulong size) + private TranslatedDrawResources CreateTranslatedDrawResources( + VulkanTranslatedGuestDraw draw, + RenderPass renderPass, + Extent2D extent) { - var bufferInfo = new BufferCreateInfo + var vertexSpirv = draw.VertexSpirv; + if (vertexSpirv.Length == 0 && + !TryCompileFullscreenVertexShader( + draw.AttributeCount, + out vertexSpirv, + out var vertexError)) { - SType = StructureType.BufferCreateInfo, - Size = size, - Usage = BufferUsageFlags.TransferSrcBit, - SharingMode = SharingMode.Exclusive, - }; - Check(_vk.CreateBuffer(_device, &bufferInfo, null, out _stagingBuffer), "vkCreateBuffer"); + throw new InvalidOperationException($"translated vertex shader failed: {vertexError}"); + } - _vk.GetBufferMemoryRequirements(_device, _stagingBuffer, out var requirements); - var memoryInfo = new MemoryAllocateInfo + var resources = new TranslatedDrawResources + { + DebugName = "SharpEmu draw", + Textures = new TextureResource[draw.Textures.Count], + GlobalMemoryBuffers = + new GlobalBufferResource[draw.GlobalMemoryBuffers.Count], + VertexBuffers = new VertexBufferResource[draw.VertexBuffers.Count], + VertexCount = GetDrawVertexCount(draw.PrimitiveType, draw.VertexCount, draw.IndexBuffer), + InstanceCount = Math.Max(draw.InstanceCount, 1), + Topology = GetPrimitiveTopology(draw.PrimitiveType), + Blend = draw.RenderState.Blend, + Scissor = draw.RenderState.Scissor, + Viewport = draw.RenderState.Viewport, + }; + + try + { + foreach (var texture in draw.Textures) + { + if (texture.IsStorage) + { + _ = ResolveStorageGuestImage(texture); + } + } + + for (var index = 0; index < draw.Textures.Count; index++) + { + resources.Textures[index] = ResolveTextureResource(draw.Textures[index]); + } + + for (var index = 0; index < draw.GlobalMemoryBuffers.Count; index++) + { + resources.GlobalMemoryBuffers[index] = + CreateGlobalBufferResource(draw.GlobalMemoryBuffers[index]); + } + + for (var index = 0; index < draw.VertexBuffers.Count; index++) + { + resources.VertexBuffers[index] = + CreateVertexBufferResource(draw.VertexBuffers[index]); + } + + if (draw.IndexBuffer is { Data.Length: > 0 } indexBuffer) + { + resources.IndexBuffer = CreateHostBuffer( + indexBuffer.Data, + BufferUsageFlags.IndexBufferBit, + out resources.IndexMemory); + resources.Index32Bit = indexBuffer.Is32Bit; + } + + CreateTranslatedDescriptorResources( + resources, + ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit); + CreateTranslatedPipeline(resources, vertexSpirv, draw.PixelSpirv, renderPass, extent); + return resources; + } + catch + { + DestroyTranslatedDrawResources(resources); + throw; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private TranslatedDrawResources CreateComputeDispatchResources( + VulkanComputeGuestDispatch dispatch) + { + var traceResources = dispatch.Textures.Count >= 8; + if (traceResources) + { + TraceVulkanShader( + $"vk.compute_resources begin groups={dispatch.GroupCountX}x" + + $"{dispatch.GroupCountY}x{dispatch.GroupCountZ} textures={dispatch.Textures.Count}"); + } + + var resources = new TranslatedDrawResources + { + DebugName = BuildComputeDebugName(dispatch), + Textures = new TextureResource[dispatch.Textures.Count], + GlobalMemoryBuffers = + new GlobalBufferResource[dispatch.GlobalMemoryBuffers.Count], + }; + + try + { + for (var index = 0; index < dispatch.Textures.Count; index++) + { + var texture = dispatch.Textures[index]; + if (texture.IsStorage) + { + if (traceResources) + { + TraceVulkanShader( + $"vk.compute_resources storage[{index}] begin " + + $"addr=0x{texture.Address:X16} fmt={texture.Format} " + + $"size={texture.Width}x{texture.Height} " + + $"mips={texture.MipLevels} level={texture.MipLevel}"); + } + + _ = ResolveStorageImageResource(texture); + if (traceResources) + { + TraceVulkanShader($"vk.compute_resources storage[{index}] ready"); + } + } + } + + if (traceResources) + { + TraceVulkanShader("vk.compute_resources resolve begin"); + } + + for (var index = 0; index < dispatch.Textures.Count; index++) + { + resources.Textures[index] = + ResolveTextureResource(dispatch.Textures[index]); + } + + if (traceResources) + { + TraceVulkanShader("vk.compute_resources resolve ready"); + } + + for (var index = 0; index < dispatch.GlobalMemoryBuffers.Count; index++) + { + resources.GlobalMemoryBuffers[index] = + CreateGlobalBufferResource(dispatch.GlobalMemoryBuffers[index]); + } + + if (traceResources) + { + TraceVulkanShader("vk.compute_resources descriptors begin"); + } + + CreateTranslatedDescriptorResources(resources, ShaderStageFlags.ComputeBit); + if (traceResources) + { + TraceVulkanShader("vk.compute_resources descriptors ready"); + } + + if (traceResources) + { + TraceVulkanShader( + $"vk.compute_resources pipeline begin " + + $"cs=0x{dispatch.ShaderAddress:X16} " + + $"spirv={dispatch.ComputeSpirv.Length} " + + $"textures={resources.Textures.Length} " + + $"globals={resources.GlobalMemoryBuffers.Length}"); + } + + CreateComputePipeline(resources, dispatch.ComputeSpirv); + if (traceResources) + { + TraceVulkanShader("vk.compute_resources pipeline ready"); + } + + return resources; + } + catch + { + DestroyTranslatedDrawResources(resources); + throw; + } + } + + private static bool TryCompileFullscreenVertexShader( + uint attributeCount, + out byte[] spirv, + out string error) + { + spirv = []; + error = string.Empty; + if (attributeCount > 32) + { + error = $"too many interpolated attributes: {attributeCount}"; + return false; + } + + spirv = SpirvFixedShaders.CreateFullscreenVertex(attributeCount); + return true; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void CreateTranslatedDescriptorResources( + TranslatedDrawResources resources, + ShaderStageFlags stageFlags) + { + var textureCount = resources.Textures.Length; + var sampledImageCount = resources.Textures.Count(texture => !texture.IsStorage); + var storageImageCount = textureCount - sampledImageCount; + var globalBufferCount = resources.GlobalMemoryBuffers.Length; + var bindingCount = textureCount + (globalBufferCount == 0 ? 0 : 1); + if (bindingCount == 0) + { + var layoutInfo = new PipelineLayoutCreateInfo + { + SType = StructureType.PipelineLayoutCreateInfo, + }; + PipelineLayout pipelineLayout; + Check( + _vk.CreatePipelineLayout(_device, &layoutInfo, null, out pipelineLayout), + "vkCreatePipelineLayout"); + resources.PipelineLayout = pipelineLayout; + return; + } + + var bindings = new DescriptorSetLayoutBinding[bindingCount]; + var bindingOffset = 0; + if (globalBufferCount != 0) + { + bindings[bindingOffset++] = new DescriptorSetLayoutBinding + { + Binding = 0, + DescriptorType = DescriptorType.StorageBuffer, + DescriptorCount = (uint)globalBufferCount, + StageFlags = stageFlags, + }; + } + + for (var index = 0; index < textureCount; index++) + { + bindings[bindingOffset + index] = new DescriptorSetLayoutBinding + { + Binding = (uint)(index + 1), + DescriptorType = resources.Textures[index].IsStorage + ? DescriptorType.StorageImage + : DescriptorType.CombinedImageSampler, + DescriptorCount = 1, + StageFlags = stageFlags, + }; + } + + fixed (DescriptorSetLayoutBinding* bindingPointer = bindings) + { + var layoutInfo = new DescriptorSetLayoutCreateInfo + { + SType = StructureType.DescriptorSetLayoutCreateInfo, + BindingCount = (uint)bindings.Length, + PBindings = bindingPointer, + }; + DescriptorSetLayout descriptorSetLayout; + Check( + _vk.CreateDescriptorSetLayout( + _device, + &layoutInfo, + null, + out descriptorSetLayout), + "vkCreateDescriptorSetLayout"); + resources.DescriptorSetLayout = descriptorSetLayout; + } + + var setLayout = resources.DescriptorSetLayout; + var pipelineLayoutInfo = new PipelineLayoutCreateInfo + { + SType = StructureType.PipelineLayoutCreateInfo, + SetLayoutCount = 1, + PSetLayouts = &setLayout, + }; + PipelineLayout translatedPipelineLayout; + Check( + _vk.CreatePipelineLayout( + _device, + &pipelineLayoutInfo, + null, + out translatedPipelineLayout), + "vkCreatePipelineLayout"); + resources.PipelineLayout = translatedPipelineLayout; + + var poolSizes = new DescriptorPoolSize[ + (sampledImageCount == 0 ? 0 : 1) + + (storageImageCount == 0 ? 0 : 1) + + (globalBufferCount == 0 ? 0 : 1)]; + var poolSizeIndex = 0; + if (sampledImageCount != 0) + { + poolSizes[poolSizeIndex++] = new DescriptorPoolSize + { + Type = DescriptorType.CombinedImageSampler, + DescriptorCount = (uint)sampledImageCount, + }; + } + + if (storageImageCount != 0) + { + poolSizes[poolSizeIndex++] = new DescriptorPoolSize + { + Type = DescriptorType.StorageImage, + DescriptorCount = (uint)storageImageCount, + }; + } + + if (globalBufferCount != 0) + { + poolSizes[poolSizeIndex] = new DescriptorPoolSize + { + Type = DescriptorType.StorageBuffer, + DescriptorCount = (uint)globalBufferCount, + }; + } + + fixed (DescriptorPoolSize* poolSizePointer = poolSizes) + { + var poolInfo = new DescriptorPoolCreateInfo + { + SType = StructureType.DescriptorPoolCreateInfo, + MaxSets = 1, + PoolSizeCount = (uint)poolSizes.Length, + PPoolSizes = poolSizePointer, + }; + DescriptorPool descriptorPool; + Check( + _vk.CreateDescriptorPool( + _device, + &poolInfo, + null, + out descriptorPool), + "vkCreateDescriptorPool"); + resources.DescriptorPool = descriptorPool; + } + + var allocateInfo = new DescriptorSetAllocateInfo + { + SType = StructureType.DescriptorSetAllocateInfo, + DescriptorPool = resources.DescriptorPool, + DescriptorSetCount = 1, + PSetLayouts = &setLayout, + }; + DescriptorSet descriptorSet; + Check( + _vk.AllocateDescriptorSets(_device, &allocateInfo, out descriptorSet), + "vkAllocateDescriptorSets"); + resources.DescriptorSet = descriptorSet; + + var imageInfos = new DescriptorImageInfo[textureCount]; + var bufferInfos = new DescriptorBufferInfo[globalBufferCount]; + var writes = new WriteDescriptorSet[bindingCount]; + fixed (DescriptorImageInfo* imageInfoPointer = imageInfos) + fixed (DescriptorBufferInfo* bufferInfoPointer = bufferInfos) + fixed (WriteDescriptorSet* writePointer = writes) + { + var writeIndex = 0; + if (globalBufferCount != 0) + { + for (var index = 0; index < globalBufferCount; index++) + { + bufferInfoPointer[index] = new DescriptorBufferInfo + { + Buffer = resources.GlobalMemoryBuffers[index].Buffer, + Offset = 0, + Range = resources.GlobalMemoryBuffers[index].Size, + }; + } + + writePointer[writeIndex++] = new WriteDescriptorSet + { + SType = StructureType.WriteDescriptorSet, + DstSet = resources.DescriptorSet, + DstBinding = 0, + DescriptorCount = (uint)globalBufferCount, + DescriptorType = DescriptorType.StorageBuffer, + PBufferInfo = bufferInfoPointer, + }; + } + + for (var index = 0; index < textureCount; index++) + { + var isStorage = resources.Textures[index].IsStorage; + if (!isStorage && + resources.Textures[index].Sampler.Handle == 0) + { + resources.Textures[index].Sampler = + CreateSampler(resources.Textures[index].SamplerState); + } + + imageInfoPointer[index] = new DescriptorImageInfo + { + Sampler = isStorage ? default : resources.Textures[index].Sampler, + ImageView = resources.Textures[index].View, + ImageLayout = isStorage || + resources.Textures[index].GuestImage is { } guestImage && + resources.Textures.Any( + texture => + texture.IsStorage && + texture.GuestImage == guestImage) + ? ImageLayout.General + : ImageLayout.ShaderReadOnlyOptimal, + }; + writePointer[writeIndex++] = new WriteDescriptorSet + { + SType = StructureType.WriteDescriptorSet, + DstSet = resources.DescriptorSet, + DstBinding = (uint)(index + 1), + DescriptorCount = 1, + DescriptorType = isStorage + ? DescriptorType.StorageImage + : DescriptorType.CombinedImageSampler, + PImageInfo = &imageInfoPointer[index], + }; + } + + _vk.UpdateDescriptorSets( + _device, + (uint)bindingCount, + writePointer, + 0, + null); + } + } + + private void CreateTranslatedPipeline( + TranslatedDrawResources resources, + byte[] vertexSpirv, + byte[] fragmentSpirv, + RenderPass renderPass, + Extent2D extent) + { + var vertexModule = CreateShaderModule(vertexSpirv); + var fragmentModule = CreateShaderModule(fragmentSpirv); + var entryPoint = (byte*)SilkMarshal.StringToPtr("main"); + try + { + var shaderStages = stackalloc PipelineShaderStageCreateInfo[2]; + shaderStages[0] = new PipelineShaderStageCreateInfo + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.VertexBit, + Module = vertexModule, + PName = entryPoint, + }; + shaderStages[1] = new PipelineShaderStageCreateInfo + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.FragmentBit, + Module = fragmentModule, + PName = entryPoint, + }; + + var vertexBindingDescriptions = + new VertexInputBindingDescription[resources.VertexBuffers.Length]; + var vertexAttributeDescriptions = + new VertexInputAttributeDescription[resources.VertexBuffers.Length]; + for (var index = 0; index < resources.VertexBuffers.Length; index++) + { + var vertexBuffer = resources.VertexBuffers[index]; + vertexBindingDescriptions[index] = new VertexInputBindingDescription + { + Binding = (uint)index, + Stride = vertexBuffer.Stride == 0 + ? Math.Max(vertexBuffer.ComponentCount, 1) * sizeof(float) + : vertexBuffer.Stride, + InputRate = VertexInputRate.Vertex, + }; + vertexAttributeDescriptions[index] = new VertexInputAttributeDescription + { + Location = vertexBuffer.Location, + Binding = (uint)index, + Format = ToVkVertexFormat( + vertexBuffer.DataFormat, + vertexBuffer.NumberFormat, + vertexBuffer.ComponentCount), + Offset = 0, + }; + } + + fixed (VertexInputBindingDescription* vertexBindingPointerBase = vertexBindingDescriptions) + fixed (VertexInputAttributeDescription* vertexAttributePointerBase = vertexAttributeDescriptions) + { + var vertexInput = new PipelineVertexInputStateCreateInfo + { + SType = StructureType.PipelineVertexInputStateCreateInfo, + VertexBindingDescriptionCount = (uint)vertexBindingDescriptions.Length, + PVertexBindingDescriptions = vertexBindingDescriptions.Length == 0 + ? null + : vertexBindingPointerBase, + VertexAttributeDescriptionCount = (uint)vertexAttributeDescriptions.Length, + PVertexAttributeDescriptions = vertexAttributeDescriptions.Length == 0 + ? null + : vertexAttributePointerBase, + }; + var inputAssembly = new PipelineInputAssemblyStateCreateInfo + { + SType = StructureType.PipelineInputAssemblyStateCreateInfo, + Topology = resources.Topology, + }; + var viewport = new Viewport(0, 0, extent.Width, extent.Height, 0, 1); + var scissor = new Rect2D(new Offset2D(0, 0), extent); + var viewportState = new PipelineViewportStateCreateInfo + { + SType = StructureType.PipelineViewportStateCreateInfo, + ViewportCount = 1, + PViewports = &viewport, + ScissorCount = 1, + PScissors = &scissor, + }; + var rasterization = new PipelineRasterizationStateCreateInfo + { + SType = StructureType.PipelineRasterizationStateCreateInfo, + PolygonMode = PolygonMode.Fill, + CullMode = CullModeFlags.None, + FrontFace = FrontFace.CounterClockwise, + LineWidth = 1, + }; + var multisample = new PipelineMultisampleStateCreateInfo + { + SType = StructureType.PipelineMultisampleStateCreateInfo, + RasterizationSamples = SampleCountFlags.Count1Bit, + }; + var colorBlendAttachment = new PipelineColorBlendAttachmentState + { + BlendEnable = resources.Blend.Enable, + SrcColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorSrcFactor), + DstColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorDstFactor), + ColorBlendOp = ToVkBlendOp(resources.Blend.ColorFunc), + SrcAlphaBlendFactor = resources.Blend.SeparateAlphaBlend + ? ToVkBlendFactor(resources.Blend.AlphaSrcFactor) + : ToVkBlendFactor(resources.Blend.ColorSrcFactor), + DstAlphaBlendFactor = resources.Blend.SeparateAlphaBlend + ? ToVkBlendFactor(resources.Blend.AlphaDstFactor) + : ToVkBlendFactor(resources.Blend.ColorDstFactor), + AlphaBlendOp = resources.Blend.SeparateAlphaBlend + ? ToVkBlendOp(resources.Blend.AlphaFunc) + : ToVkBlendOp(resources.Blend.ColorFunc), + ColorWriteMask = + ToVkColorWriteMask(resources.Blend.WriteMask), + }; + var colorBlend = new PipelineColorBlendStateCreateInfo + { + SType = StructureType.PipelineColorBlendStateCreateInfo, + AttachmentCount = 1, + PAttachments = &colorBlendAttachment, + }; + var dynamicStateValues = stackalloc DynamicState[2]; + dynamicStateValues[0] = DynamicState.Viewport; + dynamicStateValues[1] = DynamicState.Scissor; + var dynamicState = new PipelineDynamicStateCreateInfo + { + SType = StructureType.PipelineDynamicStateCreateInfo, + DynamicStateCount = 2, + PDynamicStates = dynamicStateValues, + }; + var pipelineInfo = new GraphicsPipelineCreateInfo + { + SType = StructureType.GraphicsPipelineCreateInfo, + StageCount = 2, + PStages = shaderStages, + PVertexInputState = &vertexInput, + PInputAssemblyState = &inputAssembly, + PViewportState = &viewportState, + PRasterizationState = &rasterization, + PMultisampleState = &multisample, + PColorBlendState = &colorBlend, + PDynamicState = &dynamicState, + Layout = resources.PipelineLayout, + RenderPass = renderPass, + Subpass = 0, + }; + Pipeline pipeline; + Check( + _vk.CreateGraphicsPipelines( + _device, + default, + 1, + &pipelineInfo, + null, + out pipeline), + "vkCreateGraphicsPipelines(translated)"); + resources.Pipeline = pipeline; + SetDebugName( + ObjectType.Pipeline, + pipeline.Handle, + $"SharpEmu graphics ps={fragmentSpirv.Length}b attrs={resources.Textures.Length}"); + } + } + finally + { + SilkMarshal.Free((nint)entryPoint); + _vk.DestroyShaderModule(_device, fragmentModule, null); + _vk.DestroyShaderModule(_device, vertexModule, null); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void CreateComputePipeline( + TranslatedDrawResources resources, + byte[] computeSpirv) + { + if (_computePipelines.TryGetValue(computeSpirv, out var cachedPipeline)) + { + resources.Pipeline = cachedPipeline; + resources.PipelineCached = true; + return; + } + + var computeModule = CreateShaderModule(computeSpirv); + var entryPoint = (byte*)SilkMarshal.StringToPtr("main"); + try + { + var stage = new PipelineShaderStageCreateInfo + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.ComputeBit, + Module = computeModule, + PName = entryPoint, + }; + var pipelineInfo = new ComputePipelineCreateInfo + { + SType = StructureType.ComputePipelineCreateInfo, + Stage = stage, + Layout = resources.PipelineLayout, + }; + Pipeline pipeline; + Check( + _vk.CreateComputePipelines( + _device, + default, + 1, + &pipelineInfo, + null, + out pipeline), + "vkCreateComputePipelines(translated)"); + resources.Pipeline = pipeline; + resources.PipelineCached = true; + SetDebugName( + ObjectType.Pipeline, + pipeline.Handle, + $"SharpEmu compute cs={computeSpirv.Length}b"); + _computePipelines.Add(computeSpirv, pipeline); + } + finally + { + SilkMarshal.Free((nint)entryPoint); + _vk.DestroyShaderModule(_device, computeModule, null); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private TextureResource ResolveTextureResource(VulkanGuestDrawTexture texture) + { + if (texture.IsStorage) + { + return ResolveStorageImageResource(texture); + } + + var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); + if (texture.Address != 0 && + _guestImages.TryGetValue(texture.Address, out var guestImage) && + IsCompatibleGuestImageAlias(texture, guestImage) && + IsCompatibleViewFormat(guestImage.Format, vkFormat) && + TryGetOrCreateGuestImageView( + guestImage, + vkFormat, + mipLevel: 0, + levelCount: guestImage.MipLevels, + dstSelect: texture.DstSelect, + out var view)) + { + if (_tracedTextureCacheHits.Add( + (texture.Address, texture.Width, texture.Height, vkFormat))) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.texture_cache_hit addr=0x{texture.Address:X16} " + + $"size={texture.Width}x{texture.Height} " + + $"image_format={guestImage.Format} view_format={vkFormat}"); + } + + if (guestImage.Width != texture.Width || + guestImage.Height != texture.Height) + { + TraceVulkanShader( + $"vk.texture_cache_alias addr=0x{texture.Address:X16} " + + $"texture={texture.Width}x{texture.Height} " + + $"image={guestImage.Width}x{guestImage.Height} " + + $"tile={texture.TileMode} format={vkFormat}"); + } + + return new TextureResource + { + Address = texture.Address, + Image = guestImage.Image, + View = view, + Width = guestImage.Width, + Height = guestImage.Height, + RowLength = guestImage.Width, + DstSelect = texture.DstSelect, + SamplerState = texture.Sampler, + GuestImage = guestImage, + }; + } + + return CreateTextureResource(texture); + } + + private static bool IsCompatibleGuestImageAlias( + VulkanGuestDrawTexture texture, + GuestImageResource guestImage) + { + if (guestImage.Width == texture.Width && + guestImage.Height == texture.Height) + { + return true; + } + + if (texture.TileMode == 0 || + texture.Width == 0 || + texture.Height == 0) + { + return false; + } + + return texture.Width <= guestImage.Width && + texture.Height <= guestImage.Height; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private TextureResource ResolveStorageImageResource(VulkanGuestDrawTexture texture) + { + if (texture.Address == 0) + { + return CreateStorageScratchResource(texture); + } + + var guestImage = ResolveStorageGuestImage(texture); + var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); + var view = GetOrCreateGuestImageView( + guestImage, + vkFormat, + texture.MipLevel, + levelCount: 1); + var resource = new TextureResource + { + Address = texture.Address, + Image = guestImage.Image, + View = view, + Width = guestImage.Width, + Height = guestImage.Height, + RowLength = guestImage.Width, + DstSelect = texture.DstSelect, + IsStorage = true, + SamplerState = texture.Sampler, + GuestImage = guestImage, + }; + + if (!guestImage.Initialized && + !guestImage.InitialUploadPending && + texture.MipLevel == 0) + { + var expectedSize = GetTextureByteCount( + texture.Format, + texture.Width, + texture.Height); + if ((ulong)texture.RgbaPixels.Length == expectedSize && + texture.RgbaPixels.AsSpan().IndexOfAnyExcept((byte)0) >= 0) + { + resource.StagingBuffer = CreateBuffer( + expectedSize, + BufferUsageFlags.TransferSrcBit, + MemoryPropertyFlags.HostVisibleBit | + MemoryPropertyFlags.HostCoherentBit, + out resource.StagingMemory); + + void* mapped; + Check( + _vk.MapMemory( + _device, + resource.StagingMemory, + 0, + expectedSize, + 0, + &mapped), + "vkMapMemory(storage texture)"); + fixed (byte* source = texture.RgbaPixels) + { + System.Buffer.MemoryCopy( + source, + mapped, + texture.RgbaPixels.Length, + texture.RgbaPixels.Length); + } + + _vk.UnmapMemory(_device, resource.StagingMemory); + resource.NeedsUpload = true; + guestImage.InitialUploadPending = true; + TraceVulkanShader( + $"vk.storage_upload addr=0x{texture.Address:X16} " + + $"size={texture.Width}x{texture.Height} bytes={expectedSize}"); + } + } + + return resource; + } + + private TextureResource CreateStorageScratchResource(VulkanGuestDrawTexture texture) + { + var width = Math.Max(texture.Width, 1); + var height = Math.Max(texture.Height, 1); + var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); + var imageInfo = new ImageCreateInfo + { + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.Type2D, + Format = vkFormat, + Extent = new Extent3D(width, height, 1), + MipLevels = 1, + ArrayLayers = 1, + Samples = SampleCountFlags.Count1Bit, + Tiling = ImageTiling.Optimal, + Usage = + ImageUsageFlags.SampledBit | + ImageUsageFlags.StorageBit | + ImageUsageFlags.TransferSrcBit | + ImageUsageFlags.TransferDstBit, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + }; + Check( + _vk.CreateImage(_device, &imageInfo, null, out var image), + "vkCreateImage(storage scratch)"); + _vk.GetImageMemoryRequirements(_device, image, out var requirements); + var allocationInfo = new MemoryAllocateInfo { SType = StructureType.MemoryAllocateInfo, AllocationSize = requirements.Size, MemoryTypeIndex = FindMemoryType( requirements.MemoryTypeBits, - MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit), + MemoryPropertyFlags.DeviceLocalBit), }; - Check(_vk.AllocateMemory(_device, &memoryInfo, null, out _stagingMemory), "vkAllocateMemory"); - Check(_vk.BindBufferMemory(_device, _stagingBuffer, _stagingMemory, 0), "vkBindBufferMemory"); + Check( + _vk.AllocateMemory(_device, &allocationInfo, null, out var memory), + "vkAllocateMemory(storage scratch)"); + Check( + _vk.BindImageMemory(_device, image, memory, 0), + "vkBindImageMemory(storage scratch)"); + + var viewInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = image, + ViewType = ImageViewType.Type2D, + Format = vkFormat, + Components = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity), + SubresourceRange = ColorSubresourceRange(), + }; + Check( + _vk.CreateImageView(_device, &viewInfo, null, out var view), + "vkCreateImageView(storage scratch)"); + SetDebugName(ObjectType.Image, image.Handle, $"SharpEmu scratch storage {width}x{height} {vkFormat}"); + SetDebugName(ObjectType.ImageView, view.Handle, $"SharpEmu scratch storage {width}x{height} {vkFormat} view"); + + var guestImage = new GuestImageResource + { + Address = 0, + Width = width, + Height = height, + MipLevels = 1, + Format = vkFormat, + Image = image, + Memory = memory, + View = view, + }; + + return new TextureResource + { + Address = 0, + Image = image, + ImageMemory = memory, + View = view, + Width = width, + Height = height, + RowLength = width, + DstSelect = texture.DstSelect, + OwnsStorage = true, + IsStorage = true, + SamplerState = texture.Sampler, + GuestImage = guestImage, + }; + } + + private GuestImageResource ResolveStorageGuestImage(VulkanGuestDrawTexture texture) + { + if (texture.Address == 0) + { + throw new InvalidOperationException("Storage image has no guest address."); + } + + var format = GetTextureFormat(texture.Format, texture.NumberType); + var guestImage = GetOrCreateGuestImage( + new VulkanGuestRenderTarget( + texture.Address, + texture.Width, + texture.Height, + texture.Format, + texture.NumberType, + texture.MipLevels), + format); + if (texture.MipLevel >= guestImage.MipLevels) + { + throw new InvalidOperationException( + $"Storage mip {texture.MipLevel} exceeds image mip count {guestImage.MipLevels}."); + } + + return guestImage; + } + + private TextureResource CreateTextureResource(VulkanGuestDrawTexture texture) + { + var width = Math.Max(texture.Width, 1); + var height = Math.Max(texture.Height, 1); + var rowLength = texture.TileMode == 0 + ? Math.Max(texture.Pitch, width) + : width; + var vkFormat = GetTextureFormat(texture.Format, texture.NumberType); + + var expectedSize = GetTextureByteCount(texture.Format, rowLength, height); + if (_tracedTextureUploads.Add((texture.Address, width, height, vkFormat))) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.texture addr=0x{texture.Address:X16} " + + $"fmt={texture.Format} num={texture.NumberType} vk={vkFormat} " + + $"size={width}x{height} row={rowLength} tile={texture.TileMode} " + + $"dst=0x{texture.DstSelect:X3} " + + $"bytes={texture.RgbaPixels.Length} expected={expectedSize}"); + } + var pixels = texture.RgbaPixels.Length == (int)expectedSize + ? texture.RgbaPixels + : CreateFallbackTexturePixels(texture.Format, rowLength, height, expectedSize); + DumpTextureUpload(texture, pixels, rowLength, width, height); + var uploadPixels = texture.Format == 13 + ? ExpandRgb32Pixels(pixels) + : pixels; + var uploadSize = (ulong)uploadPixels.Length; + + var stagingBuffer = CreateBuffer( + uploadSize, + BufferUsageFlags.TransferSrcBit, + MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit, + out var stagingMemory); + void* mapped; + Check(_vk.MapMemory(_device, stagingMemory, 0, uploadSize, 0, &mapped), "vkMapMemory(texture)"); + fixed (byte* source = uploadPixels) + { + System.Buffer.MemoryCopy( + source, + mapped, + uploadPixels.Length, + uploadPixels.Length); + } + _vk.UnmapMemory(_device, stagingMemory); + + var imageInfo = new ImageCreateInfo + { + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.Type2D, + Format = vkFormat, + Extent = new Extent3D(width, height, 1), + MipLevels = 1, + ArrayLayers = 1, + Samples = SampleCountFlags.Count1Bit, + Tiling = ImageTiling.Optimal, + Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + }; + Check(_vk.CreateImage(_device, &imageInfo, null, out var image), "vkCreateImage(texture)"); + _vk.GetImageMemoryRequirements(_device, image, out var imageRequirements); + var memoryInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = imageRequirements.Size, + MemoryTypeIndex = FindMemoryType( + imageRequirements.MemoryTypeBits, + MemoryPropertyFlags.DeviceLocalBit), + }; + Check(_vk.AllocateMemory(_device, &memoryInfo, null, out var imageMemory), "vkAllocateMemory(texture)"); + Check(_vk.BindImageMemory(_device, image, imageMemory, 0), "vkBindImageMemory(texture)"); + + var viewInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = image, + ViewType = ImageViewType.Type2D, + Format = vkFormat, + Components = ToVkComponentMapping(texture.DstSelect), + SubresourceRange = ColorSubresourceRange(), + }; + Check(_vk.CreateImageView(_device, &viewInfo, null, out var view), "vkCreateImageView(texture)"); + var debugName = TextureDebugName(texture, vkFormat); + SetDebugName(ObjectType.Buffer, stagingBuffer.Handle, $"{debugName} staging"); + SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image"); + SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view"); + return new TextureResource + { + Address = texture.Address, + StagingBuffer = stagingBuffer, + StagingMemory = stagingMemory, + Image = image, + ImageMemory = imageMemory, + View = view, + Width = width, + Height = height, + RowLength = rowLength, + DstSelect = texture.DstSelect, + NeedsUpload = true, + OwnsStorage = true, + SamplerState = texture.Sampler, + }; + } + + private void DumpTextureUpload( + VulkanGuestDrawTexture texture, + byte[] pixels, + uint rowLength, + uint width, + uint height) + { + if (!string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"), + "1", + StringComparison.Ordinal) || + texture.IsFallback || + texture.IsStorage || + GetTextureBytesPerPixel(texture.Format) != 4 || + width == 0 || + height == 0 || + !_dumpedTextures.Add((texture.Address, width, height, texture.Format))) + { + return; + } + + var rowBytes = checked((int)rowLength * 4); + var visibleRowBytes = checked((int)width * 4); + if (pixels.Length < checked(rowBytes * (int)height)) + { + return; + } + + var directory = Path.Combine(AppContext.BaseDirectory, "texture-dumps"); + Directory.CreateDirectory(directory); + var path = Path.Combine( + directory, + $"tex-{texture.Address:X16}-{width}x{height}-fmt{texture.Format}-row{rowLength}.bmp"); + WriteRgbaBmp(path, pixels, rowBytes, visibleRowBytes, (int)width, (int)height); + } + + private static void WriteRgbaBmp( + string path, + byte[] rgba, + int sourceRowBytes, + int visibleRowBytes, + int width, + int height) + { + const int fileHeaderSize = 14; + const int infoHeaderSize = 40; + const int bytesPerPixel = 4; + var pixelBytes = checked(width * height * bytesPerPixel); + var fileSize = fileHeaderSize + infoHeaderSize + pixelBytes; + var output = new byte[fileSize]; + + output[0] = (byte)'B'; + output[1] = (byte)'M'; + WriteUInt32(output, 2, (uint)fileSize); + WriteUInt32(output, 10, fileHeaderSize + infoHeaderSize); + WriteUInt32(output, 14, infoHeaderSize); + WriteInt32(output, 18, width); + WriteInt32(output, 22, -height); + WriteUInt16(output, 26, 1); + WriteUInt16(output, 28, 32); + WriteUInt32(output, 34, (uint)pixelBytes); + + var destinationOffset = fileHeaderSize + infoHeaderSize; + for (var y = 0; y < height; y++) + { + var sourceOffset = y * sourceRowBytes; + for (var x = 0; x < visibleRowBytes; x += bytesPerPixel) + { + var destination = destinationOffset + y * visibleRowBytes + x; + output[destination + 0] = rgba[sourceOffset + x + 2]; + output[destination + 1] = rgba[sourceOffset + x + 1]; + output[destination + 2] = rgba[sourceOffset + x + 0]; + output[destination + 3] = rgba[sourceOffset + x + 3]; + } + } + + File.WriteAllBytes(path, output); + } + + private static void WriteUInt16(byte[] output, int offset, ushort value) + { + output[offset + 0] = (byte)value; + output[offset + 1] = (byte)(value >> 8); + } + + private static void WriteUInt32(byte[] output, int offset, uint value) + { + output[offset + 0] = (byte)value; + output[offset + 1] = (byte)(value >> 8); + output[offset + 2] = (byte)(value >> 16); + output[offset + 3] = (byte)(value >> 24); + } + + private static void WriteInt32(byte[] output, int offset, int value) => + WriteUInt32(output, offset, unchecked((uint)value)); + + private Sampler CreateSampler(VulkanGuestSampler sampler) + { + var minLod = DecodeSamplerMipFilter(sampler) == 0 + ? 0f + : DecodeSamplerMinLod(sampler); + var maxLod = DecodeSamplerMipFilter(sampler) == 0 + ? 0f + : DecodeSamplerMaxLod(sampler); + var samplerInfo = new SamplerCreateInfo + { + SType = StructureType.SamplerCreateInfo, + MagFilter = ToVkFilter(DecodeSamplerMagFilter(sampler)), + MinFilter = ToVkFilter(DecodeSamplerMinFilter(sampler)), + MipmapMode = ToVkMipFilter(DecodeSamplerMipFilter(sampler)), + AddressModeU = ToVkSamplerAddressMode(DecodeSamplerClampX(sampler)), + AddressModeV = ToVkSamplerAddressMode(DecodeSamplerClampY(sampler)), + AddressModeW = ToVkSamplerAddressMode(DecodeSamplerClampZ(sampler)), + MipLodBias = DecodeSamplerLodBias(sampler), + CompareEnable = DecodeSamplerDepthCompare(sampler) != 0, + CompareOp = ToVkCompareOp(DecodeSamplerDepthCompare(sampler)), + MinLod = minLod, + MaxLod = Math.Max(minLod, maxLod), + BorderColor = ToVkBorderColor(DecodeSamplerBorderColor(sampler)), + }; + Sampler vkSampler; + Check( + _vk.CreateSampler(_device, &samplerInfo, null, out vkSampler), + "vkCreateSampler(texture)"); + return vkSampler; + } + + private static ComponentMapping ToVkComponentMapping(uint dstSelect) + { + if (dstSelect == 0) + { + dstSelect = 0xFAC; + } + + return new ComponentMapping( + ToVkComponentSwizzle(dstSelect & 0x7), + ToVkComponentSwizzle((dstSelect >> 3) & 0x7), + ToVkComponentSwizzle((dstSelect >> 6) & 0x7), + ToVkComponentSwizzle((dstSelect >> 9) & 0x7)); + } + + private static ComponentSwizzle ToVkComponentSwizzle(uint selector) => + selector switch + { + 0 => ComponentSwizzle.Zero, + 1 => ComponentSwizzle.One, + 4 => ComponentSwizzle.R, + 5 => ComponentSwizzle.G, + 6 => ComponentSwizzle.B, + 7 => ComponentSwizzle.A, + _ => ComponentSwizzle.Identity, + }; + + private static byte[] ExpandRgb32Pixels(byte[] pixels) + { + var texelCount = pixels.Length / 12; + var expanded = new byte[checked(texelCount * 16)]; + for (var texel = 0; texel < texelCount; texel++) + { + System.Buffer.BlockCopy(pixels, texel * 12, expanded, texel * 16, 12); + expanded[texel * 16 + 14] = 0x80; + expanded[texel * 16 + 15] = 0x3F; + } + + return expanded; + } + + private GlobalBufferResource CreateGlobalBufferResource( + VulkanGuestMemoryBuffer guestBuffer) + { + var buffer = CreateHostBuffer( + guestBuffer.Data, + BufferUsageFlags.StorageBufferBit, + out var memory); + var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint)); + + if (_tracedGlobalBuffers.Add((guestBuffer.BaseAddress, guestBuffer.Data.Length))) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " + + $"bytes={guestBuffer.Data.Length}"); + } + SetDebugName( + ObjectType.Buffer, + buffer.Handle, + $"SharpEmu global 0x{guestBuffer.BaseAddress:X16} {guestBuffer.Data.Length}b"); + + return new GlobalBufferResource + { + Buffer = buffer, + Memory = memory, + Size = size, + }; + } + + private VertexBufferResource CreateVertexBufferResource( + VulkanGuestVertexBuffer guestBuffer) + { + var buffer = CreateHostBuffer( + guestBuffer.Data, + BufferUsageFlags.VertexBufferBit, + out var memory); + var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint)); + SetDebugName( + ObjectType.Buffer, + buffer.Handle, + $"SharpEmu vertex loc{guestBuffer.Location} " + + $"0x{guestBuffer.BaseAddress:X16} {guestBuffer.Data.Length}b"); + if (_tracedVertexBufferCount++ < 64) + { + TraceVulkanShader( + $"vk.vertex_buffer loc={guestBuffer.Location} " + + $"base=0x{guestBuffer.BaseAddress:X16} stride={guestBuffer.Stride} " + + $"offset={guestBuffer.OffsetBytes} comps={guestBuffer.ComponentCount} " + + $"fmt={guestBuffer.DataFormat}/num={guestBuffer.NumberFormat} " + + $"bytes={guestBuffer.Data.Length}"); + } + + return new VertexBufferResource + { + Buffer = buffer, + Memory = memory, + Size = size, + Location = guestBuffer.Location, + ComponentCount = guestBuffer.ComponentCount, + DataFormat = guestBuffer.DataFormat, + NumberFormat = guestBuffer.NumberFormat, + Stride = guestBuffer.Stride, + OffsetBytes = guestBuffer.OffsetBytes, + }; + } + + private VkBuffer CreateHostBuffer( + ReadOnlySpan data, + BufferUsageFlags usage, + out DeviceMemory memory) + { + var size = (ulong)Math.Max(data.Length, sizeof(uint)); + var buffer = CreateBuffer( + size, + usage, + MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit, + out memory); + void* mapped; + Check(_vk.MapMemory(_device, memory, 0, size, 0, &mapped), "vkMapMemory(host)"); + try + { + fixed (byte* source = data) + { + System.Buffer.MemoryCopy( + source, + mapped, + checked((long)size), + data.Length); + } + } + finally + { + _vk.UnmapMemory(_device, memory); + } + + return buffer; + } + + private static PrimitiveTopology GetPrimitiveTopology(uint primitiveType) => + primitiveType switch + { + 1 => PrimitiveTopology.PointList, + 2 => PrimitiveTopology.LineList, + 3 => PrimitiveTopology.LineStrip, + 5 => PrimitiveTopology.TriangleFan, + 6 => PrimitiveTopology.TriangleStrip, + GuestPrimitiveRectList => PrimitiveTopology.TriangleStrip, + _ => PrimitiveTopology.TriangleList, + }; + + private static Format ToVkVertexFormat( + uint dataFormat, + uint numberFormat, + uint componentCount) => + (dataFormat, numberFormat) switch + { + (1, 0) => Format.R8Unorm, + (1, 1) => Format.R8SNorm, + (1, 4) => Format.R8Uint, + (1, 5) => Format.R8Sint, + (1, 9) => Format.R8Srgb, + (2, 0) => Format.R16Unorm, + (2, 1) => Format.R16SNorm, + (2, 4) => Format.R16Uint, + (2, 5) => Format.R16Sint, + (2, 7) => Format.R16Sfloat, + (3, 0) => Format.R8G8Unorm, + (3, 1) => Format.R8G8SNorm, + (3, 4) => Format.R8G8Uint, + (3, 5) => Format.R8G8Sint, + (3, 9) => Format.R8G8Srgb, + (4, 4) => Format.R32Uint, + (4, 5) => Format.R32Sint, + (4, 7) => Format.R32Sfloat, + (5, 0) => Format.R16G16Unorm, + (5, 1) => Format.R16G16SNorm, + (5, 2) => Format.R16G16Uscaled, + (5, 3) => Format.R16G16Sscaled, + (5, 4) => Format.R16G16Uint, + (5, 5) => Format.R16G16Sint, + (5, 7) => Format.R16G16Sfloat, + (6, 7) => Format.B10G11R11UfloatPack32, + (7, 7) => Format.B10G11R11UfloatPack32, + (8, 0) => Format.A2B10G10R10UnormPack32, + (8, 1) => Format.A2B10G10R10SNormPack32, + (8, 2) => Format.A2B10G10R10UscaledPack32, + (8, 3) => Format.A2B10G10R10SscaledPack32, + (8, 4) => Format.A2B10G10R10UintPack32, + (8, 5) => Format.A2B10G10R10SintPack32, + (9, 0) => Format.A2R10G10B10UnormPack32, + (9, 1) => Format.A2R10G10B10SNormPack32, + (9, 2) => Format.A2R10G10B10UscaledPack32, + (9, 3) => Format.A2R10G10B10SscaledPack32, + (9, 4) => Format.A2R10G10B10UintPack32, + (9, 5) => Format.A2R10G10B10SintPack32, + (10, 0) => Format.R8G8B8A8Unorm, + (10, 1) => Format.R8G8B8A8SNorm, + (10, 2) => Format.R8G8B8A8Uscaled, + (10, 3) => Format.R8G8B8A8Sscaled, + (10, 4) => Format.R8G8B8A8Uint, + (10, 5) => Format.R8G8B8A8Sint, + (10, 9) => Format.R8G8B8A8Srgb, + (11, 4) => Format.R32G32Uint, + (11, 5) => Format.R32G32Sint, + (11, 7) => Format.R32G32Sfloat, + (12, 0) => Format.R16G16B16A16Unorm, + (12, 1) => Format.R16G16B16A16SNorm, + (12, 2) => Format.R16G16B16A16Uscaled, + (12, 3) => Format.R16G16B16A16Sscaled, + (12, 4) => Format.R16G16B16A16Uint, + (12, 5) => Format.R16G16B16A16Sint, + (12, 6) => Format.R16G16B16A16SNorm, + (12, 7) => Format.R16G16B16A16Sfloat, + (13, 4) => Format.R32G32B32Uint, + (13, 5) => Format.R32G32B32Sint, + (13, 7) => Format.R32G32B32Sfloat, + (14, 4) => Format.R32G32B32A32Uint, + (14, 5) => Format.R32G32B32A32Sint, + (14, 7) => Format.R32G32B32A32Sfloat, + (16, 0) => Format.B5G6R5UnormPack16, + (17, 0) => Format.R5G5B5A1UnormPack16, + (19, 0) => Format.R4G4B4A4UnormPack16, + (34, 7) => Format.E5B9G9R9UfloatPack32, + _ => ToVkFloatVertexFormat(componentCount), + }; + + private static Format ToVkFloatVertexFormat(uint componentCount) => + componentCount switch + { + 1 => Format.R32Sfloat, + 2 => Format.R32G32Sfloat, + 3 => Format.R32G32B32Sfloat, + 4 => Format.R32G32B32A32Sfloat, + _ => Format.R32Sfloat, + }; + + private static ulong GetVertexBindingOffset(VertexBufferResource vertexBuffer) + { + if (vertexBuffer.OffsetBytes < vertexBuffer.Size) + { + return vertexBuffer.OffsetBytes; + } + + TraceVulkanShader( + $"vk.vertex_offset_oob loc={vertexBuffer.Location} " + + $"offset={vertexBuffer.OffsetBytes} size={vertexBuffer.Size}"); + return 0; + } + + private static uint GetDrawVertexCount( + uint primitiveType, + uint vertexCount, + VulkanGuestIndexBuffer? indexBuffer) + { + if (primitiveType == GuestPrimitiveRectList && indexBuffer is null) + { + return 4; + } + + return vertexCount; + } + + private static BlendFactor ToVkBlendFactor(uint factor) => + factor switch + { + 0 => BlendFactor.Zero, + 1 => BlendFactor.One, + 2 => BlendFactor.SrcColor, + 3 => BlendFactor.OneMinusSrcColor, + 4 => BlendFactor.SrcAlpha, + 5 => BlendFactor.OneMinusSrcAlpha, + 6 => BlendFactor.DstAlpha, + 7 => BlendFactor.OneMinusDstAlpha, + 8 => BlendFactor.DstColor, + 9 => BlendFactor.OneMinusDstColor, + 10 => BlendFactor.SrcAlphaSaturate, + 13 => BlendFactor.ConstantColor, + 14 => BlendFactor.OneMinusConstantColor, + 15 => BlendFactor.Src1Color, + 16 => BlendFactor.OneMinusSrc1Color, + 17 => BlendFactor.Src1Alpha, + 18 => BlendFactor.OneMinusSrc1Alpha, + 19 => BlendFactor.ConstantAlpha, + 20 => BlendFactor.OneMinusConstantAlpha, + _ => BlendFactor.One, + }; + + private static BlendOp ToVkBlendOp(uint function) => + function switch + { + 0 => BlendOp.Add, + 1 => BlendOp.Subtract, + 2 => BlendOp.Min, + 3 => BlendOp.Max, + 4 => BlendOp.ReverseSubtract, + _ => BlendOp.Add, + }; + + private static uint DecodeSamplerClampX(VulkanGuestSampler sampler) => + sampler.Word0 & 0x7u; + + private static uint DecodeSamplerClampY(VulkanGuestSampler sampler) => + (sampler.Word0 >> 3) & 0x7u; + + private static uint DecodeSamplerClampZ(VulkanGuestSampler sampler) => + (sampler.Word0 >> 6) & 0x7u; + + private static uint DecodeSamplerDepthCompare(VulkanGuestSampler sampler) => + (sampler.Word0 >> 12) & 0x7u; + + private static float DecodeSamplerMinLod(VulkanGuestSampler sampler) => + (sampler.Word1 & 0xFFFu) / 256.0f; + + private static float DecodeSamplerMaxLod(VulkanGuestSampler sampler) => + ((sampler.Word1 >> 12) & 0xFFFu) / 256.0f; + + private static float DecodeSamplerLodBias(VulkanGuestSampler sampler) + { + var raw = sampler.Word2 & 0x3FFFu; + var signed = (short)((raw ^ 0x2000u) - 0x2000u); + return signed / 256.0f; + } + + private static uint DecodeSamplerMagFilter(VulkanGuestSampler sampler) => + (sampler.Word2 >> 20) & 0x3u; + + private static uint DecodeSamplerMinFilter(VulkanGuestSampler sampler) => + (sampler.Word2 >> 22) & 0x3u; + + private static uint DecodeSamplerMipFilter(VulkanGuestSampler sampler) => + (sampler.Word2 >> 26) & 0x3u; + + private static uint DecodeSamplerBorderColor(VulkanGuestSampler sampler) => + (sampler.Word3 >> 30) & 0x3u; + + private static SamplerAddressMode ToVkSamplerAddressMode(uint mode) => + mode switch + { + 0 => SamplerAddressMode.Repeat, + 1 => SamplerAddressMode.MirroredRepeat, + 2 => SamplerAddressMode.ClampToEdge, + 3 or 5 or 7 => SamplerAddressMode.MirrorClampToEdge, + 4 or 6 => SamplerAddressMode.ClampToBorder, + _ => SamplerAddressMode.ClampToEdge, + }; + + private static Filter ToVkFilter(uint filter) => + filter is 1 or 3 ? Filter.Linear : Filter.Nearest; + + private static SamplerMipmapMode ToVkMipFilter(uint filter) => + filter == 2 ? SamplerMipmapMode.Linear : SamplerMipmapMode.Nearest; + + private static CompareOp ToVkCompareOp(uint compare) => + compare switch + { + 1 => CompareOp.Less, + 2 => CompareOp.Equal, + 3 => CompareOp.LessOrEqual, + 4 => CompareOp.Greater, + 5 => CompareOp.NotEqual, + 6 => CompareOp.GreaterOrEqual, + 7 => CompareOp.Always, + _ => CompareOp.Never, + }; + + private static BorderColor ToVkBorderColor(uint color) => + color switch + { + 1 => BorderColor.FloatTransparentBlack, + 2 => BorderColor.FloatOpaqueWhite, + _ => BorderColor.FloatOpaqueBlack, + }; + + private static ColorComponentFlags ToVkColorWriteMask(uint mask) + { + var flags = default(ColorComponentFlags); + if ((mask & 1u) != 0) + { + flags |= ColorComponentFlags.RBit; + } + + if ((mask & 2u) != 0) + { + flags |= ColorComponentFlags.GBit; + } + + if ((mask & 4u) != 0) + { + flags |= ColorComponentFlags.BBit; + } + + if ((mask & 8u) != 0) + { + flags |= ColorComponentFlags.ABit; + } + + return flags; + } + + private static VulkanGuestRect ClampScissor(VulkanGuestRect? scissor, Extent2D extent) + { + if (scissor is not { } rect) + { + return new VulkanGuestRect(0, 0, extent.Width, extent.Height); + } + + var left = Math.Clamp(rect.X, 0, checked((int)extent.Width)); + var top = Math.Clamp(rect.Y, 0, checked((int)extent.Height)); + var right = Math.Clamp( + rect.X + checked((int)rect.Width), + left, + checked((int)extent.Width)); + var bottom = Math.Clamp( + rect.Y + checked((int)rect.Height), + top, + checked((int)extent.Height)); + return new VulkanGuestRect( + left, + top, + checked((uint)(right - left)), + checked((uint)(bottom - top))); + } + + private static Viewport ClampViewport(VulkanGuestViewport? viewport, Extent2D extent) + { + if (viewport is not { } rect) + { + return new Viewport(0, 0, extent.Width, extent.Height, 0, 1); + } + + var maxX = (float)extent.Width; + var maxY = (float)extent.Height; + var left = Math.Clamp(rect.X, 0f, maxX); + var right = Math.Clamp(rect.X + rect.Width, left, maxX); + var yOrigin = Math.Clamp(rect.Y, 0f, maxY); + var yEnd = Math.Clamp(rect.Y + rect.Height, 0f, maxY); + var minDepth = Math.Clamp(rect.MinDepth, 0f, 1f); + var maxDepth = Math.Clamp(rect.MaxDepth, minDepth, 1f); + return new Viewport( + left, + yOrigin, + right - left, + yEnd - yOrigin, + minDepth, + maxDepth); + } + + private static byte[] CreateFallbackTexturePixels(uint format, uint width, uint height, ulong expectedSize) + { + if (format is 9 or 10 or 56 or 62 or 64) + { + return CreateBlackFrame(width, height); + } + + return new byte[checked((int)expectedSize)]; + } + + private static ulong GetTextureBytesPerPixel(uint format) => + format switch + { + 1 => 1UL, + 2 => 2UL, + 3 => 2UL, + 4 => 4UL, + 5 => 4UL, + 6 => 4UL, + 7 => 4UL, + 9 => 4UL, + 10 => 4UL, + 11 => 8UL, + 12 => 8UL, + 13 => 12UL, + 14 => 16UL, + 20 => 4UL, + 22 => 8UL, + 29 => 4UL, + 36 => 1UL, + 49 => 1UL, + 56 => 4UL, + 62 => 4UL, + 64 => 4UL, + 71 => 8UL, + _ => 4UL, + }; + + private static ulong GetTextureByteCount(uint format, uint width, uint height) + { + var blockBytes = format switch + { + 169 or 170 => 8UL, + 171 or 172 or 173 or 174 or 175 or 176 or + 177 or 178 or 179 or 180 or 181 or 182 => 16UL, + _ => 0UL, + }; + return blockBytes == 0 + ? checked((ulong)width * height * GetTextureBytesPerPixel(format)) + : checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes); + } + + private static Format GetTextureFormat(uint format, uint numberType) => + (format, numberType) switch + { + (9, _) => Format.A2R10G10B10UnormPack32, + (GuestFormatR32Uint, _) => Format.R32Uint, + (GuestFormatR32Sint, _) => Format.R32Sint, + (GuestFormatR32Sfloat, _) => Format.R32Sfloat, + (GuestFormatR16G16Uint, _) => Format.R16G16Uint, + (GuestFormatR16G16Sint, _) => Format.R16G16Sint, + (GuestFormatR16G16Sfloat, _) => Format.R16G16Sfloat, + (GuestFormatR8G8B8A8Uint, _) => Format.R8G8B8A8Uint, + (GuestFormatR8G8B8A8Sint, _) => Format.R8G8B8A8Sint, + (GuestFormatR16G16B16A16Uint, _) => Format.R16G16B16A16Uint, + (GuestFormatR16G16B16A16Sint, _) => Format.R16G16B16A16Sint, + (1, 0) => Format.R8Unorm, + (2, 7) => Format.R16Sfloat, + (3, 0) => Format.R8G8Unorm, + (4, 4) => Format.R32Uint, + (4, 5) => Format.R32Sint, + (4, 7) => Format.R32Sfloat, + (5, 0) => Format.R16G16Unorm, + (5, 4) => Format.R16G16Uint, + (5, 5) => Format.R16G16Sint, + (5, 7) => Format.R16G16Sfloat, + (6, 7) => Format.B10G11R11UfloatPack32, + (10, 0) => Format.R8G8B8A8Unorm, + (10, 4) => Format.R8G8B8A8Uint, + (10, 5) => Format.R8G8B8A8Sint, + (11, 4) => Format.R32G32Uint, + (11, 5) => Format.R32G32Sint, + (11, 7) => Format.R32G32Sfloat, + (12, 0) => Format.R16G16B16A16Unorm, + (12, 4) => Format.R16G16B16A16Uint, + (12, 5) => Format.R16G16B16A16Sint, + (12, 7) => Format.R16G16B16A16Sfloat, + (13, 4) => Format.R32G32B32A32Uint, + (13, 5) => Format.R32G32B32A32Sint, + (13, _) => Format.R32G32B32A32Sfloat, + (14, 4) => Format.R32G32B32A32Uint, + (14, 5) => Format.R32G32B32A32Sint, + (14, 7) => Format.R32G32B32A32Sfloat, + (20, _) => Format.R32Uint, + (4, _) => Format.R32Sfloat, + (5, _) => Format.R16G16Sfloat, + (7, _) => Format.B10G11R11UfloatPack32, + (14, _) => Format.R32G32B32A32Sfloat, + (22, _) => Format.R16G16B16A16Sfloat, + (29, _) => Format.R32Sfloat, + (36, _) => Format.R8Unorm, + (49, _) => Format.R8Uint, + (56, _) => Format.R8G8B8A8Unorm, + (62, _) => Format.R8G8B8A8Unorm, + (64, _) => Format.R8G8B8A8Unorm, + (71, _) => Format.R16G16B16A16Sfloat, + (75, _) => Format.R32G32Sfloat, + (169, _) => Format.BC1RgbaUnormBlock, + (170, _) => Format.BC1RgbaSrgbBlock, + (181, _) => Format.BC7UnormBlock, + (182, _) => Format.BC7SrgbBlock, + _ => Format.R8G8B8A8Unorm, + }; + + private static Format GetRenderTargetFormat(uint format, uint numberType) => + (format, numberType) switch + { + (4, 4) => Format.R32Uint, + (4, 5) => Format.R32Sint, + (4, 7) => Format.R32Sfloat, + (5, 4) => Format.R16G16Uint, + (5, 5) => Format.R16G16Sint, + (5, 7) => Format.R16G16Sfloat, + (9, _) => Format.A2R10G10B10UnormPack32, + (10, 4) => Format.R8G8B8A8Uint, + (10, 5) => Format.R8G8B8A8Sint, + (10, _) => Format.R8G8B8A8Unorm, + (12, 4) => Format.R16G16B16A16Uint, + (12, 5) => Format.R16G16B16A16Sint, + (12, 7) => Format.R16G16B16A16Sfloat, + (_, 0) => GetTextureFormat(format, numberType), + _ => Format.Undefined, + }; + + private VkBuffer CreateBuffer( + ulong size, + BufferUsageFlags usage, + MemoryPropertyFlags memoryFlags, + out DeviceMemory memory) + { + var bufferInfo = new BufferCreateInfo + { + SType = StructureType.BufferCreateInfo, + Size = size, + Usage = usage, + SharingMode = SharingMode.Exclusive, + }; + Check(_vk.CreateBuffer(_device, &bufferInfo, null, out var buffer), "vkCreateBuffer"); + + _vk.GetBufferMemoryRequirements(_device, buffer, out var requirements); + var memoryInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = requirements.Size, + MemoryTypeIndex = FindMemoryType(requirements.MemoryTypeBits, memoryFlags), + }; + Check(_vk.AllocateMemory(_device, &memoryInfo, null, out memory), "vkAllocateMemory"); + Check(_vk.BindBufferMemory(_device, buffer, memory, 0), "vkBindBufferMemory"); + return buffer; + } + + private void CreateStagingBuffer(ulong size) + { + _stagingBuffer = CreateBuffer( + size, + BufferUsageFlags.TransferSrcBit | BufferUsageFlags.TransferDstBit, + MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit, + out _stagingMemory); _stagingSize = size; } @@ -806,15 +3557,741 @@ internal static unsafe class VulkanVideoPresenter throw new InvalidOperationException("No compatible Vulkan host-visible memory type was found."); } + private void ExecuteComputeDispatch(VulkanComputeGuestDispatch work) + { + TranslatedDrawResources? resources = null; + CommandBuffer commandBuffer = default; + var submitted = false; + try + { + EnsureGuestSubmissionCapacity(); + resources = CreateComputeDispatchResources(work); + commandBuffer = AllocateGuestCommandBuffer(); + _commandBuffer = commandBuffer; + var beginInfo = new CommandBufferBeginInfo + { + SType = StructureType.CommandBufferBeginInfo, + Flags = CommandBufferUsageFlags.OneTimeSubmitBit, + }; + Check( + _vk.BeginCommandBuffer(_commandBuffer, &beginInfo), + "vkBeginCommandBuffer(compute)"); + + BeginDebugLabel(_commandBuffer, resources.DebugName); + RecordTextureUploads(resources, PipelineStageFlags.ComputeShaderBit); + RecordStorageImagesForWrite(resources, PipelineStageFlags.ComputeShaderBit); + _vk.CmdBindPipeline( + _commandBuffer, + PipelineBindPoint.Compute, + resources.Pipeline); + if (resources.DescriptorSet.Handle != 0) + { + var descriptorSet = resources.DescriptorSet; + _vk.CmdBindDescriptorSets( + _commandBuffer, + PipelineBindPoint.Compute, + resources.PipelineLayout, + 0, + 1, + &descriptorSet, + 0, + null); + } + + RecordChunkedComputeDispatch(_commandBuffer, work); + RecordStorageImagesForRead(resources, PipelineStageFlags.ComputeShaderBit); + EndDebugLabel(_commandBuffer); + + Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(compute)"); + SubmitGuestCommandBuffer( + commandBuffer, + resources, + GetTraceImages(resources)); + submitted = true; + MarkStorageImagesInitialized(resources, traceContents: false); + TraceVulkanShader( + $"vk.compute_dispatch groups={work.GroupCountX}x" + + $"{work.GroupCountY}x{work.GroupCountZ} " + + $"textures={work.Textures.Count} cs=0x{work.ShaderAddress:X16}"); + } + catch (Exception exception) + { + Console.Error.WriteLine( + $"[LOADER][ERROR] Vulkan compute dispatch failed: {exception.Message}"); + } + finally + { + _commandBuffer = _presentationCommandBuffer; + if (!submitted && commandBuffer.Handle != 0) + { + _vk.FreeCommandBuffers( + _device, + _commandPool, + 1, + &commandBuffer); + } + + if (!submitted && resources is not null) + { + DestroyTranslatedDrawResources(resources); + } + } + } + + private void RecordChunkedComputeDispatch( + CommandBuffer commandBuffer, + VulkanComputeGuestDispatch work) + { + const uint maxWorkgroupsPerCommand = 4096; + var yChunk = Math.Max( + 1u, + Math.Min( + work.GroupCountY, + maxWorkgroupsPerCommand / Math.Max(work.GroupCountX, 1u))); + var commandCount = 0u; + + for (var z = 0u; z < work.GroupCountZ; z++) + { + for (var y = 0u; y < work.GroupCountY; y += yChunk) + { + var countY = Math.Min(yChunk, work.GroupCountY - y); + _vk.CmdDispatchBase( + commandBuffer, + 0, + y, + z, + work.GroupCountX, + countY, + 1); + commandCount++; + } + } + + if (commandCount > 1) + { + TraceVulkanShader( + $"vk.compute_chunked cs=0x{work.ShaderAddress:X16} " + + $"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " + + $"commands={commandCount} y_chunk={yChunk}"); + } + } + + private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work) + { + var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType); + if (format == Format.Undefined) + { + Console.Error.WriteLine( + $"[LOADER][WARN] Vulkan skipped unsupported render target " + + $"addr=0x{work.Target.Address:X16} format={work.Target.Format} " + + $"number={work.Target.NumberType}"); + return; + } + + if (work.Draw.Textures.Any(texture => + texture.Address == work.Target.Address && + texture.Address != 0)) + { + Console.Error.WriteLine( + $"[LOADER][WARN] Vulkan skipped render-target feedback loop " + + $"addr=0x{work.Target.Address:X16}"); + return; + } + + var target = GetOrCreateGuestImage(work.Target, format); + TranslatedDrawResources? resources = null; + CommandBuffer commandBuffer = default; + var submitted = false; + try + { + EnsureGuestSubmissionCapacity(); + var extent = new Extent2D(target.Width, target.Height); + resources = CreateTranslatedDrawResources( + work.Draw, + target.RenderPass, + extent); + resources.DebugName = + $"SharpEmu offscreen rt=0x{work.Target.Address:X16} " + + $"{work.Target.Width}x{work.Target.Height} fmt{work.Target.Format}"; + + commandBuffer = AllocateGuestCommandBuffer(); + _commandBuffer = commandBuffer; + var beginInfo = new CommandBufferBeginInfo + { + SType = StructureType.CommandBufferBeginInfo, + Flags = CommandBufferUsageFlags.OneTimeSubmitBit, + }; + Check( + _vk.BeginCommandBuffer(_commandBuffer, &beginInfo), + "vkBeginCommandBuffer(offscreen)"); + + BeginDebugLabel(_commandBuffer, resources.DebugName); + RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit); + RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit); + + var toColorAttachment = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = target.Initialized ? AccessFlags.ShaderReadBit : 0, + DstAccessMask = AccessFlags.ColorAttachmentWriteBit, + OldLayout = target.Initialized + ? ImageLayout.ShaderReadOnlyOptimal + : ImageLayout.Undefined, + NewLayout = ImageLayout.ColorAttachmentOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = target.Image, + SubresourceRange = ColorSubresourceRange(), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + target.Initialized + ? PipelineStageFlags.FragmentShaderBit + : PipelineStageFlags.TopOfPipeBit, + PipelineStageFlags.ColorAttachmentOutputBit, + 0, + 0, + null, + 0, + null, + 1, + &toColorAttachment); + + RecordTranslatedGraphicsPass( + resources, + target.RenderPass, + target.Framebuffer, + extent); + RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit); + + var toShaderRead = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.ColorAttachmentWriteBit, + DstAccessMask = AccessFlags.ShaderReadBit, + OldLayout = ImageLayout.ColorAttachmentOptimal, + NewLayout = ImageLayout.ShaderReadOnlyOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = target.Image, + SubresourceRange = ColorSubresourceRange(), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.ColorAttachmentOutputBit, + PipelineStageFlags.FragmentShaderBit, + 0, + 0, + null, + 0, + null, + 1, + &toShaderRead); + EndDebugLabel(_commandBuffer); + + Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(offscreen)"); + SubmitGuestCommandBuffer( + commandBuffer, + resources, + GetTraceImages(resources, target)); + submitted = true; + target.Initialized = true; + MarkStorageImagesInitialized(resources, traceContents: false); + + var guestTextureFormat = GetGuestTextureFormat(target.Format); + if (work.PublishTarget && guestTextureFormat != 0) + { + lock (_gate) + { + _availableGuestImages[target.Address] = guestTextureFormat; + } + } + if (ShouldTraceGuestImageWriteForDiagnostics(target.Address)) + { + var writeCount = _tracedGuestWriteCounts.TryGetValue( + target.Address, + out var previousCount) + ? previousCount + 1 + : 1; + _tracedGuestWriteCounts[target.Address] = writeCount; + if (writeCount <= 3) + { + _commandBuffer = _presentationCommandBuffer; + Check( + _vk.QueueWaitIdle(_queue), + "vkQueueWaitIdle(guest write trace)"); + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.guest_write_sample " + + $"addr=0x{target.Address:X16} write={writeCount} " + + $"ps_bytes={work.Draw.PixelSpirv.Length}"); + TraceGuestImageContents(target); + } + } + TraceVulkanShader( + $"vk.offscreen_draw addr=0x{target.Address:X16} " + + $"size={target.Width}x{target.Height} format={target.Format} " + + $"textures={work.Draw.Textures.Count}"); + } + catch (Exception exception) + { + if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) || + !failedTarget.Initialized) + { + lock (_gate) + { + _availableGuestImages.Remove(work.Target.Address); + } + } + + Console.Error.WriteLine( + $"[LOADER][ERROR] Vulkan offscreen draw failed " + + $"addr=0x{work.Target.Address:X16}: {exception.Message}"); + } + finally + { + _commandBuffer = _presentationCommandBuffer; + if (!submitted && commandBuffer.Handle != 0) + { + _vk.FreeCommandBuffers( + _device, + _commandPool, + 1, + &commandBuffer); + } + + if (!submitted && resources is not null) + { + DestroyTranslatedDrawResources(resources); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private GuestImageResource GetOrCreateGuestImage( + VulkanGuestRenderTarget target, + Format format) + { + var mipLevels = ClampMipLevels(target.Width, target.Height, target.MipLevels); + if (_guestImages.TryGetValue(target.Address, out var existing)) + { + if (existing.Width == target.Width && + existing.Height == target.Height && + existing.MipLevels == mipLevels && + existing.Format == format) + { + return existing; + } + + DestroyGuestImage(existing); + _guestImages.Remove(target.Address); + lock (_gate) + { + _availableGuestImages.Remove(target.Address); + } + } + + var imageInfo = new ImageCreateInfo + { + SType = StructureType.ImageCreateInfo, + Flags = + ImageCreateFlags.CreateMutableFormatBit | + ImageCreateFlags.CreateExtendedUsageBit, + ImageType = ImageType.Type2D, + Format = format, + Extent = new Extent3D(target.Width, target.Height, 1), + MipLevels = mipLevels, + ArrayLayers = 1, + Samples = SampleCountFlags.Count1Bit, + Tiling = ImageTiling.Optimal, + Usage = + ImageUsageFlags.ColorAttachmentBit | + ImageUsageFlags.SampledBit | + ImageUsageFlags.StorageBit | + ImageUsageFlags.TransferSrcBit | + ImageUsageFlags.TransferDstBit, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + }; + Check(_vk.CreateImage(_device, &imageInfo, null, out var image), "vkCreateImage(offscreen)"); + _vk.GetImageMemoryRequirements(_device, image, out var requirements); + var allocationInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = requirements.Size, + MemoryTypeIndex = FindMemoryType( + requirements.MemoryTypeBits, + MemoryPropertyFlags.DeviceLocalBit), + }; + Check( + _vk.AllocateMemory(_device, &allocationInfo, null, out var memory), + "vkAllocateMemory(offscreen)"); + Check(_vk.BindImageMemory(_device, image, memory, 0), "vkBindImageMemory(offscreen)"); + + var viewInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = image, + ViewType = ImageViewType.Type2D, + Format = format, + Components = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity), + SubresourceRange = ColorSubresourceRange(0, mipLevels), + }; + Check( + _vk.CreateImageView(_device, &viewInfo, null, out var view), + "vkCreateImageView(offscreen)"); + + var mipViews = new ImageView[mipLevels]; + for (uint mipLevel = 0; mipLevel < mipLevels; mipLevel++) + { + viewInfo.SubresourceRange = ColorSubresourceRange(mipLevel, 1); + ImageView mipView; + Check( + _vk.CreateImageView( + _device, + &viewInfo, + null, + out mipView), + "vkCreateImageView(offscreen mip)"); + mipViews[mipLevel] = mipView; + } + + var colorAttachment = new AttachmentDescription + { + Format = format, + Samples = SampleCountFlags.Count1Bit, + LoadOp = AttachmentLoadOp.Load, + StoreOp = AttachmentStoreOp.Store, + StencilLoadOp = AttachmentLoadOp.DontCare, + StencilStoreOp = AttachmentStoreOp.DontCare, + InitialLayout = ImageLayout.ColorAttachmentOptimal, + FinalLayout = ImageLayout.ColorAttachmentOptimal, + }; + var colorReference = new AttachmentReference + { + Attachment = 0, + Layout = ImageLayout.ColorAttachmentOptimal, + }; + var subpass = new SubpassDescription + { + PipelineBindPoint = PipelineBindPoint.Graphics, + ColorAttachmentCount = 1, + PColorAttachments = &colorReference, + }; + var renderPassInfo = new RenderPassCreateInfo + { + SType = StructureType.RenderPassCreateInfo, + AttachmentCount = 1, + PAttachments = &colorAttachment, + SubpassCount = 1, + PSubpasses = &subpass, + }; + Check( + _vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass), + "vkCreateRenderPass(offscreen)"); + + var attachment = mipViews[0]; + var framebufferInfo = new FramebufferCreateInfo + { + SType = StructureType.FramebufferCreateInfo, + RenderPass = renderPass, + AttachmentCount = 1, + PAttachments = &attachment, + Width = target.Width, + Height = target.Height, + Layers = 1, + }; + Check( + _vk.CreateFramebuffer(_device, &framebufferInfo, null, out var framebuffer), + "vkCreateFramebuffer(offscreen)"); + + var resource = new GuestImageResource + { + Address = target.Address, + Width = target.Width, + Height = target.Height, + MipLevels = mipLevels, + Format = format, + Image = image, + Memory = memory, + View = view, + MipViews = mipViews, + RenderPass = renderPass, + Framebuffer = framebuffer, + }; + var debugName = GuestImageDebugName(target, format); + SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image"); + SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view"); + for (var mipLevel = 0; mipLevel < mipViews.Length; mipLevel++) + { + SetDebugName( + ObjectType.ImageView, + mipViews[mipLevel].Handle, + $"{debugName} mip{mipLevel}"); + } + SetDebugName(ObjectType.RenderPass, renderPass.Handle, $"{debugName} renderpass"); + SetDebugName(ObjectType.Framebuffer, framebuffer.Handle, $"{debugName} framebuffer"); + _guestImages.Add(target.Address, resource); + return resource; + } + + private static uint ClampMipLevels(uint width, uint height, uint requestedMipLevels) + { + var largestDimension = Math.Max(width, height); + uint maximumMipLevels = 1; + while (largestDimension > 1) + { + largestDimension >>= 1; + maximumMipLevels++; + } + + return Math.Min(Math.Max(requestedMipLevels, 1u), maximumMipLevels); + } + + private void DestroyGuestImage(GuestImageResource resource) + { + foreach (var view in resource.FormatViews.Values) + { + if (view.Handle != 0) + { + _vk.DestroyImageView(_device, view, null); + } + } + resource.FormatViews.Clear(); + + if (resource.Framebuffer.Handle != 0) + { + _vk.DestroyFramebuffer(_device, resource.Framebuffer, null); + } + + if (resource.RenderPass.Handle != 0) + { + _vk.DestroyRenderPass(_device, resource.RenderPass, null); + } + + if (resource.View.Handle != 0) + { + _vk.DestroyImageView(_device, resource.View, null); + } + + foreach (var mipView in resource.MipViews) + { + if (mipView.Handle != 0) + { + _vk.DestroyImageView(_device, mipView, null); + } + } + + if (resource.Image.Handle != 0) + { + _vk.DestroyImage(_device, resource.Image, null); + } + + if (resource.Memory.Handle != 0) + { + _vk.FreeMemory(_device, resource.Memory, null); + } + } + + private static uint GetGuestTextureFormat(Format format) => + format switch + { + Format.A2R10G10B10UnormPack32 => 9, + Format.R8G8B8A8Unorm => 56, + Format.R16G16Unorm => 5, + Format.R16G16B16A16Unorm => 12, + Format.R32Uint => GuestFormatR32Uint, + Format.R32Sint => GuestFormatR32Sint, + Format.R32Sfloat => GuestFormatR32Sfloat, + Format.R16G16Uint => GuestFormatR16G16Uint, + Format.R16G16Sint => GuestFormatR16G16Sint, + Format.R16G16Sfloat => GuestFormatR16G16Sfloat, + Format.R8G8B8A8Uint => GuestFormatR8G8B8A8Uint, + Format.R8G8B8A8Sint => GuestFormatR8G8B8A8Sint, + Format.R16G16B16A16Uint => GuestFormatR16G16B16A16Uint, + Format.R16G16B16A16Sint => GuestFormatR16G16B16A16Sint, + Format.R16G16B16A16Sfloat => 71, + _ => 0, + }; + + private bool TryGetOrCreateGuestImageView( + GuestImageResource resource, + Format format, + uint mipLevel, + uint levelCount, + uint dstSelect, + out ImageView view) + { + try + { + view = GetOrCreateGuestImageView(resource, format, mipLevel, levelCount, dstSelect); + return true; + } + catch (Exception exception) + { + view = default; + TraceVulkanShader( + $"vk.texture_alias_view_failed addr=0x{resource.Address:X16} " + + $"image_format={resource.Format} view_format={format}: {exception.Message}"); + return false; + } + } + + private ImageView GetOrCreateGuestImageView( + GuestImageResource resource, + Format format, + uint mipLevel, + uint levelCount, + uint dstSelect = 0xFAC) + { + if (mipLevel >= resource.MipLevels) + { + throw new InvalidOperationException( + $"View mip {mipLevel} exceeds image mip count {resource.MipLevels}."); + } + + levelCount = Math.Max(levelCount, 1); + levelCount = Math.Min(levelCount, resource.MipLevels - mipLevel); + if (format == resource.Format && dstSelect == 0xFAC) + { + if (mipLevel == 0 && levelCount == resource.MipLevels) + { + return resource.View; + } + + if (levelCount == 1) + { + return resource.MipViews[mipLevel]; + } + } + + if (!IsCompatibleViewFormat(resource.Format, format)) + { + throw new InvalidOperationException( + $"Incompatible image view format {format} for image {resource.Format}."); + } + + var key = (format, mipLevel, levelCount, dstSelect); + if (resource.FormatViews.TryGetValue(key, out var existing)) + { + return existing; + } + + var viewInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = resource.Image, + ViewType = ImageViewType.Type2D, + Format = format, + Components = ToVkComponentMapping(dstSelect), + SubresourceRange = ColorSubresourceRange(mipLevel, levelCount), + }; + ImageView view; + Check( + _vk.CreateImageView(_device, &viewInfo, null, out view), + "vkCreateImageView(guest alias)"); + resource.FormatViews.Add(key, view); + SetDebugName( + ObjectType.ImageView, + view.Handle, + $"SharpEmu guest 0x{resource.Address:X16} alias {format} mip{mipLevel}+{levelCount}"); + TraceVulkanShader( + $"vk.texture_alias_view addr=0x{resource.Address:X16} " + + $"image_format={resource.Format} view_format={format} " + + $"mip={mipLevel} levels={levelCount} dst=0x{dstSelect:X3}"); + return view; + } + + private static bool IsCompatibleViewFormat(Format imageFormat, Format viewFormat) + { + if (imageFormat == viewFormat) + { + return true; + } + + var imageClass = GetFormatCompatibilityClass(imageFormat); + return imageClass != 0 && imageClass == GetFormatCompatibilityClass(viewFormat); + } + + private static uint GetFormatCompatibilityClass(Format format) => + format switch + { + Format.R8Unorm or + Format.R8Uint or + Format.R8Sint => 8, + Format.R16Sfloat => 16, + Format.R32Uint or + Format.R32Sint or + Format.R32Sfloat or + Format.R16G16Unorm or + Format.R16G16Uint or + Format.R16G16Sint or + Format.R16G16Sfloat or + Format.R8G8B8A8Unorm or + Format.R8G8B8A8Uint or + Format.R8G8B8A8Sint or + Format.A2R10G10B10UnormPack32 or + Format.B10G11R11UfloatPack32 => 32, + Format.R32G32Uint or + Format.R32G32Sint or + Format.R32G32Sfloat or + Format.R16G16B16A16Unorm or + Format.R16G16B16A16Uint or + Format.R16G16B16A16Sint or + Format.R16G16B16A16Sfloat => 64, + Format.R32G32B32Sfloat => 96, + Format.R32G32B32A32Uint or + Format.R32G32B32A32Sint or + Format.R32G32B32A32Sfloat => 128, + _ => 0, + }; + private void Render(double _) { - if (!_vulkanReady || !TryTakePresentation(_presentedSequence, out var presentation)) + if (!_vulkanReady) + { + return; + } + + _commandBuffer = _presentationCommandBuffer; + CollectCompletedGuestSubmissions(waitForOldest: false); + + var completedWork = 0; + while (completedWork < MaxGuestWorkPerRender && + TryTakeGuestWork(out var work)) + { + try + { + switch (work) + { + case VulkanOffscreenGuestDraw offscreenDraw: + ExecuteOffscreenDraw(offscreenDraw); + break; + case VulkanComputeGuestDispatch computeDispatch: + ExecuteComputeDispatch(computeDispatch); + break; + } + } + finally + { + CompleteGuestWork(); + } + + completedWork++; + } + + if (!TryTakePresentation(_presentedSequence, out var presentation)) { return; } if (presentation.Pixels is null && - presentation.DrawKind != GuestDrawKind.FullscreenBarycentric) + presentation.DrawKind != GuestDrawKind.FullscreenBarycentric && + presentation.TranslatedDraw is null && + presentation.GuestImageAddress == 0) { return; } @@ -836,16 +4313,78 @@ internal static unsafe class VulkanVideoPresenter } } + TranslatedDrawResources? translatedResources = null; + GuestImageResource? presentedGuestImage = null; + if (presentation.GuestImageAddress != 0 && + (!_guestImages.TryGetValue( + presentation.GuestImageAddress, + out presentedGuestImage) || + !presentedGuestImage.Initialized)) + { + return; + } + if (presentedGuestImage is not null) + { + _directPresentationCount++; + if (ShouldTracePresentedGuestImageContentsForDiagnostics() && + _directPresentationCount is 1 or 30 or 120) + { + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.present_sample frame={_directPresentationCount} " + + $"addr=0x{presentedGuestImage.Address:X16}"); + TraceGuestImageContents(presentedGuestImage); + } + } + + if (presentation.TranslatedDraw is { } translatedDraw) + { + try + { + translatedResources = CreateTranslatedDrawResources( + translatedDraw, + _renderPass, + _extent); + if (ShouldTracePresentedGuestImageContentsForDiagnostics() && + !_firstGuestDrawPresented && + translatedResources.Textures is + [ + { GuestImage: { } guestImage }, + ] && + _tracedGuestImageContents.Add(guestImage.Address)) + { + TraceGuestImageContents(guestImage); + } + } + catch (Exception exception) + { + _presentedSequence = presentation.Sequence; + Console.Error.WriteLine( + $"[LOADER][ERROR] Vulkan VideoOut translated draw setup failed: {exception.Message}"); + return; + } + } + uint imageIndex; - Check( - _swapchainApi.AcquireNextImage( - _device, - _swapchain, - ulong.MaxValue, - _imageAvailable, - default, - &imageIndex), - "vkAcquireNextImageKHR"); + var acquireResult = _swapchainApi.AcquireNextImage( + _device, + _swapchain, + ulong.MaxValue, + _imageAvailable, + default, + &imageIndex); + if (acquireResult == Result.ErrorOutOfDateKhr) + { + RecreateSwapchainResources("vkAcquireNextImageKHR", acquireResult); + if (translatedResources is not null) + { + DestroyTranslatedDrawResources(translatedResources); + } + + return; + } + + CheckSwapchainResult(acquireResult, "vkAcquireNextImageKHR"); + var recreateAfterPresent = acquireResult == Result.SuboptimalKhr; if (pixels is not null) { @@ -898,6 +4437,16 @@ internal static unsafe class VulkanVideoPresenter _vk.CmdEndRenderPass(_commandBuffer); waitStage = PipelineStageFlags.ColorAttachmentOutputBit; } + else if (presentedGuestImage is not null) + { + RecordGuestImageBlit(imageIndex, presentedGuestImage); + waitStage = PipelineStageFlags.TransferBit; + } + else if (translatedResources is not null) + { + RecordTranslatedDraw(imageIndex, translatedResources); + waitStage = PipelineStageFlags.AllCommandsBit; + } else { throw new InvalidOperationException( @@ -932,8 +4481,34 @@ internal static unsafe class VulkanVideoPresenter PSwapchains = &swapchain, PImageIndices = &imageIndex, }; - Check(_swapchainApi.QueuePresent(_queue, &presentInfo), "vkQueuePresentKHR"); + var presentResult = _swapchainApi.QueuePresent(_queue, &presentInfo); + if (presentResult == Result.ErrorOutOfDateKhr) + { + Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); + CollectCompletedGuestSubmissions(waitForOldest: false); + if (translatedResources is not null) + { + DestroyTranslatedDrawResources(translatedResources); + } + + RecreateSwapchainResources("vkQueuePresentKHR", presentResult); + return; + } + + CheckSwapchainResult(presentResult, "vkQueuePresentKHR"); + recreateAfterPresent |= presentResult == Result.SuboptimalKhr; Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); + if (_swapchainReadbackPending) + { + TraceSwapchainReadback(); + } + CollectCompletedGuestSubmissions(waitForOldest: false); + if (translatedResources is not null) + { + MarkStorageImagesInitialized(translatedResources); + DestroyTranslatedDrawResources(translatedResources); + } + _imageInitialized[imageIndex] = true; _presentedSequence = presentation.Sequence; if (presentation.IsSplash && !_splashPresented) @@ -955,8 +4530,800 @@ internal static unsafe class VulkanVideoPresenter { _firstGuestDrawPresented = true; Console.Error.WriteLine( - $"[LOADER][INFO] Vulkan VideoOut presented translated guest draw: " + - $"{presentation.DrawKind}"); + $"[LOADER][INFO] Vulkan VideoOut presented guest frame: " + + (presentedGuestImage is not null + ? $"image=0x{presentedGuestImage.Address:X16} " + + $"{presentedGuestImage.Width}x{presentedGuestImage.Height}" + : presentation.TranslatedDraw is null + ? $"{presentation.DrawKind}" + : $"shader textures={presentation.TranslatedDraw.Textures.Count}")); + } + + if (recreateAfterPresent) + { + RecreateSwapchainResources("present suboptimal", Result.SuboptimalKhr); + } + } + + private void TraceGuestImageContents(GuestImageResource image) + { + var bytesPerPixel = GetReadbackBytesPerPixel(image.Format); + if (bytesPerPixel == 0) + { + TraceVulkanShader( + $"vk.guest_image addr=0x{image.Address:X16} " + + $"format={image.Format} readback=unsupported"); + return; + } + + var byteCount = checked((ulong)image.Width * image.Height * bytesPerPixel); + var buffer = CreateBuffer( + byteCount, + BufferUsageFlags.TransferDstBit, + MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit, + out var memory); + try + { + Check( + _vk.ResetCommandBuffer(_commandBuffer, 0), + "vkResetCommandBuffer(guest readback)"); + var beginInfo = new CommandBufferBeginInfo + { + SType = StructureType.CommandBufferBeginInfo, + Flags = CommandBufferUsageFlags.OneTimeSubmitBit, + }; + Check( + _vk.BeginCommandBuffer(_commandBuffer, &beginInfo), + "vkBeginCommandBuffer(guest readback)"); + + var toTransfer = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.ShaderReadBit, + DstAccessMask = AccessFlags.TransferReadBit, + OldLayout = ImageLayout.ShaderReadOnlyOptimal, + NewLayout = ImageLayout.TransferSrcOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image.Image, + SubresourceRange = ColorSubresourceRange(), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.FragmentShaderBit | + PipelineStageFlags.ComputeShaderBit, + PipelineStageFlags.TransferBit, + 0, + 0, + null, + 0, + null, + 1, + &toTransfer); + + var region = new BufferImageCopy + { + ImageSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ColorBit, + LayerCount = 1, + }, + ImageExtent = new Extent3D(image.Width, image.Height, 1), + }; + _vk.CmdCopyImageToBuffer( + _commandBuffer, + image.Image, + ImageLayout.TransferSrcOptimal, + buffer, + 1, + ®ion); + + var toShaderRead = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.TransferReadBit, + DstAccessMask = AccessFlags.ShaderReadBit, + OldLayout = ImageLayout.TransferSrcOptimal, + NewLayout = ImageLayout.ShaderReadOnlyOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image.Image, + SubresourceRange = ColorSubresourceRange(), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.TransferBit, + PipelineStageFlags.FragmentShaderBit | + PipelineStageFlags.ComputeShaderBit, + 0, + 0, + null, + 0, + null, + 1, + &toShaderRead); + + Check( + _vk.EndCommandBuffer(_commandBuffer), + "vkEndCommandBuffer(guest readback)"); + var commandBuffer = _commandBuffer; + var submitInfo = new SubmitInfo + { + SType = StructureType.SubmitInfo, + CommandBufferCount = 1, + PCommandBuffers = &commandBuffer, + }; + Check( + _vk.QueueSubmit(_queue, 1, &submitInfo, default), + "vkQueueSubmit(guest readback)"); + Check( + _vk.QueueWaitIdle(_queue), + "vkQueueWaitIdle(guest readback)"); + + void* mapped; + Check( + _vk.MapMemory(_device, memory, 0, byteCount, 0, &mapped), + "vkMapMemory(guest readback)"); + try + { + var bytes = new ReadOnlySpan(mapped, checked((int)byteCount)); + var nonzeroBytes = 0L; + ulong hash = 14695981039346656037UL; + foreach (var value in bytes) + { + nonzeroBytes += value == 0 ? 0 : 1; + hash = (hash ^ value) * 1099511628211UL; + } + + var nonblackPixels = CountNonblackPixels( + bytes, + image.Format, + bytesPerPixel); + var centerOffset = checked( + ((int)(image.Height / 2) * (int)image.Width + + (int)(image.Width / 2)) * + (int)bytesPerPixel); + var center = Convert.ToHexString( + bytes.Slice(centerOffset, (int)bytesPerPixel)); + TraceVulkanShader( + $"vk.guest_image addr=0x{image.Address:X16} " + + $"size={image.Width}x{image.Height} format={image.Format} " + + $"nonzero_bytes={nonzeroBytes}/{byteCount} " + + $"nonblack_pixels={nonblackPixels}/{(ulong)image.Width * image.Height} " + + $"center={center} hash=0x{hash:X16}"); + DumpGuestImageBytes(image, bytes); + } + finally + { + _vk.UnmapMemory(_device, memory); + } + } + finally + { + _vk.DestroyBuffer(_device, buffer, null); + _vk.FreeMemory(_device, memory, null); + } + } + + private static void DumpGuestImageBytes( + GuestImageResource image, + ReadOnlySpan bytes) + { + var directory = + Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_DUMP_DIR"); + if (string.IsNullOrWhiteSpace(directory)) + { + return; + } + + Directory.CreateDirectory(directory); + var path = Path.Combine( + directory, + $"0x{image.Address:X16}-{image.Width}x{image.Height}-{image.Format}.rgba"); + File.WriteAllBytes(path, bytes.ToArray()); + } + + private static uint GetReadbackBytesPerPixel(Format format) => + format switch + { + Format.R8Unorm or + Format.R8Uint or + Format.R8Sint => 1, + Format.R32Uint or + Format.R32Sint or + Format.R32Sfloat or + Format.R16G16Uint or + Format.R16G16Sint or + Format.R16G16Sfloat or + Format.R8G8B8A8Uint or + Format.R8G8B8A8Sint or + Format.R8G8B8A8Unorm or + Format.A2R10G10B10UnormPack32 => 4, + Format.R16G16B16A16Uint or + Format.R16G16B16A16Sint or + Format.R16G16B16A16Sfloat => 8, + Format.R32G32Uint or + Format.R32G32Sint or + Format.R32G32Sfloat => 8, + Format.R32G32B32A32Uint or + Format.R32G32B32A32Sint or + Format.R32G32B32A32Sfloat => 16, + _ => 0, + }; + + private static long CountNonblackPixels( + ReadOnlySpan bytes, + Format format, + uint bytesPerPixel) + { + var count = 0L; + for (var offset = 0; offset < bytes.Length; offset += (int)bytesPerPixel) + { + var pixel = bytes.Slice(offset, (int)bytesPerPixel); + var hasColor = format switch + { + Format.A2R10G10B10UnormPack32 => + (BitConverter.ToUInt32(pixel) & 0x3FFFFFFFu) != 0, + Format.R8G8B8A8Uint or + Format.R8G8B8A8Sint or + Format.R8G8B8A8Unorm => + pixel[0] != 0 || pixel[1] != 0 || pixel[2] != 0, + Format.R16G16B16A16Uint or + Format.R16G16B16A16Sint or + Format.R16G16B16A16Sfloat => + pixel[..6].IndexOfAnyExcept((byte)0) >= 0, + _ => pixel.IndexOfAnyExcept((byte)0) >= 0, + }; + count += hasColor ? 1 : 0; + } + + return count; + } + + private void RecordTranslatedDraw(uint imageIndex, TranslatedDrawResources resources) + { + BeginDebugLabel(_commandBuffer, "SharpEmu swapchain draw"); + RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit); + RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit); + RecordTranslatedGraphicsPass( + resources, + _renderPass, + _framebuffers[imageIndex], + _extent); + RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit); + EndDebugLabel(_commandBuffer); + } + + private void RecordTextureUploads( + TranslatedDrawResources resources, + PipelineStageFlags shaderStage) + { + foreach (var texture in resources.Textures) + { + if (!texture.NeedsUpload) + { + continue; + } + + var toTransfer = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + DstAccessMask = AccessFlags.TransferWriteBit, + OldLayout = ImageLayout.Undefined, + NewLayout = ImageLayout.TransferDstOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = texture.Image, + SubresourceRange = ColorSubresourceRange(), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.TopOfPipeBit, + PipelineStageFlags.TransferBit, + 0, + 0, + null, + 0, + null, + 1, + &toTransfer); + + var copyRegion = new BufferImageCopy + { + BufferRowLength = texture.RowLength > texture.Width + ? texture.RowLength + : 0, + ImageSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ColorBit, + LayerCount = 1, + }, + ImageExtent = new Extent3D(texture.Width, texture.Height, 1), + }; + _vk.CmdCopyBufferToImage( + _commandBuffer, + texture.StagingBuffer, + texture.Image, + ImageLayout.TransferDstOptimal, + 1, + ©Region); + + var toShaderRead = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.TransferWriteBit, + DstAccessMask = AccessFlags.ShaderReadBit, + OldLayout = ImageLayout.TransferDstOptimal, + NewLayout = ImageLayout.ShaderReadOnlyOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = texture.Image, + SubresourceRange = ColorSubresourceRange(), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.TransferBit, + shaderStage, + 0, + 0, + null, + 0, + null, + 1, + &toShaderRead); + } + } + + private void RecordStorageImagesForWrite( + TranslatedDrawResources resources, + PipelineStageFlags shaderStage) + { + var transitioned = new HashSet(); + foreach (var texture in resources.Textures) + { + if (!texture.IsStorage || + texture.GuestImage is not { } guestImage || + !transitioned.Add(guestImage)) + { + continue; + } + + var barrier = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = + guestImage.Initialized || guestImage.InitialUploadPending + ? AccessFlags.ShaderReadBit + : 0, + DstAccessMask = + AccessFlags.ShaderReadBit | + AccessFlags.ShaderWriteBit, + OldLayout = + guestImage.Initialized || guestImage.InitialUploadPending + ? ImageLayout.ShaderReadOnlyOptimal + : ImageLayout.Undefined, + NewLayout = ImageLayout.General, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = guestImage.Image, + SubresourceRange = ColorSubresourceRange(0, guestImage.MipLevels), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + guestImage.Initialized || guestImage.InitialUploadPending + ? shaderStage + : PipelineStageFlags.TopOfPipeBit, + shaderStage, + 0, + 0, + null, + 0, + null, + 1, + &barrier); + } + } + + private void RecordStorageImagesForRead( + TranslatedDrawResources resources, + PipelineStageFlags shaderStage) + { + var transitioned = new HashSet(); + foreach (var texture in resources.Textures) + { + if (!texture.IsStorage || + texture.GuestImage is not { } guestImage || + !transitioned.Add(guestImage)) + { + continue; + } + + var barrier = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = + AccessFlags.ShaderReadBit | + AccessFlags.ShaderWriteBit, + DstAccessMask = AccessFlags.ShaderReadBit, + OldLayout = ImageLayout.General, + NewLayout = ImageLayout.ShaderReadOnlyOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = guestImage.Image, + SubresourceRange = ColorSubresourceRange(0, guestImage.MipLevels), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + shaderStage, + shaderStage, + 0, + 0, + null, + 0, + null, + 1, + &barrier); + } + } + + private void MarkStorageImagesInitialized( + TranslatedDrawResources resources, + bool traceContents = true) + { + List? traceImages = null; + lock (_gate) + { + foreach (var texture in resources.Textures) + { + if (!texture.IsStorage || + texture.Address == 0 || + texture.GuestImage is not { } guestImage) + { + continue; + } + + guestImage.Initialized = true; + guestImage.InitialUploadPending = false; + var format = GetGuestTextureFormat(guestImage.Format); + if (format != 0) + { + _availableGuestImages[texture.Address] = format; + } + + if (traceContents && + ShouldTraceGuestImageContents(guestImage)) + { + traceImages ??= []; + traceImages.Add(guestImage); + } + } + } + + if (traceImages is null) + { + return; + } + + foreach (var image in traceImages) + { + TraceGuestImageContents(image); + } + } + + private bool ShouldTraceGuestImageContents(GuestImageResource image) + { + if (image.Address == 0) + { + return false; + } + + var addressMatched = ShouldTraceGuestImageAddressForDiagnostics(image.Address); + var broadTrace = + ShouldTraceGuestImageContentsForDiagnostics() && + image.Width >= 1280 && + image.Height >= 720; + return (addressMatched || broadTrace) && + _tracedGuestImageContents.Add(image.Address); + } + + private static bool ShouldTraceGuestImageContentsForDiagnostics() => + string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES"), + "1", + StringComparison.Ordinal); + + private static bool ShouldTraceGuestImageAddressForDiagnostics(ulong address) + { + return AddressListContains( + "SHARPEMU_TRACE_GUEST_IMAGE_ADDRS", + address); + } + + private static bool ShouldTraceGuestImageWriteForDiagnostics(ulong address) + { + return AddressListContains( + "SHARPEMU_TRACE_GUEST_WRITES", + address); + } + + private static bool AddressListContains( + string environmentVariable, + ulong address) + { + var addresses = Environment.GetEnvironmentVariable(environmentVariable); + if (string.IsNullOrWhiteSpace(addresses)) + { + return false; + } + + foreach (var token in addresses.Split( + [',', ';', ' ', '\t'], + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + if (token == "*") + { + return true; + } + + var span = token.AsSpan(); + if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + span = span[2..]; + } + + if (ulong.TryParse( + span, + System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, + out var parsed) && + parsed == address) + { + return true; + } + } + + return false; + } + + private static bool ShouldTracePresentedGuestImageContentsForDiagnostics() + { + var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES"); + return string.Equals(mode, "1", StringComparison.Ordinal) || + string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase); + } + + private void RecordTranslatedGraphicsPass( + TranslatedDrawResources resources, + RenderPass renderPass, + Framebuffer framebuffer, + Extent2D extent) + { + var clearValue = default(ClearValue); + var renderPassInfo = new RenderPassBeginInfo + { + SType = StructureType.RenderPassBeginInfo, + RenderPass = renderPass, + Framebuffer = framebuffer, + RenderArea = new Rect2D(new Offset2D(0, 0), extent), + ClearValueCount = 1, + PClearValues = &clearValue, + }; + _vk.CmdBeginRenderPass( + _commandBuffer, + &renderPassInfo, + SubpassContents.Inline); + _vk.CmdBindPipeline( + _commandBuffer, + PipelineBindPoint.Graphics, + resources.Pipeline); + if (resources.DescriptorSet.Handle != 0) + { + var descriptorSet = resources.DescriptorSet; + _vk.CmdBindDescriptorSets( + _commandBuffer, + PipelineBindPoint.Graphics, + resources.PipelineLayout, + 0, + 1, + &descriptorSet, + 0, + null); + } + + var drawScissor = ClampScissor(resources.Scissor, extent); + if (drawScissor.Width == 0 || drawScissor.Height == 0) + { + _vk.CmdEndRenderPass(_commandBuffer); + return; + } + + var drawViewport = ClampViewport(resources.Viewport, extent); + _vk.CmdSetViewport(_commandBuffer, 0, 1, &drawViewport); + if (resources.VertexBuffers.Length != 0) + { + var buffers = stackalloc VkBuffer[resources.VertexBuffers.Length]; + var offsets = stackalloc ulong[resources.VertexBuffers.Length]; + for (var index = 0; index < resources.VertexBuffers.Length; index++) + { + buffers[index] = resources.VertexBuffers[index].Buffer; + offsets[index] = GetVertexBindingOffset(resources.VertexBuffers[index]); + } + + _vk.CmdBindVertexBuffers( + _commandBuffer, + 0, + (uint)resources.VertexBuffers.Length, + buffers, + offsets); + } + + const uint maxPixelsPerDraw = 512 * 512; + var rowsPerDraw = Math.Max( + 1u, + Math.Min(drawScissor.Height, maxPixelsPerDraw / Math.Max(drawScissor.Width, 1u))); + var drawCount = 0u; + for (var y = 0u; y < drawScissor.Height; y += rowsPerDraw) + { + var scissor = new Rect2D( + new Offset2D( + drawScissor.X, + checked(drawScissor.Y + (int)y)), + new Extent2D( + drawScissor.Width, + Math.Min(rowsPerDraw, drawScissor.Height - y))); + _vk.CmdSetScissor(_commandBuffer, 0, 1, &scissor); + + if (resources.IndexBuffer.Handle != 0) + { + _vk.CmdBindIndexBuffer( + _commandBuffer, + resources.IndexBuffer, + 0, + resources.Index32Bit ? IndexType.Uint32 : IndexType.Uint16); + _vk.CmdDrawIndexed( + _commandBuffer, + resources.VertexCount, + resources.InstanceCount, + 0, + 0, + 0); + } + else + { + _vk.CmdDraw( + _commandBuffer, + resources.VertexCount, + resources.InstanceCount, + 0, + 0); + } + + drawCount++; + } + + if (drawCount > 1) + { + TraceVulkanShader( + $"vk.graphics_chunked target={extent.Width}x{extent.Height} " + + $"draws={drawCount} rows={rowsPerDraw} " + + $"scissor={drawScissor.X},{drawScissor.Y},{drawScissor.Width}x{drawScissor.Height} " + + $"viewport={drawViewport.X:0.###},{drawViewport.Y:0.###}," + + $"{drawViewport.Width:0.###}x{drawViewport.Height:0.###} " + + $"name={resources.DebugName}"); + } + _vk.CmdEndRenderPass(_commandBuffer); + } + + private void DestroyTranslatedDrawResources(TranslatedDrawResources resources) + { + foreach (var texture in resources.Textures) + { + if (texture is null) + { + continue; + } + + if (texture.OwnsStorage && texture.View.Handle != 0) + { + _vk.DestroyImageView(_device, texture.View, null); + } + + if (texture.OwnsStorage && texture.Image.Handle != 0) + { + _vk.DestroyImage(_device, texture.Image, null); + } + + if (texture.OwnsStorage && texture.ImageMemory.Handle != 0) + { + _vk.FreeMemory(_device, texture.ImageMemory, null); + } + + if (texture.StagingBuffer.Handle != 0) + { + _vk.DestroyBuffer(_device, texture.StagingBuffer, null); + } + + if (texture.StagingMemory.Handle != 0) + { + _vk.FreeMemory(_device, texture.StagingMemory, null); + } + + if (texture.Sampler.Handle != 0) + { + _vk.DestroySampler(_device, texture.Sampler, null); + } + + if (texture.NeedsUpload && + texture.GuestImage is { Initialized: false } guestImage) + { + guestImage.InitialUploadPending = false; + } + } + + foreach (var globalBuffer in resources.GlobalMemoryBuffers) + { + if (globalBuffer is null) + { + continue; + } + + if (globalBuffer.Buffer.Handle != 0) + { + _vk.DestroyBuffer(_device, globalBuffer.Buffer, null); + } + + if (globalBuffer.Memory.Handle != 0) + { + _vk.FreeMemory(_device, globalBuffer.Memory, null); + } + } + + foreach (var vertexBuffer in resources.VertexBuffers) + { + if (vertexBuffer is null) + { + continue; + } + + if (vertexBuffer.Buffer.Handle != 0) + { + _vk.DestroyBuffer(_device, vertexBuffer.Buffer, null); + } + + if (vertexBuffer.Memory.Handle != 0) + { + _vk.FreeMemory(_device, vertexBuffer.Memory, null); + } + } + + if (resources.IndexBuffer.Handle != 0) + { + _vk.DestroyBuffer(_device, resources.IndexBuffer, null); + } + + if (resources.IndexMemory.Handle != 0) + { + _vk.FreeMemory(_device, resources.IndexMemory, null); + } + + if (!resources.PipelineCached && resources.Pipeline.Handle != 0) + { + _vk.DestroyPipeline(_device, resources.Pipeline, null); + } + + if (resources.DescriptorPool.Handle != 0) + { + _vk.DestroyDescriptorPool(_device, resources.DescriptorPool, null); + } + + if (resources.PipelineLayout.Handle != 0) + { + _vk.DestroyPipelineLayout(_device, resources.PipelineLayout, null); + } + + if (resources.DescriptorSetLayout.Handle != 0) + { + _vk.DestroyDescriptorSetLayout(_device, resources.DescriptorSetLayout, null); } } @@ -1033,17 +5400,281 @@ internal static unsafe class VulkanVideoPresenter &toPresent); } + private void RecordGuestImageBlit( + uint imageIndex, + GuestImageResource source) + { + var traceDestination = + ShouldTracePresentedGuestImageContentsForDiagnostics() && + !_tracedPresentedSwapchain; + _tracedPresentedSwapchain |= traceDestination; + BeginDebugLabel( + _commandBuffer, + $"SharpEmu present image 0x{source.Address:X16}"); + + var sourceToTransfer = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.ShaderReadBit, + DstAccessMask = AccessFlags.TransferReadBit, + OldLayout = ImageLayout.ShaderReadOnlyOptimal, + NewLayout = ImageLayout.TransferSrcOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = source.Image, + SubresourceRange = ColorSubresourceRange(), + }; + var destinationToTransfer = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = _imageInitialized[imageIndex] + ? AccessFlags.MemoryReadBit + : 0, + DstAccessMask = AccessFlags.TransferWriteBit, + OldLayout = _imageInitialized[imageIndex] + ? ImageLayout.PresentSrcKhr + : ImageLayout.Undefined, + NewLayout = ImageLayout.TransferDstOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = _swapchainImages[imageIndex], + SubresourceRange = ColorSubresourceRange(), + }; + var barriers = stackalloc ImageMemoryBarrier[2]; + barriers[0] = sourceToTransfer; + barriers[1] = destinationToTransfer; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.AllCommandsBit, + PipelineStageFlags.TransferBit, + 0, + 0, + null, + 0, + null, + 2, + barriers); + + var sourceOffsets = new ImageBlit.SrcOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D( + checked((int)source.Width), + checked((int)source.Height), + 1), + }; + var destinationOffsets = new ImageBlit.DstOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D( + checked((int)_extent.Width), + checked((int)_extent.Height), + 1), + }; + var region = new ImageBlit + { + SrcSubresource = new ImageSubresourceLayers( + ImageAspectFlags.ColorBit, + 0, + 0, + 1), + SrcOffsets = sourceOffsets, + DstSubresource = new ImageSubresourceLayers( + ImageAspectFlags.ColorBit, + 0, + 0, + 1), + DstOffsets = destinationOffsets, + }; + _vk.CmdBlitImage( + _commandBuffer, + source.Image, + ImageLayout.TransferSrcOptimal, + _swapchainImages[imageIndex], + ImageLayout.TransferDstOptimal, + 1, + ®ion, + Filter.Nearest); + + if (traceDestination) + { + var destinationToReadback = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.TransferWriteBit, + DstAccessMask = AccessFlags.TransferReadBit, + OldLayout = ImageLayout.TransferDstOptimal, + NewLayout = ImageLayout.TransferSrcOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = _swapchainImages[imageIndex], + SubresourceRange = ColorSubresourceRange(), + }; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.TransferBit, + PipelineStageFlags.TransferBit, + 0, + 0, + null, + 0, + null, + 1, + &destinationToReadback); + + var copyRegion = new BufferImageCopy + { + ImageSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ColorBit, + LayerCount = 1, + }, + ImageExtent = new Extent3D(_extent.Width, _extent.Height, 1), + }; + _vk.CmdCopyImageToBuffer( + _commandBuffer, + _swapchainImages[imageIndex], + ImageLayout.TransferSrcOptimal, + _stagingBuffer, + 1, + ©Region); + _swapchainReadbackPending = true; + } + + var sourceToShaderRead = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = AccessFlags.TransferReadBit, + DstAccessMask = AccessFlags.ShaderReadBit, + OldLayout = ImageLayout.TransferSrcOptimal, + NewLayout = ImageLayout.ShaderReadOnlyOptimal, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = source.Image, + SubresourceRange = ColorSubresourceRange(), + }; + var destinationToPresent = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = traceDestination + ? AccessFlags.TransferReadBit + : AccessFlags.TransferWriteBit, + DstAccessMask = AccessFlags.MemoryReadBit, + OldLayout = traceDestination + ? ImageLayout.TransferSrcOptimal + : ImageLayout.TransferDstOptimal, + NewLayout = ImageLayout.PresentSrcKhr, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = _swapchainImages[imageIndex], + SubresourceRange = ColorSubresourceRange(), + }; + barriers[0] = sourceToShaderRead; + barriers[1] = destinationToPresent; + _vk.CmdPipelineBarrier( + _commandBuffer, + PipelineStageFlags.TransferBit, + PipelineStageFlags.AllCommandsBit, + 0, + 0, + null, + 0, + null, + 2, + barriers); + EndDebugLabel(_commandBuffer); + } + + private void TraceSwapchainReadback() + { + _swapchainReadbackPending = false; + var byteCount = checked((ulong)_extent.Width * _extent.Height * 4); + void* mapped; + Check( + _vk.MapMemory(_device, _stagingMemory, 0, byteCount, 0, &mapped), + "vkMapMemory(swapchain readback)"); + try + { + var bytes = new ReadOnlySpan(mapped, checked((int)byteCount)); + var nonzeroBytes = 0L; + var nonblackPixels = 0L; + ulong hash = 14695981039346656037UL; + for (var offset = 0; offset < bytes.Length; offset += 4) + { + var b0 = bytes[offset]; + var b1 = bytes[offset + 1]; + var b2 = bytes[offset + 2]; + var b3 = bytes[offset + 3]; + nonzeroBytes += b0 == 0 ? 0 : 1; + nonzeroBytes += b1 == 0 ? 0 : 1; + nonzeroBytes += b2 == 0 ? 0 : 1; + nonzeroBytes += b3 == 0 ? 0 : 1; + nonblackPixels += b0 != 0 || b1 != 0 || b2 != 0 ? 1 : 0; + hash = (hash ^ b0) * 1099511628211UL; + hash = (hash ^ b1) * 1099511628211UL; + hash = (hash ^ b2) * 1099511628211UL; + hash = (hash ^ b3) * 1099511628211UL; + } + + Console.Error.WriteLine( + $"[LOADER][TRACE] vk.swapchain_image size={_extent.Width}x{_extent.Height} " + + $"format={_swapchainFormat} nonzero_bytes={nonzeroBytes}/{byteCount} " + + $"nonblack_pixels={nonblackPixels}/{(ulong)_extent.Width * _extent.Height} " + + $"hash=0x{hash:X16}"); + } + finally + { + _vk.UnmapMemory(_device, _stagingMemory); + } + } + private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities) { if (capabilities.CurrentExtent.Width != uint.MaxValue) { - return capabilities.CurrentExtent; + var fallbackWidth = _extent.Width != 0 + ? _extent.Width + : DefaultWindowWidth; + var fallbackHeight = _extent.Height != 0 + ? _extent.Height + : DefaultWindowHeight; + return new Extent2D( + ClampSurfaceExtent( + capabilities.CurrentExtent.Width, + fallbackWidth, + capabilities.MinImageExtent.Width, + capabilities.MaxImageExtent.Width), + ClampSurfaceExtent( + capabilities.CurrentExtent.Height, + fallbackHeight, + capabilities.MinImageExtent.Height, + capabilities.MaxImageExtent.Height)); } var size = _window.FramebufferSize; return new Extent2D( - Math.Clamp((uint)Math.Max(size.X, 1), capabilities.MinImageExtent.Width, capabilities.MaxImageExtent.Width), - Math.Clamp((uint)Math.Max(size.Y, 1), capabilities.MinImageExtent.Height, capabilities.MaxImageExtent.Height)); + ClampSurfaceExtent( + (uint)Math.Max(size.X, 1), + DefaultWindowWidth, + capabilities.MinImageExtent.Width, + capabilities.MaxImageExtent.Width), + ClampSurfaceExtent( + (uint)Math.Max(size.Y, 1), + DefaultWindowHeight, + capabilities.MinImageExtent.Height, + capabilities.MaxImageExtent.Height)); + } + + private static uint ClampSurfaceExtent( + uint value, + uint fallback, + uint minimum, + uint maximum) + { + value = value <= 1 && fallback > 1 ? fallback : value; + minimum = Math.Max(minimum, 1u); + maximum = Math.Max(maximum, minimum); + return Math.Clamp(value, minimum, maximum); } private static SurfaceFormatKHR ChooseSurfaceFormat(IReadOnlyList formats) @@ -1081,11 +5712,14 @@ internal static unsafe class VulkanVideoPresenter throw new InvalidOperationException("The Vulkan surface exposes no composite alpha mode."); } - private static ImageSubresourceRange ColorSubresourceRange() => + private static ImageSubresourceRange ColorSubresourceRange( + uint baseMipLevel = 0, + uint levelCount = 1) => new() { AspectMask = ImageAspectFlags.ColorBit, - LevelCount = 1, + BaseMipLevel = baseMipLevel, + LevelCount = levelCount, LayerCount = 1, }; @@ -1116,29 +5750,119 @@ internal static unsafe class VulkanVideoPresenter _vulkanReady = false; _vk.DeviceWaitIdle(_device); + CollectCompletedGuestSubmissions(waitForOldest: false); + foreach (var pipeline in _computePipelines.Values) + { + _vk.DestroyPipeline(_device, pipeline, null); + } + _computePipelines.Clear(); + foreach (var guestImage in _guestImages.Values) + { + DestroyGuestImage(guestImage); + } + _guestImages.Clear(); + lock (_gate) + { + _availableGuestImages.Clear(); + } + DestroySwapchainResources(); + if (_device.Handle != 0) + { + _vk.DestroyDevice(_device, null); + _device = default; + } + if (_surface.Handle != 0) + { + _surfaceApi.DestroySurface(_instance, _surface, null); + _surface = default; + } + if (_instance.Handle != 0) + { + _vk.DestroyInstance(_instance, null); + _instance = default; + } + } + + private void RecreateSwapchainResources(string operation, Result result) + { + if (_device.Handle == 0) + { + return; + } + + Check( + _surfaceApi.GetPhysicalDeviceSurfaceCapabilities( + _physicalDevice, + _surface, + out var capabilities), + "vkGetPhysicalDeviceSurfaceCapabilitiesKHR"); + var framebufferSize = _window.FramebufferSize; + var hasFixedExtent = capabilities.CurrentExtent.Width != uint.MaxValue; + var surfaceWidth = hasFixedExtent + ? capabilities.CurrentExtent.Width + : (uint)Math.Max(framebufferSize.X, 0); + var surfaceHeight = hasFixedExtent + ? capabilities.CurrentExtent.Height + : (uint)Math.Max(framebufferSize.Y, 0); + if (surfaceWidth <= 1 || surfaceHeight <= 1) + { + if (!_swapchainRecreateDeferred) + { + _swapchainRecreateDeferred = true; + Console.Error.WriteLine( + $"[LOADER][INFO] Vulkan VideoOut deferred swapchain recreation: " + + $"surface={surfaceWidth}x{surfaceHeight}"); + } + + return; + } + + _swapchainRecreateDeferred = false; + Console.Error.WriteLine( + $"[LOADER][INFO] Vulkan VideoOut recreating swapchain after {operation}: {result}"); + _vk.DeviceWaitIdle(_device); + CollectCompletedGuestSubmissions(waitForOldest: false); + DestroySwapchainResources(); + CreateSwapchain(); + CreateCommandResources(); + CreateGuestDrawResources(); + Console.Error.WriteLine( + $"[LOADER][INFO] Vulkan VideoOut recreated swapchain: " + + $"{_extent.Width}x{_extent.Height}, format={_swapchainFormat}"); + } + + private void DestroySwapchainResources() + { if (_stagingBuffer.Handle != 0) { _vk.DestroyBuffer(_device, _stagingBuffer, null); + _stagingBuffer = default; } if (_stagingMemory.Handle != 0) { _vk.FreeMemory(_device, _stagingMemory, null); + _stagingMemory = default; + _stagingSize = 0; } if (_imageAvailable.Handle != 0) { _vk.DestroySemaphore(_device, _imageAvailable, null); + _imageAvailable = default; } if (_renderFinished.Handle != 0) { _vk.DestroySemaphore(_device, _renderFinished, null); + _renderFinished = default; } if (_barycentricPipeline.Handle != 0) { _vk.DestroyPipeline(_device, _barycentricPipeline, null); + _barycentricPipeline = default; } if (_pipelineLayout.Handle != 0) { _vk.DestroyPipelineLayout(_device, _pipelineLayout, null); + _pipelineLayout = default; } foreach (var framebuffer in _framebuffers) { @@ -1150,6 +5874,7 @@ internal static unsafe class VulkanVideoPresenter if (_renderPass.Handle != 0) { _vk.DestroyRenderPass(_device, _renderPass, null); + _renderPass = default; } foreach (var imageView in _swapchainImageViews) { @@ -1161,23 +5886,30 @@ internal static unsafe class VulkanVideoPresenter if (_commandPool.Handle != 0) { _vk.DestroyCommandPool(_device, _commandPool, null); + _commandPool = default; + _commandBuffer = default; + _presentationCommandBuffer = default; } if (_swapchain.Handle != 0) { _swapchainApi.DestroySwapchain(_device, _swapchain, null); + _swapchain = default; } - if (_device.Handle != 0) + + _swapchainImages = []; + _swapchainImageViews = []; + _framebuffers = []; + _imageInitialized = []; + } + + private static void CheckSwapchainResult(Result result, string operation) + { + if (result is Result.Success or Result.SuboptimalKhr) { - _vk.DestroyDevice(_device, null); - } - if (_surface.Handle != 0) - { - _surfaceApi.DestroySurface(_instance, _surface, null); - } - if (_instance.Handle != 0) - { - _vk.DestroyInstance(_instance, null); + return; } + + throw new InvalidOperationException($"{operation} failed with {result}."); } private static void Check(Result result, string operation) @@ -1187,5 +5919,16 @@ internal static unsafe class VulkanVideoPresenter throw new InvalidOperationException($"{operation} failed with {result}."); } } + + private static void TraceVulkanShader(string message) + { + if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) && + !string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC_SHADER"), "1", StringComparison.Ordinal)) + { + return; + } + + Console.Error.WriteLine($"[LOADER][TRACE] {message}"); + } } }