[GPU] Report LOD statistics measured on the GPU
This commit is contained in:
parent
5d6b59eeb8
commit
32c9a37b0e
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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}."),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -680,6 +680,7 @@ internal sealed class ShaderProgramCache
|
|||
SupportsPerVertexPixelInputs = _host.PerVertexPixelInputsSupported,
|
||||
PixelInputAddress = options.PixelInputAddress,
|
||||
PixelInputCntl = interpolators,
|
||||
MipStatisticsSubgroupReduction = _host.MipStatisticsSubgroupReduction,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ internal static unsafe partial class VulkanVideoPresenter
|
|||
}
|
||||
|
||||
_commandStream.NotifyCompletedGpuTick(_scheduler.Timeline.CompletedTick);
|
||||
PublishLodStatistics();
|
||||
|
||||
ProcessDeferredTextureDestroys();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue