[GPU] Report LOD statistics measured on the GPU

This commit is contained in:
ParantezTech 2026-10-11 01:38:13 +03:00
parent 5d6b59eeb8
commit 32c9a37b0e
9 changed files with 206 additions and 1 deletions

View File

@ -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];

View File

@ -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<byte> destination);

View File

@ -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}."),
};

View File

@ -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

View File

@ -680,6 +680,7 @@ internal sealed class ShaderProgramCache
SupportsPerVertexPixelInputs = _host.PerVertexPixelInputsSupported,
PixelInputAddress = options.PixelInputAddress,
PixelInputCntl = interpolators,
MipStatisticsSubgroupReduction = _host.MipStatisticsSubgroupReduction,
};
}

View File

@ -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:

View File

@ -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;

View File

@ -182,6 +182,7 @@ internal static unsafe partial class VulkanVideoPresenter
}
_commandStream.NotifyCompletedGpuTick(_scheduler.Timeline.CompletedTick);
PublishLodStatistics();
ProcessDeferredTextureDestroys();
}

View File

@ -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<byte, uint>(
_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;
}
}
}