From 32c9a37b0ee22374ef699dd348c19084d61d6af5 Mon Sep 17 00:00:00 2001 From: ParantezTech <12572227+par274@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:38:13 +0300 Subject: [PATCH] [GPU] Report LOD statistics measured on the GPU --- .../GpuCommandInterpreter.Memory.cs | 5 + .../Gpu/GpuCommands/ICommandStreamHost.cs | 2 + .../Gpu/Pipelines/DescriptorWriter.cs | 3 +- .../Gpu/Pipelines/IShaderPipelineHost.cs | 2 + .../Gpu/Pipelines/ShaderProgramCache.cs | 1 + .../VulkanVideoPresenter.Descriptors.cs | 9 + .../VulkanVideoPresenter.Device.Setup.cs | 6 + .../VulkanVideoPresenter.GuestSubmissions.cs | 1 + .../VulkanVideoPresenter.LodStatistics.cs | 178 ++++++++++++++++++ 9 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.LodStatistics.cs diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Memory.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Memory.cs index 3b98876..6388ed0 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Memory.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/GpuCommandInterpreter.Memory.cs @@ -367,6 +367,11 @@ public sealed partial class GpuCommandInterpreter var bufferSize = payload[0]; var destination = (payload[1] & 0xFFFF_FFC0u) | ((ulong)payload[2] << 32); + if (destination != 0 && bufferSize != 0 && _host.TryRecordLodStatistics(destination, bufferSize)) + { + return 4; + } + if (destination != 0 && bufferSize != 0) { var report = new byte[bufferSize]; diff --git a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs index 460ed63..f1e0b9d 100644 --- a/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs +++ b/src/SharpEmu.Libs/Gpu/GpuCommands/ICommandStreamHost.cs @@ -81,6 +81,8 @@ public interface ICommandStreamHost { ICpuMemory Memory { get; } + bool TryRecordLodStatistics(ulong destination, uint size) => false; + // Reads guest memory the GPU may have written; the host synchronizes GPU-owned pages first. bool TryReadGuest(ulong address, Span destination); diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/DescriptorWriter.cs b/src/SharpEmu.Libs/Gpu/Pipelines/DescriptorWriter.cs index 7a2d378..da19348 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/DescriptorWriter.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/DescriptorWriter.cs @@ -28,7 +28,8 @@ public static class DescriptorWriter { DescriptorBindingKind.Samplers => Silk.NET.Vulkan.DescriptorType.Sampler, DescriptorBindingKind.Buffers or DescriptorBindingKind.GlobalDataShare or DescriptorBindingKind.DeviceAddressPageTable or - DescriptorBindingKind.FaultBuffer or DescriptorBindingKind.FlattenedResourceTable or DescriptorBindingKind.ShaderData => + DescriptorBindingKind.FaultBuffer or DescriptorBindingKind.FlattenedResourceTable or DescriptorBindingKind.ShaderData or + DescriptorBindingKind.MipStatistics => Silk.NET.Vulkan.DescriptorType.StorageBuffer, _ => throw SubmissionScheduler.Fatal($"The descriptor binding kind is invalid: kind={kind}."), }; diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs index be02c23..2595149 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/IShaderPipelineHost.cs @@ -41,6 +41,8 @@ internal interface IShaderPipelineHost bool ComputeRunsOn32LaneSubgroups(ComputeInputInfo input) => false; + bool MipStatisticsSubgroupReduction => false; + bool GraphicsSubgroupOperationsEnabled { get; } // The device supports shaderSharedInt64Atomics, so LDS 64-bit atomics can be diff --git a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs index 93c037d..7607f3d 100644 --- a/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs +++ b/src/SharpEmu.Libs/Gpu/Pipelines/ShaderProgramCache.cs @@ -680,6 +680,7 @@ internal sealed class ShaderProgramCache SupportsPerVertexPixelInputs = _host.PerVertexPixelInputsSupported, PixelInputAddress = options.PixelInputAddress, PixelInputCntl = interpolators, + MipStatisticsSubgroupReduction = _host.MipStatisticsSubgroupReduction, }; } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs index b888a05..558f095 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Descriptors.cs @@ -663,6 +663,12 @@ internal static unsafe partial class VulkanVideoPresenter } prepared.Descriptors.Buffers = views; + if (layout.MipStatisticsCount != 0) + { + LodStatistics().EnsureInitialized(); + WriteMipStatisticsFields(shaderData, layout, info, snapshot.Images); + } + if (layout.Find(DescriptorBindingKind.FlattenedResourceTable) is not null) { prepared.Descriptors.FlattenedTable = UploadDwords(snapshot.FlattenedResourceTable, "flattened resource table", program); @@ -995,6 +1001,9 @@ internal static unsafe partial class VulkanVideoPresenter bufferInfos[bufferIndex++] = new DescriptorBufferInfo { Buffer = view.Buffer, Offset = view.Offset, Range = view.Range }; } + break; + case DescriptorBindingKind.MipStatistics: + bufferInfos[bufferIndex++] = MipStatisticsDescriptor(); break; case DescriptorBindingKind.DeviceAddressPageTable: case DescriptorBindingKind.FaultBuffer: diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs index 057605d..08099d2 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.Device.Setup.cs @@ -608,6 +608,7 @@ internal static unsafe partial class VulkanVideoPresenter _supportsShaderSignedZeroInfNanPreserveFloat32 = floatControls.ShaderSignedZeroInfNanPreserveFloat32; SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages); + SetMipStatisticsCapabilities(true, subgroup.SupportedOperations, subgroup.SupportedStages); _canRequireComputeSubgroup32 = subgroup.SubgroupSize != RdnaSubgroupSize && subgroupSizeControl.MinSubgroupSize <= RdnaSubgroupSize && @@ -754,6 +755,11 @@ internal static unsafe partial class VulkanVideoPresenter queueInfoCount = 2; } _vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures); + if (!supportedFeatures.FragmentStoresAndAtomics) + { + ShaderCompiler.Resources.ResourceMaterializer.RecordMipStatistics = false; + } + _supportsIndependentBlend = supportedFeatures.IndependentBlend; _supportsDepthBiasClamp = supportedFeatures.DepthBiasClamp; _supportsDepthBounds = supportedFeatures.DepthBounds; diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.GuestSubmissions.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.GuestSubmissions.cs index b838735..4f721ab 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.GuestSubmissions.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.GuestSubmissions.cs @@ -182,6 +182,7 @@ internal static unsafe partial class VulkanVideoPresenter } _commandStream.NotifyCompletedGpuTick(_scheduler.Timeline.CompletedTick); + PublishLodStatistics(); ProcessDeferredTextureDestroys(); } diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.LodStatistics.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.LodStatistics.cs new file mode 100644 index 0000000..7901d88 --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.LodStatistics.cs @@ -0,0 +1,178 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Collections.Concurrent; +using SharpEmu.Libs.Gpu.Buffers; +using SharpEmu.Libs.Gpu.Images; +using SharpEmu.Libs.Gpu.Scheduling; +using SharpEmu.ShaderCompiler.Resources; +using Silk.NET.Vulkan; + +namespace SharpEmu.Libs.VideoOut; + +internal static unsafe partial class VulkanVideoPresenter +{ + private static readonly bool GpuLodStatistics = + !string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOD_STATS"), "synthetic", StringComparison.Ordinal); + + private static int _fragmentSubgroupArithmetic; + + internal static bool FragmentSubgroupArithmetic => Volatile.Read(ref _fragmentSubgroupArithmetic) != 0; + + private static void SetMipStatisticsCapabilities(bool fragmentStoresAndAtomics, SubgroupFeatureFlags operations, ShaderStageFlags stages) + { + ResourceMaterializer.RecordMipStatistics = GpuLodStatistics && fragmentStoresAndAtomics; + var arithmetic = (operations & (SubgroupFeatureFlags.ArithmeticBit | SubgroupFeatureFlags.BallotBit | SubgroupFeatureFlags.BasicBit)) == + (SubgroupFeatureFlags.ArithmeticBit | SubgroupFeatureFlags.BallotBit | SubgroupFeatureFlags.BasicBit) && + (stages & ShaderStageFlags.FragmentBit) != 0; + Volatile.Write(ref _fragmentSubgroupArithmetic, arithmetic ? 1 : 0); + } + + private sealed partial class Presenter + { + private LodStatisticsRecorder? _lodStatistics; + + private LodStatisticsRecorder LodStatistics() => + _lodStatistics ??= new LodStatisticsRecorder(_deviceInfo, _scheduler); + + bool Gpu.Pipelines.IShaderPipelineHost.MipStatisticsSubgroupReduction => FragmentSubgroupArithmetic && GraphicsSubgroupOperationsEnabled; + + bool Gpu.GpuCommands.ICommandStreamHost.TryRecordLodStatistics(ulong destination, uint size) + { + if (!ResourceMaterializer.RecordMipStatistics || size < MipStatistics.ReportBytes) + { + return false; + } + + if (OnCommandThread) + { + _commandThread!.Post(() => RecordLodStatistics(destination, size)); + return true; + } + + RecordLodStatistics(destination, size); + return true; + } + + private void RecordLodStatistics(ulong destination, uint size) + { + DrainDraws(); + EndRendering(); + _ = BeginBatchedGuestCommands(); + LodStatistics().Record(destination, size); + } + + private void PublishLodStatistics() + { + if (_lodStatistics is not { } recorder) + { + return; + } + + while (recorder.TryTakeReport(out var destination, out var report)) + { + if (!((Gpu.GpuCommands.ICommandStreamHost)this).Memory.TryWrite(destination, report)) + { + throw SubmissionScheduler.Fatal($"The LOD statistics report could not be written: address=0x{destination:X16} size={report.Length}."); + } + } + } + + private void WriteMipStatisticsFields(uint[] shaderData, BindingLayout layout, ShaderResourceInfo info, uint[][] images) + { + for (var index = 0; index < layout.MipStatisticsCount; index++) + { + var image = index < info.Images.Count ? info.Images[index] : null; + var words = index < images.Length ? images[index] : null; + shaderData[layout.MipStatisticsDword + (uint)index] = image is { MipStatistics: true } && words is { Length: >= 8 } + ? MipStatistics.ImageField(words) + : MipStatistics.NoCounterField; + } + } + + private DescriptorBufferInfo MipStatisticsDescriptor() + { + var counters = LodStatistics().Counters; + return new DescriptorBufferInfo { Buffer = counters.Handle, Offset = 0, Range = counters.Size }; + } + } + + private sealed class LodStatisticsRecorder + { + private const int Slots = 32; + private const ulong SlotStride = 4096; + + private readonly SubmissionScheduler _scheduler; + private readonly GpuBuffer _publish; + private readonly ulong[] _slotTicks = new ulong[Slots]; + private readonly ConcurrentQueue<(ulong Destination, byte[] Report)> _ready = new(); + private int _nextSlot; + private bool _initialized; + + public LodStatisticsRecorder(GpuDeviceInfo device, SubmissionScheduler scheduler) + { + _scheduler = scheduler; + Counters = new GpuBuffer(device, scheduler, GpuBufferUsage.DeviceLocal, 0, GpuBuffer.AllFlags, MipStatistics.CounterBufferBytes); + _publish = new GpuBuffer(device, scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, Slots * SlotStride); + } + + public GpuBuffer Counters { get; } + + public void Reset() + { + Counters.Fill(0, MipStatistics.CounterWords * sizeof(uint), uint.MaxValue); + Counters.Fill(MipStatistics.CounterWords * sizeof(uint), MipStatistics.CounterWords * sizeof(uint), 0); + _initialized = true; + } + + public void EnsureInitialized() + { + if (!_initialized) + { + Reset(); + } + } + + public void Record(ulong destination, uint size) + { + EnsureInitialized(); + var slot = _nextSlot; + _nextSlot = (_nextSlot + 1) % Slots; + if (_slotTicks[slot] != 0 && !_scheduler.IsTickComplete(_slotTicks[slot])) + { + _scheduler.Wait(_slotTicks[slot]); + } + + var offset = (ulong)slot * SlotStride; + _publish.CopyFrom( + _scheduler.Current, Counters, 0, offset, MipStatistics.CounterBufferBytes, + AccessFlags.ShaderWriteBit | AccessFlags.MemoryWriteBit, AccessFlags.None, + AccessFlags.ShaderReadBit | AccessFlags.ShaderWriteBit | AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit); + Reset(); + _slotTicks[slot] = _scheduler.CurrentTick; + var reportSize = (int)Math.Min(size, (uint)MipStatistics.ReportBytes); + _scheduler.QueueCompletionAction(() => + { + _publish.Invalidate(offset, MipStatistics.CounterBufferBytes); + var words = System.Runtime.InteropServices.MemoryMarshal.Cast( + _publish.Mapped.Slice((int)offset, (int)MipStatistics.CounterBufferBytes)); + var report = new byte[reportSize]; + MipStatistics.PackReport(words, report); + _ready.Enqueue((destination, report)); + }); + } + + public bool TryTakeReport(out ulong destination, out byte[] report) + { + if (_ready.TryDequeue(out var entry)) + { + (destination, report) = entry; + return true; + } + + destination = 0; + report = []; + return false; + } + } +}