sharpemu/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Pipeli...

1272 lines
63 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.VideoOut;
using SharpEmu.HLE.GpuMemory;
using System.Diagnostics;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Pipelines;
using SharpEmu.Libs.Gpu.Rendering;
using SharpEmu.Libs.Gpu.ShaderCache;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Resources;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Core.Native;
using Silk.NET.Vulkan;
using Silk.NET.Vulkan.Extensions.KHR;
// This partial creates the graphics and compute pipelines of the shader cache and the presentation pipeline.
internal static unsafe partial class VulkanVideoPresenter
{
private const string PushDescriptorExtensionName = "VK_KHR_push_descriptor";
private sealed partial class Presenter : IShaderPipelineHost
{
private const string FullscreenBarycentricVertexSpirv =
"AwIjBwAAAQALAAgAMgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ACAAAAAAABAAAAG1haW4AAAAADQAAABoAAAApAAAAAwADAAIAAADCAQAABQAEAAQAAABtYWluAAAAAAUABgALAAAAZ2xfUGVyVmVydGV4AAAAAAYABgALAAAAAAAAAGdsX1Bvc2l0aW9uAAYABwALAAAAAQAAAGdsX1BvaW50U2l6ZQAAAAAGAAcACwAAAAIAAABnbF9DbGlwRGlzdGFuY2UABgAHAAsAAAADAAAAZ2xfQ3VsbERpc3RhbmNlAAUAAwANAAAAAAAAAAUABgAaAAAAZ2xfVmVydGV4SW5kZXgAAAUABQAdAAAAaW5kZXhhYmxlAAAABQAFACkAAABiYXJ5Y2VudHJpYwAFAAUALwAAAGluZGV4YWJsZQAAAEcAAwALAAAAAgAAAEgABQALAAAAAAAAAAsAAAAAAAAASAAFAAsAAAABAAAACwAAAAEAAABIAAUACwAAAAIAAAALAAAAAwAAAEgABQALAAAAAwAAAAsAAAAEAAAARwAEABoAAAALAAAAKgAAAEcABAApAAAAHgAAAAAAAAATAAIAAgAAACEAAwADAAAAAgAAABYAAwAGAAAAIAAAABcABAAHAAAABgAAAAQAAAAVAAQACAAAACAAAAAAAAAAKwAEAAgAAAAJAAAAAQAAABwABAAKAAAABgAAAAkAAAAeAAYACwAAAAcAAAAGAAAACgAAAAoAAAAgAAQADAAAAAMAAAALAAAAOwAEAAwAAAANAAAAAwAAABUABAAOAAAAIAAAAAEAAAArAAQADgAAAA8AAAAAAAAAFwAEABAAAAAGAAAAAgAAACsABAAIAAAAEQAAAAMAAAAcAAQAEgAAABAAAAARAAAAKwAEAAYAAAATAAAAAACAvywABQAQAAAAFAAAABMAAAATAAAAKwAEAAYAAAAVAAAAAABAQCwABQAQAAAAFgAAABUAAAATAAAALAAFABAAAAAXAAAAEwAAABUAAAAsAAYAEgAAABgAAAAUAAAAFgAAABcAAAAgAAQAGQAAAAEAAAAOAAAAOwAEABkAAAAaAAAAAQAAACAABAAcAAAABwAAABIAAAAgAAQAHgAAAAcAAAAQAAAAKwAEAAYAAAAhAAAAAAAAACsABAAGAAAAIgAAAAAAgD8gAAQAJgAAAAMAAAAHAAAAIAAEACgAAAADAAAAEAAAADsABAAoAAAAKQAAAAMAAAAsAAUAEAAAACoAAAAiAAAAIQAAACwABQAQAAAAKwAAACEAAAAiAAAALAAFABAAAAAsAAAAIQAAACEAAAAsAAYAEgAAAC0AAAAqAAAAKwAAACwAAAA2AAUAAgAAAAQAAAAAAAAAAwAAAPgAAgAFAAAAOwAEABwAAAAdAAAABwAAADsABAAcAAAALwAAAAcAAAA9AAQADgAAABsAAAAaAAAAPgADAB0AAAAYAAAAQQAFAB4AAAAfAAAAHQAAABsAAAA9AAQAEAAAACAAAAAfAAAAUQAFAAYAAAAjAAAAIAAAAAAAAABRAAUABgAAACQAAAAgAAAAAQAAAFAABwAHAAAAJQAAACMAAAAkAAAAIQAAACIAAABBAAUAJgAAACcAAAANAAAADwAAAD4AAwAnAAAAJQAAAD0ABAAOAAAALgAAABoAAAA+AAMALwAAAC0AAABBAAUAHgAAADAAAAAvAAAALgAAAD0ABAAQAAAAMQAAADAAAAA+AAMAKQAAADEAAAD9AAEAOAABAA==";
private const string FullscreenBarycentricFragmentSpirv =
"AwIjBwAAAQALAAgAEgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ABwAEAAAABAAAAG1haW4AAAAACQAAAAwAAAAQAAMABAAAAAcAAAADAAMAAgAAAMIBAAAFAAQABAAAAG1haW4AAAAABQAFAAkAAABvdXRDb2xvcgAAAAAFAAUADAAAAGJhcnljZW50cmljAEcABAAJAAAAHgAAAAAAAABHAAQADAAAAB4AAAAAAAAAEwACAAIAAAAhAAMAAwAAAAIAAAAWAAMABgAAACAAAAAXAAQABwAAAAYAAAAEAAAAIAAEAAgAAAADAAAABwAAADsABAAIAAAACQAAAAMAAAAXAAQACgAAAAYAAAACAAAAIAAEAAsAAAABAAAACgAAADsABAALAAAADAAAAAEAAAArAAQABgAAAA4AAAAAAAAANgAFAAIAAAAEAAAAAAAAAAMAAAD4AAIABQAAAD0ABAAKAAAADQAAAAwAAABRAAUABgAAAA8AAAANAAAAAAAAAFEABQAGAAAAEAAAAA0AAAABAAAAUAAHAAcAAAARAAAADwAAABAAAAAOAAAADgAAAD4AAwAJAAAAEQAAAP0AAQA4AAEA";
private const uint PushConstantBytes = PushData.ByteSize;
private static int _shaderModuleDumpSequence;
private static int _vertexSwizzleLines;
private static int _vertexOutOfBoundsLines;
// One created pipeline with the layout it binds and the description its rectangle-list variants derive from.
private sealed class RenderPipelineEntry
{
public ulong Id;
public Pipeline Pipeline;
public PipelineLayout Layout;
public DescriptorSetLayout SetLayout;
public DescriptorSetDemand Demand;
public bool UsesPushDescriptors;
public GraphicsPipelineDescription? Description;
public Pipeline StripVariant;
public Pipeline ListVariant;
public Pipeline RectangleVariant;
public ulong ProfileVertexHash;
public ulong ProfilePixelHash;
public ulong ProfileComputeHash;
public bool RectangleList => Description is { StaticParameters.Topology: PrimitiveTopology.PatchList };
}
private readonly Dictionary<ulong, ShaderModule> _shaderModules = new();
private readonly Dictionary<ulong, int> _shaderModuleSpirvBytes = new();
private readonly Dictionary<ulong, string> _shaderModuleCacheIdentities = new();
private long _pipelineCreationMilliseconds;
private KhrPushDescriptor _pushDescriptorApi = null!;
private uint _maxPushDescriptors;
private SampleCountFlags _noAttachmentSampleCounts;
private DescriptorHeap _descriptorHeap = null!;
uint IShaderPipelineHost.MaxPushDescriptors => _maxPushDescriptors;
// Wave64 compute runs natively when the device's subgroup is that wide; smaller devices emulate it.
bool IShaderPipelineHost.ComputeWave64Supported => Volatile.Read(ref _nativeSubgroupSize) >= 64;
// Only a 64-invocation wave64 workgroup is translated for either host subgroup width. Every
// other compute translation maps a guest wave to 32-lane host subgroups, and a 64-lane host
// subgroup (AMD's default) left lanes 32..63 inactive: a wave64 8x8x8 group lost rows 4..7.
private const uint RdnaSubgroupSize = 32;
private bool _canRequireComputeSubgroup32;
private uint _maxComputeWorkgroupSubgroups;
private bool RequiresComputeSubgroup32(ComputeInputInfo input)
{
var invocations = (ulong)Math.Max(input.ThreadsX, 1) * Math.Max(input.ThreadsY, 1) * Math.Max(input.ThreadsZ, 1);
return _canRequireComputeSubgroup32 &&
!(input.WaveSize == 64 && invocations == 64) &&
invocations <= (ulong)_maxComputeWorkgroupSubgroups * RdnaSubgroupSize;
}
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => GraphicsSubgroupOperationsEnabled;
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => SharedInt64AtomicsEnabled;
bool IShaderPipelineHost.ShaderSignedZeroInfNanPreserveFloat32Supported =>
_supportsShaderSignedZeroInfNanPreserveFloat32;
// NVIDIA's compiler rejects the elided-EXEC wave64 compute module with NVVM error 3.
bool IShaderPipelineHost.ExecGuardElisionEnabled => _physicalDeviceVendorId != NvidiaVendorId;
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;
bool IShaderPipelineHost.ClipDistanceEnabled => _supportsShaderClipDistance;
bool IShaderPipelineHost.PortableBufferDescriptors => true;
RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits;
SampleCountFlags IShaderPipelineHost.NoAttachmentSampleCounts => _noAttachmentSampleCounts;
// A range the GPU wrote is downloaded first, so the word is what the guest CPU would read.
public bool TryReadGuestWord(ulong address, out uint word)
{
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.GuestRead);
word = 0;
var synchronized = false;
if (IsCleanReadPage(address, sizeof(uint)))
{
if (TryGetAliasPointer(address, sizeof(uint), out var alias))
{
word = System.Runtime.CompilerServices.Unsafe.ReadUnaligned<uint>(alias);
return true;
}
}
else
{
// Resource planning can inspect a dynamic descriptor before the draw has
// supplied a valid guest address. Do not pass an invalid range to the
// page tracker: it treats that as an emulator invariant violation and
// terminates the process. A failed read lets the materializer reject or
// specialize the source normally.
if (!_guestMemory.CanRead(address, sizeof(uint)))
{
return false;
}
if (_bufferCache.HasGpuDirtyBytes(address, sizeof(uint)))
{
if (Diagnostics.GpuReadTrace.Enabled)
{
Diagnostics.GpuReadTrace.Record(address, ResourceMaterializationCache.ReadingTable);
}
if (!_bufferCache.TrySynchronizeCpuRead(address, sizeof(uint),
SharpEmu.HLE.GuestMemory.GuestMemoryProfile.ReadbackSource.ShaderResourceRead))
{
return false;
}
synchronized = true;
}
else
{
NoteCleanReadPage(address, sizeof(uint));
}
}
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!_guestMemory.TryRead(address, bytes))
{
return false;
}
word = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes);
if (Diagnostics.GpuReadTrace.Enabled && synchronized)
{
Diagnostics.GpuReadTrace.RecordValue(address, word);
}
return true;
}
// Refused while a GPU buffer or image write may still own the range.
public bool TryReadCleanGuestWord(ulong address, out uint word)
{
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.CleanGuestRead);
word = 0;
if (!_guestMemory.CanRead(address, sizeof(uint)))
{
return false;
}
if ((_bufferCache.MayHaveGpuDirtyPages(address, sizeof(uint)) && _bufferCache.HasGpuDirtyPages(address, sizeof(uint))) ||
_bufferCache.HasGpuDirtyBytes(address, sizeof(uint)) ||
_imageCache.HasGpuModifiedImageBytes(address, sizeof(uint)))
{
return false;
}
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!_guestMemory.TryRead(address, bytes))
{
return false;
}
word = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes);
return true;
}
// The same ownership rules as the two word readers, checked once for a whole range.
public bool TryReadResidentGuestBytes(ulong address, Span<byte> destination, bool clean)
{
var size = (ulong)destination.Length;
if (clean || !IsCleanReadPage(address, size))
{
if (!_guestMemory.CanRead(address, size) ||
_bufferCache.HasGpuDirtyBytes(address, size) ||
(clean && (_bufferCache.HasGpuDirtyPages(address, size) || _imageCache.HasGpuModifiedImageBytes(address, size))))
{
return false;
}
NoteCleanReadPage(address, size);
}
if (TryGetAliasPointer(address, size, out var alias))
{
new ReadOnlySpan<byte>(alias, destination.Length).CopyTo(destination);
return true;
}
return _guestMemory.TryRead(address, destination);
}
private const ulong CleanReadPageBytes = 0x1000;
private const int CleanReadPageSlots = 64;
private CleanReadPages? _cleanReadPages;
private bool _backingAliasAccess;
private sealed class CleanReadPages
{
public readonly ulong[] Tags = new ulong[CleanReadPageSlots];
public readonly long[] Versions = new long[CleanReadPageSlots];
public readonly ulong[] Aliases = new ulong[CleanReadPageSlots];
public readonly object?[] Snapshots = new object?[CleanReadPageSlots];
}
private bool TryGetAliasPointer(ulong address, ulong size, out byte* pointer)
{
pointer = null;
if (!_backingAliasAccess || _cleanReadPages is not { } pages ||
!TryGetCleanReadPage(address, size, out var page, out var slot) || pages.Tags[slot] != page + 1 ||
_guestBacking.BackingAliasSnapshot is not { } snapshot)
{
return false;
}
if (pages.Aliases[slot] == 0 || !ReferenceEquals(pages.Snapshots[slot], snapshot))
{
if (!_guestBacking.TryResolveBackingAlias(page, CleanReadPageBytes, out var resolved) || resolved == 0)
{
return false;
}
pages.Aliases[slot] = resolved;
pages.Snapshots[slot] = snapshot;
}
pointer = (byte*)(pages.Aliases[slot] + (address - page));
return true;
}
private static bool TryGetCleanReadPage(ulong address, ulong size, out ulong page, out int slot)
{
page = address & ~(CleanReadPageBytes - 1);
slot = (int)((page / CleanReadPageBytes) & (CleanReadPageSlots - 1));
return size != 0 && address + size > address && ((address + size - 1) & ~(CleanReadPageBytes - 1)) == page;
}
private bool IsCleanReadPage(ulong address, ulong size) =>
_cleanReadPages is { } pages &&
TryGetCleanReadPage(address, size, out var page, out var slot) &&
pages.Tags[slot] == page + 1 &&
pages.Versions[slot] == _bufferCache.GpuModifiedVersion;
private void NoteCleanReadPage(ulong address, ulong size)
{
if (!TryGetCleanReadPage(address, size, out var page, out var slot))
{
return;
}
var version = _bufferCache.GpuModifiedVersion;
if (!_guestMemory.CanRead(page, CleanReadPageBytes) || _bufferCache.HasGpuDirtyBytes(page, CleanReadPageBytes))
{
return;
}
var pages = _cleanReadPages ??= new CleanReadPages();
if (pages.Tags[slot] != page + 1)
{
pages.Tags[slot] = page + 1;
pages.Aliases[slot] = 0;
pages.Snapshots[slot] = null;
}
pages.Versions[slot] = version;
}
public ulong CreateShaderModule(IGuestCompiledShader shader, ShaderStage stage, ulong hash, ulong programId)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.ProgramCompile);
var module = CreateShaderModule(shader.Payload);
SetDebugName(ObjectType.ShaderModule, module.Handle, $"SharpEmu {stage} 0x{hash:X16}");
_shaderModules.Add(programId, module);
_shaderModuleSpirvBytes[module.Handle] = shader.Payload.Length;
_shaderModuleCacheIdentities[module.Handle] = VulkanPipelineCacheStorage.CompiledShaderIdentity(shader.Payload);
return module.Handle;
}
// The Metal shader compiler can spend seconds on one translated program and the
// command stream cannot advance while it does, so a slow creation is reported with
// the SPIR-V size that produced it.
private const long SlowPipelineCreationMilliseconds = 250;
private int SpirvBytesOf(ulong moduleHandle) =>
_shaderModuleSpirvBytes.TryGetValue(moduleHandle, out var bytes) ? bytes : -1;
private void ReportPipelineCreation(long elapsedMilliseconds, string kind, string stages, string spirv, bool unoptimized = false)
{
Interlocked.Add(ref _pipelineCreationMilliseconds, elapsedMilliseconds);
if (elapsedMilliseconds < SlowPipelineCreationMilliseconds)
{
return;
}
Console.Error.WriteLine(
$"[GPU][WARN] Slow pipeline creation: kind={kind} {stages} spirv_bytes={spirv} " +
$"ms={elapsedMilliseconds}{(unoptimized ? " unoptimized" : string.Empty)} " +
$"total_s={Interlocked.Read(ref _pipelineCreationMilliseconds) / 1000.0:F1}");
}
private void CreateBarycentricPipeline()
{
var vertexBytes = Convert.FromBase64String(FullscreenBarycentricVertexSpirv);
var fragmentBytes = Convert.FromBase64String(FullscreenBarycentricFragmentSpirv);
var vertexModule = CreateShaderModule(vertexBytes);
var fragmentModule = CreateShaderModule(fragmentBytes);
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 vertexInput = new PipelineVertexInputStateCreateInfo
{
SType = StructureType.PipelineVertexInputStateCreateInfo,
};
var inputAssembly = new PipelineInputAssemblyStateCreateInfo
{
SType = StructureType.PipelineInputAssemblyStateCreateInfo,
Topology = PrimitiveTopology.TriangleList,
};
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
{
ColorWriteMask =
ColorComponentFlags.RBit |
ColorComponentFlags.GBit |
ColorComponentFlags.BBit |
ColorComponentFlags.ABit,
};
var colorBlend = new PipelineColorBlendStateCreateInfo
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
AttachmentCount = 1,
PAttachments = &colorBlendAttachment,
};
var pipelineInfo = new GraphicsPipelineCreateInfo
{
SType = StructureType.GraphicsPipelineCreateInfo,
StageCount = 2,
PStages = shaderStages,
PVertexInputState = &vertexInput,
PInputAssemblyState = &inputAssembly,
PViewportState = &viewportState,
PRasterizationState = &rasterization,
PMultisampleState = &multisample,
PColorBlendState = &colorBlend,
Layout = _pipelineLayout,
RenderPass = _renderPass,
Subpass = 0,
};
Check(
_vk.CreateGraphicsPipelines(
_device,
_pipelineCache,
1,
&pipelineInfo,
null,
out _barycentricPipeline),
"vkCreateGraphicsPipelines");
MarkPipelineCacheDirty();
}
finally
{
SilkMarshal.Free((nint)entryPoint);
_vk.DestroyShaderModule(_device, fragmentModule, null);
_vk.DestroyShaderModule(_device, vertexModule, null);
}
}
private ShaderModule CreateShaderModule(byte[] code)
{
if (!_supportsFragmentShaderBarycentric && RequiresFragmentShaderBarycentric(code))
{
throw new NotSupportedException(
"The shader requires the fragmentShaderBarycentric device feature.");
}
string? dumpPath = null;
var dumpDirectory = Environment.GetEnvironmentVariable("SHARPEMU_SHADER_SPIRV_DUMP_DIR");
if (!string.IsNullOrWhiteSpace(dumpDirectory))
{
Directory.CreateDirectory(dumpDirectory);
var sequence = Interlocked.Increment(ref _shaderModuleDumpSequence);
dumpPath = Path.Combine(dumpDirectory, $"{sequence:D4}.spv");
File.WriteAllBytes(dumpPath, code);
_pendingShaderModuleDumpPath = dumpPath;
}
try
{
fixed (byte* codePointer = code)
{
var createInfo = new ShaderModuleCreateInfo
{
SType = StructureType.ShaderModuleCreateInfo,
CodeSize = (nuint)code.Length,
PCode = (uint*)codePointer,
};
Check(
_vk.CreateShaderModule(_device, &createInfo, null, out var module),
"vkCreateShaderModule");
return module;
}
}
finally
{
_pendingShaderModuleDumpPath = null;
}
}
private static bool RequiresFragmentShaderBarycentric(ReadOnlySpan<byte> code)
{
for (var offset = 20; offset + 4 <= code.Length;)
{
var header = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code[offset..]);
var wordCount = (int)(header >> 16);
if (wordCount == 0 || wordCount > (code.Length - offset) / 4)
{
throw new InvalidOperationException("The shader contains an invalid SPIR-V instruction.");
}
if ((header & 0xFFFF) == (uint)SpirvOp.Capability && wordCount == 2 &&
System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(code[(offset + 4)..]) ==
(uint)SpirvCapability.FragmentBarycentricKhr)
{
return true;
}
offset += wordCount * 4;
}
return false;
}
// One layout binding per descriptor binding of the stage, at the stage's native binding numbers.
private static void CollectLayoutBindings(List<DescriptorSetLayoutBinding> bindings, ShaderProgramInfo program, ShaderStage stage) =>
CollectLayoutBindings(
bindings,
program.Bindings ?? throw SubmissionScheduler.Fatal($"The program has no binding layout: hash=0x{program.Hash:X16}."),
stage);
private static void CollectLayoutBindings(List<DescriptorSetLayoutBinding> bindings, BindingLayout layout, ShaderStage stage)
{
foreach (var binding in layout.Descriptors)
{
bindings.Add(new DescriptorSetLayoutBinding
{
Binding = BindingLayout.NativeBindingIndex(stage, binding.Kind),
DescriptorType = DescriptorWriter.DescriptorType(binding.Kind),
DescriptorCount = DescriptorWriter.DescriptorCount(binding),
StageFlags = DescriptorWriter.ShaderStageFlag(stage),
});
}
}
// The set is pushed when its descriptors fit the device limit, else it comes from the heap.
private DescriptorSetLayout CreateDescriptorSetLayout(List<DescriptorSetLayoutBinding> bindings, out bool usesPushDescriptors, out DescriptorSetDemand demand)
{
var descriptorCount = 0u;
demand = default;
foreach (var binding in bindings)
{
descriptorCount += binding.DescriptorCount;
demand = demand.Add(DescriptorSetDemand.Of(binding.DescriptorType, binding.DescriptorCount));
}
usesPushDescriptors = descriptorCount <= _maxPushDescriptors;
var bindingArray = bindings.ToArray();
fixed (DescriptorSetLayoutBinding* bindingPointer = bindingArray)
{
var create = new DescriptorSetLayoutCreateInfo
{
SType = StructureType.DescriptorSetLayoutCreateInfo,
Flags = usesPushDescriptors ? DescriptorSetLayoutCreateFlags.PushDescriptorBitKhr : 0,
BindingCount = (uint)bindingArray.Length,
PBindings = bindingArray.Length == 0 ? null : bindingPointer,
};
Check(_vk.CreateDescriptorSetLayout(_device, &create, null, out var layout), "vkCreateDescriptorSetLayout");
return layout;
}
}
private PipelineLayout CreatePipelineLayout(DescriptorSetLayout setLayout, ShaderStageFlags pushStages)
{
var pushConstants = new PushConstantRange { StageFlags = pushStages, Offset = 0, Size = PushConstantBytes };
var create = new PipelineLayoutCreateInfo
{
SType = StructureType.PipelineLayoutCreateInfo,
SetLayoutCount = 1,
PSetLayouts = &setLayout,
PushConstantRangeCount = 1,
PPushConstantRanges = &pushConstants,
};
Check(_vk.CreatePipelineLayout(_device, &create, null, out var layout), "vkCreatePipelineLayout");
return layout;
}
private PipelineHandle RegisterPipeline(RenderPipelineEntry entry)
{
entry.Id = ++_nextPipelineId;
_pipelineEntries.Add(entry.Id, entry);
if (_gpuCommandProfile is not null)
Console.Error.WriteLine($"[PERF][GPU_PIPELINE] pipeline={entry.Id} vertex=0x{entry.ProfileVertexHash:X16} pixel=0x{entry.ProfilePixelHash:X16} compute=0x{entry.ProfileComputeHash:X16}");
return new PipelineHandle(entry.Id, entry.Layout.Handle, entry.UsesPushDescriptors);
}
public PipelineHandle CreateGraphicsPipeline(GraphicsPipelineDescription description)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.PipelineSetup);
if (description.Rendering.ColorCount > _maxColorAttachments)
{
throw SubmissionScheduler.Fatal($"The draw binds more color attachments than the device supports: count={description.Rendering.ColorCount} max={_maxColorAttachments}.");
}
var bindings = new List<DescriptorSetLayoutBinding>();
CollectLayoutBindings(bindings, description.VertexStage, ShaderStage.Vertex);
if (description.PixelStage is { } pixelStage)
{
CollectLayoutBindings(bindings, pixelStage, ShaderStage.Pixel);
}
var setLayout = CreateDescriptorSetLayout(bindings, out var usesPushDescriptors, out var demand);
const ShaderStageFlags graphicsStages = ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit;
var entry = new RenderPipelineEntry
{
SetLayout = setLayout,
Demand = demand,
Layout = CreatePipelineLayout(setLayout, graphicsStages),
UsesPushDescriptors = usesPushDescriptors,
Description = description,
ProfileVertexHash = description.VertexStage.Hash,
ProfilePixelHash = description.PixelStage?.Hash ?? 0,
};
// Rectangle draws bind a native fill or compatibility variant at draw time.
if (!entry.RectangleList)
{
entry.Pipeline = CreateRenderPipeline(description, description.StaticParameters.Topology, entry.Layout);
}
return RegisterPipeline(entry);
}
private static StencilOpState ToVkStencilOpState(in StencilOperations operations) => new()
{
FailOp = operations.FailOperation,
PassOp = operations.PassOperation,
DepthFailOp = operations.DepthFailOperation,
CompareOp = operations.Compare,
};
// The host format of each attribute, narrowed to the components the program fetches.
private static void BuildVertexAttributes(
GraphicsPipelineDescription description,
VertexInputAttributeDescription[] attributes,
VertexInputBindingDescription[] bindings)
{
var vertexInput = description.VertexInput;
var info = description.VertexInfo;
for (var binding = 0; binding < vertexInput.BindingCount; binding++)
{
bindings[binding] = new VertexInputBindingDescription
{
Binding = (uint)binding,
Stride = vertexInput.Bindings[binding].Stride,
InputRate = vertexInput.Bindings[binding].Instance ? VertexInputRate.Instance : VertexInputRate.Vertex,
};
}
for (var index = 0; index < vertexInput.AttributeCount; index++)
{
var resource = info.Attributes[index];
var descriptor = resource.Descriptor;
var compiledComponents = description.VertexStage.VertexFetchComponents[index];
var usedComponents = compiledComponents > 0 ? compiledComponents : (uint)resource.RegisterCount;
var format = VertexAttributeFormats.Resolve(in descriptor, usedComponents, out var attributeSize);
if (descriptor.OutOfBounds != 0 && Interlocked.Exchange(ref _vertexOutOfBoundsLines, 1) == 0)
{
Console.Error.WriteLine($"[LOADER][INFO] vertex_input accepted the out-of-bounds mode {descriptor.OutOfBounds} of an attribute buffer");
}
if (descriptor.AddThreadId)
{
throw SubmissionScheduler.Fatal($"A vertex attribute buffer adds the thread index: attribute={index} hash=0x{description.VertexStage.Hash:X16}.");
}
if (descriptor.SwizzleEnabled)
{
throw SubmissionScheduler.Fatal($"A vertex attribute buffer is swizzled: attribute={index} hash=0x{description.VertexStage.Hash:X16}.");
}
CheckVertexSwizzle(in descriptor, (int)usedComponents, attributeSize, index);
attributes[index] = new VertexInputAttributeDescription
{
Location = (uint)index,
Binding = vertexInput.Attributes[index].Binding,
Format = format,
Offset = vertexInput.Attributes[index].Offset,
};
}
}
private static uint DestinationSelect(uint x, uint y = 0, uint z = 0, uint w = 0) => x | (y << 3) | (z << 6) | (w << 9);
// A destination select the fixed-function fetch cannot apply is logged once and accepted.
private static void CheckVertexSwizzle(in BufferDescriptorWords descriptor, int fetchedComponents, uint attributeSize, int index)
{
uint swizzle;
uint expected;
var supported = true;
switch (fetchedComponents)
{
case 1:
swizzle = descriptor.DestinationSelectX;
expected = DestinationSelect(4);
supported = swizzle == expected;
break;
case 2:
swizzle = descriptor.DestinationSelectXY;
expected = attributeSize == 1 ? DestinationSelect(4, 0) : DestinationSelect(4, 5);
supported = swizzle == expected;
break;
case 3:
swizzle = descriptor.DestinationSelectXYZ;
expected = attributeSize switch
{
1 => DestinationSelect(4, 0, 0),
2 => DestinationSelect(4, 5, 0),
_ => DestinationSelect(4, 5, 6),
};
supported = swizzle == expected;
break;
case 4:
swizzle = descriptor.DestinationSelectXYZW;
switch (attributeSize)
{
case 1:
expected = DestinationSelect(4, 0, 0, 1);
supported = swizzle == expected;
break;
case 2:
expected = DestinationSelect(4, 5, 0, 1);
supported = swizzle == expected;
break;
case 3:
expected = DestinationSelect(4, 5, 6, 1);
supported = swizzle == expected || swizzle == DestinationSelect(4, 5, 6, 0);
break;
default:
expected = DestinationSelect(4, 5, 6, 7);
supported = swizzle == expected || swizzle == DestinationSelect(4, 5, 6, 1) || swizzle == DestinationSelect(4, 5, 6, 0);
break;
}
break;
default:
throw SubmissionScheduler.Fatal($"A vertex attribute fetch uses an invalid component count: attribute={index} components={fetchedComponents}.");
}
if (!supported && Interlocked.Exchange(ref _vertexSwizzleLines, 1) == 0)
{
Console.Error.WriteLine(
$"[LOADER][INFO] vertex_input accepted an unsupported destination select attribute={index} size={attributeSize} " +
$"components={fetchedComponents} swizzle=0x{swizzle:X3} expected=0x{expected:X3}");
}
}
// One graphics pipeline for dynamic rendering: the attachment formats travel in the create info.
private Pipeline CreateRenderPipeline(GraphicsPipelineDescription description, PrimitiveTopology topology, PipelineLayout layout,
PolygonMode polygonMode = PolygonMode.Fill, bool precompile = false, bool optimize = false)
{
var parameters = description.StaticParameters;
var rendering = description.Rendering;
var vertexModule = new ShaderModule(description.VertexProgram.Module);
var pixelModule = description.PixelStage is null ? default : new ShaderModule(description.PixelProgram.Module);
if (vertexModule.Handle == 0 || (description.PixelStage is not null && pixelModule.Handle == 0))
{
throw SubmissionScheduler.Fatal($"A graphics pipeline stage has no module: vertex=0x{vertexModule.Handle:X} pixel=0x{pixelModule.Handle:X}.");
}
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
try
{
var shaderStages = stackalloc PipelineShaderStageCreateInfo[2];
var stageCount = 1u;
shaderStages[0] = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.VertexBit,
Module = vertexModule,
PName = entryPoint,
};
if (pixelModule.Handle != 0)
{
shaderStages[stageCount++] = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.FragmentBit,
Module = pixelModule,
PName = entryPoint,
};
}
var vertexBindings = new VertexInputBindingDescription[description.VertexInput.BindingCount];
var vertexAttributes = new VertexInputAttributeDescription[description.VertexInput.AttributeCount];
BuildVertexAttributes(description, vertexAttributes, vertexBindings);
var colorCount = (int)parameters.ColorCount;
var blends = new PipelineColorBlendAttachmentState[colorCount];
for (var index = 0; index < colorCount; index++)
{
var mask = parameters.GetColorMask(index);
if ((mask & ~0xFu) != 0)
{
throw SubmissionScheduler.Fatal($"A color write mask has unknown bits: attachment={index} mask=0x{mask:X}.");
}
var separateAlpha = parameters.GetSeparateAlphaBlend(index);
blends[index] = new PipelineColorBlendAttachmentState
{
ColorWriteMask = ToVkColorWriteMask(mask),
BlendEnable = parameters.GetBlendEnable(index) && !parameters.GetBlendBypass(index),
SrcColorBlendFactor = ToVkBlendFactor(parameters.GetColorSourceBlend(index)),
DstColorBlendFactor = ToVkBlendFactor(parameters.GetColorDestinationBlend(index)),
ColorBlendOp = ToVkBlendOp(parameters.GetColorBlendFunction(index)),
SrcAlphaBlendFactor = ToVkBlendFactor(separateAlpha ? parameters.GetAlphaSourceBlend(index) : parameters.GetColorSourceBlend(index)),
DstAlphaBlendFactor = ToVkBlendFactor(separateAlpha ? parameters.GetAlphaDestinationBlend(index) : parameters.GetColorDestinationBlend(index)),
AlphaBlendOp = ToVkBlendOp(separateAlpha ? parameters.GetAlphaBlendFunction(index) : parameters.GetColorBlendFunction(index)),
};
}
var colorFormats = new Format[colorCount];
Array.Copy(rendering.ColorFormats, colorFormats, colorCount);
var cullMode = CullModeFlags.None;
if (parameters.CullBack)
{
cullMode |= CullModeFlags.BackBit;
}
if (parameters.CullFront)
{
cullMode |= CullModeFlags.FrontBit;
}
fixed (VertexInputBindingDescription* bindingPointer = vertexBindings)
fixed (VertexInputAttributeDescription* attributePointer = vertexAttributes)
fixed (PipelineColorBlendAttachmentState* blendPointer = blends)
fixed (Format* colorFormatPointer = colorFormats)
{
var vertexInput = new PipelineVertexInputStateCreateInfo
{
SType = StructureType.PipelineVertexInputStateCreateInfo,
VertexBindingDescriptionCount = (uint)vertexBindings.Length,
PVertexBindingDescriptions = vertexBindings.Length == 0 ? null : bindingPointer,
VertexAttributeDescriptionCount = (uint)vertexAttributes.Length,
PVertexAttributeDescriptions = vertexAttributes.Length == 0 ? null : attributePointer,
};
var inputAssembly = new PipelineInputAssemblyStateCreateInfo
{
SType = StructureType.PipelineInputAssemblyStateCreateInfo,
Topology = topology,
PrimitiveRestartEnable = parameters.PrimitiveRestartEnable,
};
var depthClipControl = new PipelineViewportDepthClipControlCreateInfoEXT
{
SType = StructureType.PipelineViewportDepthClipControlCreateInfoExt,
NegativeOneToOne = parameters.NegativeOneToOne,
};
var viewportState = new PipelineViewportStateCreateInfo
{
SType = StructureType.PipelineViewportStateCreateInfo,
PNext = _supportsDepthClipControl ? &depthClipControl : null,
ViewportCount = 1,
ScissorCount = 1,
};
var depthClip = new PipelineRasterizationDepthClipStateCreateInfoEXT
{
SType = StructureType.PipelineRasterizationDepthClipStateCreateInfoExt,
DepthClipEnable = parameters.DepthClipEnable,
};
var rasterization = new PipelineRasterizationStateCreateInfo
{
SType = StructureType.PipelineRasterizationStateCreateInfo,
PNext = _supportsDepthClipEnable ? &depthClip : null,
PolygonMode = polygonMode,
CullMode = cullMode,
FrontFace = parameters.FrontFaceClockwise ? FrontFace.Clockwise : FrontFace.CounterClockwise,
LineWidth = 1,
};
var multisample = new PipelineMultisampleStateCreateInfo
{
SType = StructureType.PipelineMultisampleStateCreateInfo,
SampleShadingEnable = parameters.SampleShadingEnable,
RasterizationSamples = ImageDescription.VulkanSampleCount(parameters.Samples),
MinSampleShading = 1f,
};
var colorBlend = new PipelineColorBlendStateCreateInfo
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
LogicOp = LogicOp.Copy,
AttachmentCount = (uint)blends.Length,
PAttachments = blends.Length == 0 ? null : blendPointer,
};
var depthStencil = new PipelineDepthStencilStateCreateInfo
{
SType = StructureType.PipelineDepthStencilStateCreateInfo,
DepthBoundsTestEnable = _supportsDepthBounds && parameters.DepthBoundsTestEnable,
StencilTestEnable = parameters.StencilTestEnable,
Front = ToVkStencilOpState(parameters.StencilFront),
Back = ToVkStencilOpState(parameters.StencilBack),
MinDepthBounds = parameters.DepthMinBounds,
MaxDepthBounds = parameters.DepthMaxBounds,
};
var dynamicStates = stackalloc DynamicState[13];
dynamicStates[0] = DynamicState.Viewport;
dynamicStates[1] = DynamicState.Scissor;
dynamicStates[2] = DynamicState.LineWidth;
dynamicStates[3] = DynamicState.DepthTestEnableExt;
dynamicStates[4] = DynamicState.DepthWriteEnableExt;
dynamicStates[5] = DynamicState.DepthCompareOpExt;
dynamicStates[6] = DynamicState.DepthBiasEnableExt;
dynamicStates[7] = DynamicState.DepthBias;
dynamicStates[8] = DynamicState.StencilCompareMask;
dynamicStates[9] = DynamicState.StencilReference;
dynamicStates[10] = DynamicState.StencilWriteMask;
dynamicStates[11] = DynamicState.BlendConstants;
var dynamicStateCount = 12u;
// Last so a pipeline without color attachments can leave it out.
if (_colorWriteEnableApi is not null && colorCount != 0)
{
dynamicStates[dynamicStateCount++] = DynamicState.ColorWriteEnableExt;
}
var dynamicState = new PipelineDynamicStateCreateInfo
{
SType = StructureType.PipelineDynamicStateCreateInfo,
DynamicStateCount = dynamicStateCount,
PDynamicStates = dynamicStates,
};
var renderingInfo = new PipelineRenderingCreateInfo
{
SType = StructureType.PipelineRenderingCreateInfo,
ColorAttachmentCount = (uint)colorCount,
PColorAttachmentFormats = colorCount == 0 ? null : colorFormatPointer,
DepthAttachmentFormat = rendering.DepthFormat,
StencilAttachmentFormat = rendering.StencilFormat,
};
var pipelineInfo = new GraphicsPipelineCreateInfo
{
SType = StructureType.GraphicsPipelineCreateInfo,
PNext = &renderingInfo,
StageCount = stageCount,
PStages = shaderStages,
PVertexInputState = &vertexInput,
PInputAssemblyState = &inputAssembly,
PViewportState = &viewportState,
PRasterizationState = &rasterization,
PMultisampleState = &multisample,
PDepthStencilState = parameters.WithDepth ? &depthStencil : null,
PColorBlendState = &colorBlend,
PDynamicState = &dynamicState,
Layout = layout,
};
if (precompile || optimize)
{
if (precompile && HasStoredPipeline(&pipelineInfo))
{
return default;
}
Check(CreateCachedPipelineObject(true, &pipelineInfo, out var prepared, out _, fast: false),
$"vkCreateGraphicsPipelines(precompile vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16})");
return prepared;
}
var graphicsStart = Stopwatch.GetTimestamp();
Pipeline pipeline;
var unoptimized = false;
if (UsesPipelineStore || FastPipelineBuildEnabled)
{
Check(CreateCachedPipelineObject(true, &pipelineInfo, out pipeline, out unoptimized, FastPipelineBuildEnabled),
$"vkCreateGraphicsPipelines(rendering vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16})");
if (unoptimized)
{
QueuePipelineOptimization(pipeline, () => CreateRenderPipeline(description, topology, layout, polygonMode, optimize: true));
}
}
else
{
var cache = GetGuestPipelineCache(GraphicsCacheKey(description.VertexStage.Hash,
description.PixelStage?.Hash ?? 0, vertexModule.Handle, pixelModule.Handle));
Check(_vk.CreateGraphicsPipelines(_device, cache, 1, &pipelineInfo, null, out pipeline),
$"vkCreateGraphicsPipelines(rendering vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16})");
}
ReportPipelineCreation(
(long)Stopwatch.GetElapsedTime(graphicsStart).TotalMilliseconds,
"graphics",
$"vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16}",
string.Join(
'/',
Enumerable
.Range(0, (int)stageCount)
.Select(index => SpirvBytesOf(shaderStages[index].Module.Handle))),
unoptimized);
MarkPipelineCacheDirty();
Interlocked.Increment(ref _perfPipelineCreations);
SetDebugName(
ObjectType.Pipeline,
pipeline.Handle,
$"SharpEmu graphics vs=0x{description.VertexStage.Hash:X16} ps=0x{description.PixelStage?.Hash ?? 0:X16} colors={colorCount}");
return pipeline;
}
}
finally
{
SilkMarshal.Free((nint)entryPoint);
}
}
public PipelineHandle CreateComputePipeline(ComputePipelineDescription description)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.PipelineSetup);
var bindings = new List<DescriptorSetLayoutBinding>();
CollectLayoutBindings(bindings, description.Stage, ShaderStage.Compute);
var setLayout = CreateDescriptorSetLayout(bindings, out var usesPushDescriptors, out var demand);
var layout = CreatePipelineLayout(setLayout, ShaderStageFlags.ComputeBit);
var computeModule = new ShaderModule(description.Program.Module);
if (computeModule.Handle == 0)
{
throw SubmissionScheduler.Fatal($"The compute pipeline has no module: hash=0x{description.Stage.Hash:X16}.");
}
var requireSubgroup32 = RequiresComputeSubgroup32(description.Input);
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
Pipeline pipeline;
try
{
var requiredSubgroupSize = RequiredSubgroupSize();
var pipelineInfo = ComputeCreateInfo(computeModule, layout, entryPoint, requireSubgroup32 ? &requiredSubgroupSize : null);
var computeStart = Stopwatch.GetTimestamp();
var unoptimized = false;
if (UsesPipelineStore || FastPipelineBuildEnabled)
{
Check(CreateCachedPipelineObject(false, &pipelineInfo, out pipeline, out unoptimized, FastPipelineBuildEnabled),
$"vkCreateComputePipelines(rendering) hash=0x{description.Stage.Hash:X16}");
if (unoptimized)
{
QueuePipelineOptimization(pipeline, () => CompileStoredComputePipeline(computeModule, layout, requireSubgroup32));
}
}
else
{
var cache = GetGuestPipelineCache(ComputeCacheKey(description.Stage.Hash, computeModule.Handle));
Check(_vk.CreateComputePipelines(_device, cache, 1, &pipelineInfo, null, out pipeline), $"vkCreateComputePipelines(rendering) hash=0x{description.Stage.Hash:X16}");
}
ReportPipelineCreation(
(long)Stopwatch.GetElapsedTime(computeStart).TotalMilliseconds,
"compute",
$"cs=0x{description.Stage.Hash:X16}",
SpirvBytesOf(computeModule.Handle).ToString(),
unoptimized);
MarkPipelineCacheDirty();
Interlocked.Increment(ref _perfPipelineCreations);
SetDebugName(ObjectType.Pipeline, pipeline.Handle, $"SharpEmu compute cs=0x{description.Stage.Hash:X16}");
}
finally
{
SilkMarshal.Free((nint)entryPoint);
}
var entry = new RenderPipelineEntry
{
Pipeline = pipeline,
Layout = layout,
SetLayout = setLayout,
Demand = demand,
UsesPushDescriptors = usesPushDescriptors,
ProfileComputeHash = description.Stage.Hash,
};
return RegisterPipeline(entry);
}
// One compute pipeline whose vkCreateComputePipelines call runs on a worker
// thread. Everything the command stream owns (descriptor and pipeline layout,
// the module handle) is prepared before the task starts. The worker loads
// its optional driver-cache shard and creates the native pipeline.
private sealed class PendingComputePipeline
{
public required DescriptorSetLayout SetLayout;
public required PipelineLayout Layout;
public required DescriptorSetDemand Demand;
public required bool UsesPushDescriptors;
public required ulong Hash;
public required int SpirvBytes;
public Task<Pipeline> Compile = Task.FromResult(default(Pipeline));
public long StartTimestamp;
public long CompileMilliseconds;
}
private readonly Dictionary<ulong, PendingComputePipeline> _pendingComputePipelines = new();
private static readonly SemaphoreSlim _computeCompileSlots =
new(Math.Max(2, Environment.ProcessorCount / 2));
// A host shader compiler can spend tens of seconds on one large translated
// program. Running the compile on a worker thread keeps it out of the pipeline
// cache's lock, so other queues can still create their own pipelines meanwhile;
// SHARPEMU_ASYNC_COMPUTE_PIPELINES=0 compiles inline instead.
private static readonly bool _asyncComputePipelines =
!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_ASYNC_COMPUTE_PIPELINES"),
"0",
StringComparison.Ordinal);
public bool TryCreateComputePipeline(ComputePipelineDescription description, out PipelineHandle handle)
{
if (!_asyncComputePipelines)
{
handle = CreateComputePipeline(description);
return true;
}
handle = default;
var key = description.Program.Id;
if (_pendingComputePipelines.TryGetValue(key, out var pending))
{
if (!pending.Compile.IsCompleted)
{
return false;
}
_pendingComputePipelines.Remove(key);
var compiled = pending.Compile.GetAwaiter().GetResult();
ReportPipelineCreation(
Interlocked.Read(ref pending.CompileMilliseconds),
"compute-async",
$"cs=0x{pending.Hash:X16} waited_ms={(long)Stopwatch.GetElapsedTime(pending.StartTimestamp).TotalMilliseconds}",
pending.SpirvBytes.ToString());
MarkPipelineCacheDirty();
Interlocked.Increment(ref _perfPipelineCreations);
SetDebugName(ObjectType.Pipeline, compiled.Handle, $"SharpEmu compute cs=0x{pending.Hash:X16}");
handle = RegisterPipeline(new RenderPipelineEntry
{
Pipeline = compiled,
Layout = pending.Layout,
SetLayout = pending.SetLayout,
Demand = pending.Demand,
UsesPushDescriptors = pending.UsesPushDescriptors,
ProfileComputeHash = pending.Hash,
});
return true;
}
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.PipelineSetup);
var bindings = new List<DescriptorSetLayoutBinding>();
CollectLayoutBindings(bindings, description.Stage, ShaderStage.Compute);
var setLayout = CreateDescriptorSetLayout(bindings, out var usesPushDescriptors, out var demand);
var layout = CreatePipelineLayout(setLayout, ShaderStageFlags.ComputeBit);
var computeModule = new ShaderModule(description.Program.Module);
if (computeModule.Handle == 0)
{
throw SubmissionScheduler.Fatal($"The compute pipeline has no module: hash=0x{description.Stage.Hash:X16}.");
}
var requireSubgroup32 = RequiresComputeSubgroup32(description.Input);
var usesStore = UsesPipelineStore;
var device = _device;
var cacheSource = GetGuestPipelineCacheSource(ComputeCacheKey(description.Stage.Hash, computeModule.Handle));
var vk = _vk;
var started = new PendingComputePipeline
{
SetLayout = setLayout,
Layout = layout,
Demand = demand,
UsesPushDescriptors = usesPushDescriptors,
Hash = description.Stage.Hash,
SpirvBytes = SpirvBytesOf(computeModule.Handle),
StartTimestamp = Stopwatch.GetTimestamp(),
};
started.Compile = Task.Factory.StartNew(
() =>
{
// Each compile blocks its thread inside the Metal compiler service,
// so the number in flight is bounded instead of one thread per
// program the frame happens to touch.
_computeCompileSlots.Wait();
var compileStart = Stopwatch.GetTimestamp();
try
{
// Importing a MoltenVK cache compiles its MSL libraries.
// Keep that work inside the same bounded compiler slot.
if (usesStore || FastPipelineBuildEnabled)
{
return CompileStoredComputePipeline(computeModule, layout, requireSubgroup32, FastPipelineBuildEnabled);
}
var cache = ResolveGuestPipelineCache(cacheSource);
return CompileComputePipeline(vk, device, cache, computeModule, layout, requireSubgroup32);
}
finally
{
Interlocked.Exchange(
ref started.CompileMilliseconds,
(long)Stopwatch.GetElapsedTime(compileStart).TotalMilliseconds);
_computeCompileSlots.Release();
}
},
CancellationToken.None,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
_pendingComputePipelines.Add(key, started);
return false;
}
// vkCreateComputePipelines is the only call here; Vulkan synchronises host
// access to the pipeline cache internally, so several may run at once.
private static Pipeline CompileComputePipeline(
Vk vk,
Device device,
PipelineCache cache,
ShaderModule module,
PipelineLayout layout,
bool requireSubgroup32)
{
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
try
{
var requiredSubgroupSize = RequiredSubgroupSize();
var pipelineInfo = ComputeCreateInfo(module, layout, entryPoint, requireSubgroup32 ? &requiredSubgroupSize : null);
var result = vk.CreateComputePipelines(device, cache, 1, &pipelineInfo, null, out var pipeline);
if (result != Result.Success)
{
throw SubmissionScheduler.Fatal($"vkCreateComputePipelines(async) failed: {result}.");
}
return pipeline;
}
finally
{
SilkMarshal.Free((nint)entryPoint);
}
}
private Pipeline CompileStoredComputePipeline(ShaderModule module, PipelineLayout layout, bool requireSubgroup32, bool fast = false)
{
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
try
{
var requiredSubgroupSize = RequiredSubgroupSize();
var pipelineInfo = ComputeCreateInfo(module, layout, entryPoint, requireSubgroup32 ? &requiredSubgroupSize : null);
var result = CreateCachedPipelineObject(false, &pipelineInfo, out var pipeline, out var unoptimized, fast);
Check(result, "vkCreateComputePipelines(async)");
if (unoptimized)
{
QueuePipelineOptimization(pipeline, () => CompileStoredComputePipeline(module, layout, requireSubgroup32));
}
return pipeline;
}
finally
{
SilkMarshal.Free((nint)entryPoint);
}
}
private void DrainPendingComputePipelines()
{
foreach (var pending in _pendingComputePipelines.Values)
{
try
{
var pipeline = pending.Compile.GetAwaiter().GetResult();
if (pipeline.Handle != 0) _vk.DestroyPipeline(_device, pipeline, null);
}
catch (Exception exception)
{
Console.Error.WriteLine($"[LOADER][WARN] Pending compute pipeline failed during shutdown: {exception.Message}");
}
_vk.DestroyPipelineLayout(_device, pending.Layout, null);
_vk.DestroyDescriptorSetLayout(_device, pending.SetLayout, null);
}
_pendingComputePipelines.Clear();
}
private void DestroyRenderPipelines()
{
foreach (var entry in _pipelineEntries.Values)
{
foreach (var pipeline in new[] { entry.Pipeline, entry.StripVariant, entry.ListVariant, entry.RectangleVariant })
{
if (pipeline.Handle != 0)
{
_vk.DestroyPipeline(_device, pipeline, null);
}
}
_vk.DestroyPipelineLayout(_device, entry.Layout, null);
_vk.DestroyDescriptorSetLayout(_device, entry.SetLayout, null);
}
_pipelineEntries.Clear();
foreach (var module in _shaderModules.Values)
{
_vk.DestroyShaderModule(_device, module, null);
}
_shaderModules.Clear();
_shaderModuleCacheIdentities.Clear();
}
}
}