diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.BindingShadow.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.BindingShadow.cs new file mode 100644 index 0000000..022640c --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.BindingShadow.cs @@ -0,0 +1,249 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.Gpu.Rendering; +using SharpEmu.Libs.Gpu.Vulkan; +using SharpEmu.ShaderCompiler.Resources; +using Silk.NET.Vulkan; +using VkBuffer = Silk.NET.Vulkan.Buffer; + +namespace SharpEmu.Libs.VideoOut; + +internal static unsafe partial class VulkanVideoPresenter +{ + private static readonly bool NativeStateReuse = + Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_STATE_REUSE") != "0"; + + private sealed partial class Presenter + { + private const int ShadowVertexBufferCapacity = 32; + + private nint _shadowCommand; + private ulong _shadowTick; + private long _shadowEpoch = -1; + private Pipeline _shadowGraphicsPipeline; + private Pipeline _shadowComputePipeline; + private PipelineLayout _shadowPushLayout; + private ShaderStageFlags _shadowPushStages; + private uint[]? _shadowPushData; + private DescriptorShadow? _graphicsDescriptorShadow; + private DescriptorShadow? _computeDescriptorShadow; + private ulong[]? _shadowVertexBindings; + private int _shadowVertexCount = -1; + private VkBuffer _shadowIndexBuffer; + private ulong _shadowIndexOffset; + private IndexType _shadowIndexType; + private bool _shadowIndexValid; + private ulong[]? _descriptorKeyScratch; + + private sealed class DescriptorShadow + { + public bool Valid; + public PipelineLayout Layout; + public ulong[] Key = new ulong[256]; + public int Length; + } + + private bool SyncNativeShadow(CommandBuffer command) + { + if (!NativeStateReuse) + { + return false; + } + + var tick = _scheduler.CurrentTick; + var epoch = NativeBindingEpoch.Value; + if (command.Handle == _shadowCommand && tick == _shadowTick && epoch == _shadowEpoch) + { + return true; + } + + _shadowCommand = command.Handle; + _shadowTick = tick; + _shadowEpoch = epoch; + _shadowGraphicsPipeline = default; + _shadowComputePipeline = default; + _shadowPushLayout = default; + if (_graphicsDescriptorShadow is not null) + { + _graphicsDescriptorShadow.Valid = false; + } + + if (_computeDescriptorShadow is not null) + { + _computeDescriptorShadow.Valid = false; + } + + _shadowVertexCount = -1; + _shadowIndexValid = false; + return true; + } + + private void BindPipelineShadowed(CommandBuffer command, PipelineBindPoint bindPoint, Pipeline pipeline) + { + if (SyncNativeShadow(command)) + { + ref var bound = ref bindPoint == PipelineBindPoint.Compute ? ref _shadowComputePipeline : ref _shadowGraphicsPipeline; + var reused = bound.Handle == pipeline.Handle; + CountNativeState(NativeStateKind.Pipeline, reused); + if (reused) + { + return; + } + + bound = pipeline; + } + + _vk.CmdBindPipeline(command, bindPoint, pipeline); + } + + private void PushConstantsShadowed(CommandBuffer command, PipelineLayout layout, ShaderStageFlags stages, uint* data) + { + if (SyncNativeShadow(command)) + { + var values = new ReadOnlySpan(data, (int)PushData.DwordCount); + var shadowData = _shadowPushData ??= new uint[PushData.DwordCount]; + var reused = _shadowPushLayout.Handle == layout.Handle && _shadowPushStages == stages && values.SequenceEqual(shadowData); + CountNativeState(NativeStateKind.PushConstants, reused); + if (reused) + { + return; + } + + _shadowPushLayout = layout; + _shadowPushStages = stages; + values.CopyTo(shadowData); + } + + _vk.CmdPushConstants(command, layout, stages, 0, PushData.ByteSize, data); + } + + private bool DescriptorsAlreadyBound(CommandBuffer command, PipelineBindPoint bindPoint, PipelineLayout layout, ReadOnlySpan writes) + { + if (!SyncNativeShadow(command)) + { + return false; + } + + var length = DescriptorKey(writes); + var shadow = bindPoint == PipelineBindPoint.Compute + ? _computeDescriptorShadow ??= new DescriptorShadow() + : _graphicsDescriptorShadow ??= new DescriptorShadow(); + var key = _descriptorKeyScratch.AsSpan(0, length); + var reused = shadow.Valid && shadow.Layout.Handle == layout.Handle && key.SequenceEqual(shadow.Key.AsSpan(0, shadow.Length)); + CountNativeState(NativeStateKind.Descriptors, reused); + if (reused) + { + return true; + } + + if (shadow.Key.Length < length) + { + shadow.Key = new ulong[Math.Max(length, shadow.Key.Length * 2)]; + } + + key.CopyTo(shadow.Key); + shadow.Length = length; + shadow.Layout = layout; + shadow.Valid = true; + return false; + } + + private int DescriptorKey(ReadOnlySpan writes) + { + var length = 0; + _descriptorKeyScratch ??= new ulong[256]; + foreach (ref readonly var write in writes) + { + var needed = length + 2 + (int)write.DescriptorCount * 3; + if (_descriptorKeyScratch.Length < needed) + { + Array.Resize(ref _descriptorKeyScratch, Math.Max(needed, _descriptorKeyScratch.Length * 2)); + } + + var key = _descriptorKeyScratch; + key[length++] = write.DstBinding | ((ulong)write.DescriptorType << 32); + key[length++] = write.DescriptorCount | (write.PBufferInfo != null ? 1UL << 32 : 0) | (write.PImageInfo != null ? 1UL << 33 : 0); + for (var index = 0; index < write.DescriptorCount; index++) + { + if (write.PBufferInfo != null) + { + var info = write.PBufferInfo[index]; + key[length++] = info.Buffer.Handle; + key[length++] = info.Offset; + key[length++] = info.Range; + } + else if (write.PImageInfo != null) + { + var info = write.PImageInfo[index]; + key[length++] = info.Sampler.Handle; + key[length++] = info.ImageView.Handle; + key[length++] = (ulong)info.ImageLayout; + } + } + } + + return length; + } + + private bool VertexBuffersAlreadyBound(CommandBuffer command, ReadOnlySpan bindings) + { + if (!SyncNativeShadow(command) || bindings.Length > ShadowVertexBufferCapacity) + { + _shadowVertexCount = -1; + return false; + } + + var shadow = _shadowVertexBindings ??= new ulong[ShadowVertexBufferCapacity * 2]; + var reused = _shadowVertexCount == bindings.Length; + for (var index = 0; reused && index < bindings.Length; index++) + { + reused = shadow[index * 2] == bindings[index].Handle && shadow[index * 2 + 1] == bindings[index].Offset; + } + + CountNativeState(NativeStateKind.VertexBuffers, reused); + if (reused) + { + return true; + } + + for (var index = 0; index < bindings.Length; index++) + { + shadow[index * 2] = bindings[index].Handle; + shadow[index * 2 + 1] = bindings[index].Offset; + } + + _shadowVertexCount = bindings.Length; + return false; + } + + private bool IndexBufferAlreadyBound(CommandBuffer command, VkBuffer buffer, ulong offset, IndexType type) + { + if (!SyncNativeShadow(command)) + { + return false; + } + + var reused = _shadowIndexValid && _shadowIndexBuffer.Handle == buffer.Handle && _shadowIndexOffset == offset && _shadowIndexType == type; + CountNativeState(NativeStateKind.IndexBuffer, reused); + if (reused) + { + return true; + } + + _shadowIndexBuffer = buffer; + _shadowIndexOffset = offset; + _shadowIndexType = type; + _shadowIndexValid = true; + return false; + } + + private static void CountNativeState(NativeStateKind kind, bool reused) + { + if (GpuSyncProfile.Enabled) + { + GpuSyncProfile.CountNativeState(kind, reused); + } + } + } +} diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs index 558f095..5fc1f57 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs @@ -1092,10 +1092,10 @@ internal static unsafe partial class VulkanVideoPresenter if (hasPushData) { var pushStages = bindPoint == PipelineBindPoint.Graphics ? ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit : ShaderStageFlags.ComputeBit; - _vk.CmdPushConstants(command, entry.Layout, pushStages, 0, PushData.ByteSize, pushData); + PushConstantsShadowed(command, entry.Layout, pushStages, pushData); } - if (writeIndex != 0) + if (writeIndex != 0 && !DescriptorsAlreadyBound(command, bindPoint, entry.Layout, writes[..writeIndex])) { fixed (WriteDescriptorSet* writePointer = writes) { diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs index 3f9fb8e..61d3317 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Rendering.cs @@ -381,6 +381,11 @@ internal static unsafe partial class VulkanVideoPresenter { if (bindings.IsEmpty) return; var command = BeginBatchedGuestCommands(); + if (VertexBuffersAlreadyBound(command, bindings)) + { + return; + } + var buffers = stackalloc VkBuffer[bindings.Length]; var offsets = stackalloc ulong[bindings.Length]; for (var index = 0; index < bindings.Length; index++) @@ -392,8 +397,14 @@ internal static unsafe partial class VulkanVideoPresenter _vk.CmdBindVertexBuffers(command, 0, (uint)bindings.Length, buffers, offsets); } - public void BindIndexBuffer(BufferBinding binding, IndexType type) => - _vk.CmdBindIndexBuffer(BeginBatchedGuestCommands(), new VkBuffer(binding.Handle), binding.Offset, type); + public void BindIndexBuffer(BufferBinding binding, IndexType type) + { + var command = BeginBatchedGuestCommands(); + if (!IndexBufferAlreadyBound(command, new VkBuffer(binding.Handle), binding.Offset, type)) + { + _vk.CmdBindIndexBuffer(command, new VkBuffer(binding.Handle), binding.Offset, type); + } + } public IResourcePreparation BeginPreparation() { @@ -997,13 +1008,13 @@ internal static unsafe partial class VulkanVideoPresenter _boundGraphicsPipeline = entry; if (!entry.RectangleList) { - _vk.CmdBindPipeline(command, bindPoint, entry.Pipeline); + BindPipelineShadowed(command, bindPoint, entry.Pipeline); } return; } - _vk.CmdBindPipeline(command, bindPoint, entry.Pipeline); + BindPipelineShadowed(command, bindPoint, entry.Pipeline); _profileComputePipeline = entry.Id; } @@ -1027,7 +1038,7 @@ internal static unsafe partial class VulkanVideoPresenter variant = CreateRenderPipeline(entry.Description!, strip ? PrimitiveTopology.TriangleStrip : PrimitiveTopology.TriangleList, entry.Layout); } - _vk.CmdBindPipeline(command, PipelineBindPoint.Graphics, variant); + BindPipelineShadowed(command, PipelineBindPoint.Graphics, variant); } private void BindNativeRectangleList(RenderPipelineEntry entry, CommandBuffer command) @@ -1038,7 +1049,7 @@ internal static unsafe partial class VulkanVideoPresenter entry.Layout, PolygonMode.FillRectangleNV); } - _vk.CmdBindPipeline(command, PipelineBindPoint.Graphics, entry.RectangleVariant); + BindPipelineShadowed(command, PipelineBindPoint.Graphics, entry.RectangleVariant); } // Rectangle2D consumes three vertices and fills their projected bounding box.