diff --git a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanAsyncReadback.cs b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanAsyncReadback.cs index 42d8420..d771e3f 100644 --- a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanAsyncReadback.cs +++ b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanAsyncReadback.cs @@ -172,10 +172,13 @@ internal sealed unsafe class VulkanAsyncReadback : IDisposable PSemaphores = &semaphore, PValues = &signalValue, }; + var waitStart = VideoOut.GpuWaitSiteProfile.Begin(); using (VideoOut.RenderPhaseProfile.MeasureDetail(VideoOut.RenderPhaseProfile.Phase.GpuCompletionWait)) { Require(vk.WaitSemaphores(_device.Device, &waitInfo, ulong.MaxValue), "vkWaitSemaphores(readback)"); } + + VideoOut.GpuWaitSiteProfile.End(waitStart); } finally { diff --git a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanTickDevice.cs b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanTickDevice.cs index db3e26f..cae352a 100644 --- a/src/SharpEmu.Libs/Gpu/Vulkan/VulkanTickDevice.cs +++ b/src/SharpEmu.Libs/Gpu/Vulkan/VulkanTickDevice.cs @@ -220,10 +220,13 @@ internal sealed unsafe class VulkanTickDevice : IGpuTickDevice PValues = &tick, }; Result result; + var waitStart = GpuWaitSiteProfile.Begin(); using (RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.GpuCompletionWait)) { result = _vk.WaitSemaphores(_device, &waitInfo, ulong.MaxValue); } + + GpuWaitSiteProfile.End(waitStart); failure = result.ToString(); return result == Result.Success && WaitRetired(tick); } diff --git a/src/SharpEmu.Libs/VideoOut/GpuWaitSiteProfile.cs b/src/SharpEmu.Libs/VideoOut/GpuWaitSiteProfile.cs new file mode 100644 index 0000000..d86cc75 --- /dev/null +++ b/src/SharpEmu.Libs/VideoOut/GpuWaitSiteProfile.cs @@ -0,0 +1,96 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Diagnostics; + +namespace SharpEmu.Libs.VideoOut; + +internal static class GpuWaitSiteProfile +{ + private const int SiteFrames = 5; + + private static readonly object _gate = new(); + private static readonly Dictionary _sites = new(StringComparer.Ordinal); + + private static readonly HashSet PlumbingTypes = new(StringComparer.Ordinal) + { + "GpuWaitSiteProfile", + "TickTimeline", + "SubmissionScheduler", + "VulkanTickDevice", + "VulkanAsyncReadback", + "GpuWorkerRelay", + "GpuWaitSpin", + }; + + public static long Begin() => RenderPhaseProfile.DetailMeasurementsEnabled ? Stopwatch.GetTimestamp() : 0; + + public static void End(long start) + { + if (start == 0) + { + return; + } + + var elapsed = Stopwatch.GetTimestamp() - start; + var site = Site(); + lock (_gate) + { + _sites.TryGetValue(site, out var entry); + _sites[site] = (entry.Count + 1, entry.Ticks + elapsed); + } + } + + private static string Site() + { + var names = new List(SiteFrames); + foreach (var frame in new StackTrace(2, false).GetFrames()) + { + if (frame.GetMethod() is not { } method) + { + continue; + } + + var type = method.DeclaringType; + while (type is { DeclaringType: not null } && type.Name.StartsWith('<')) + { + type = type.DeclaringType; + } + + var typeName = type?.Name ?? "?"; + if (PlumbingTypes.Contains(typeName) || method.Name.StartsWith('<')) + { + continue; + } + + names.Add($"{typeName}.{method.Name}"); + if (names.Count == SiteFrames) + { + break; + } + } + + return string.Join(" < ", names); + } + + public static void Report() + { + List> ranked; + lock (_gate) + { + if (_sites.Count == 0) + { + return; + } + + ranked = _sites.OrderByDescending(pair => pair.Value.Ticks).Take(12).ToList(); + _sites.Clear(); + } + + foreach (var (site, (count, ticks)) in ranked) + { + Console.Error.WriteLine( + $"[PERF][GPU_WAIT_SITE] ms={ticks * 1000.0 / Stopwatch.Frequency:F1} n={count} site={site}"); + } + } +} diff --git a/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs b/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs index 33b1c50..eb5e5d5 100644 --- a/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs +++ b/src/SharpEmu.Libs/VideoOut/RenderPhaseProfile.cs @@ -465,6 +465,7 @@ internal static class RenderPhaseProfile $"[PERF][RENDER_MS] window_s={seconds:F1} frames={frames} " + string.Join(" ", parts.Select(part => $"{part.Phase}={part.Milliseconds:F2}ms/n{part.Entries}"))); + GpuWaitSiteProfile.Report(); ReportImageUploads(); BufferUploadProfile.Report(); Console.Error.WriteLine(SharpEmu.ShaderCompiler.Resources.ResourceMaterializationCache.TakeReport());