From c086e32f3d72cbb0d0f3a113a63ef1b658216630 Mon Sep 17 00:00:00 2001 From: Foued Attar Date: Mon, 3 Aug 2026 20:18:23 +0200 Subject: [PATCH] Fix GuestDataPool lease leak on non-GPU compute dispatch paths (#759) ObserveComputeDispatch only returned evaluation's pooled arrays when evaluationHandledByCpu was set. Dispatches rejected before submission (empty resource tables, oversized workgroup, compile failure) or a compute submit that got dropped instead of enqueued (workSequence == 0) never handed those buffers to a consumer that would return them, leaking one lease per occurrence and growing GuestDataPool.Shared without bound over a long session. Adds GuestDataPool.DiagnosticStats() (outstanding lease count, idle cached bytes) surfaced in the periodic [LOADER][PERF] line, to catch this class of regression going forward. Verified on Ghost of Yotei and Demon's Souls: pool_leases grew unbounded before the fix (525 in 5 min on Demon's Souls, 1114 in 180s on Yotei) and stays flat/bounded after (0-6 and 2-18 respectively), with no behavioral regression observed. --- src/SharpEmu.Libs/Agc/AgcExports.cs | 4 +++- src/SharpEmu.Libs/Gpu/GuestDataPool.cs | 12 ++++++++++++ src/SharpEmu.Libs/VideoOut/VideoOutExports.cs | 4 +++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index f535c649..1b58a13a 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -11307,7 +11307,9 @@ public static partial class AgcExports } } - if (evaluationHandledByCpu) + // Rejected/CPU-handled dispatches never hand evaluation's pooled buffers to a + // consumer that would return them; reclaim here to keep GuestDataPool.Shared bounded. + if (evaluationHandledByCpu || !gpuDispatch) { ReturnPooledEvaluationArrays(evaluation); } diff --git a/src/SharpEmu.Libs/Gpu/GuestDataPool.cs b/src/SharpEmu.Libs/Gpu/GuestDataPool.cs index a9d395dc..af0740f7 100644 --- a/src/SharpEmu.Libs/Gpu/GuestDataPool.cs +++ b/src/SharpEmu.Libs/Gpu/GuestDataPool.cs @@ -23,6 +23,10 @@ internal static class GuestDataPool public static void Trim() => ((BoundedByteArrayPool)Shared).Trim(); + /// Outstanding lease count and idle cached bytes, for leak diagnostics. + public static (int LeaseCount, ulong CachedBytes) DiagnosticStats() => + ((BoundedByteArrayPool)Shared).Stats(); + private sealed class BoundedByteArrayPool : ArrayPool { private readonly object _gate = new(); @@ -119,6 +123,14 @@ internal static class GuestDataPool } } + public (int LeaseCount, ulong CachedBytes) Stats() + { + lock (_gate) + { + return (_leases.Count, _cachedBytes); + } + } + private int GetAllocationLength(int minimumLength) { if (minimumLength <= 16) diff --git a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs index c5895ced..ca2689c9 100644 --- a/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs +++ b/src/SharpEmu.Libs/VideoOut/VideoOutExports.cs @@ -1300,10 +1300,12 @@ public static class VideoOutExports var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0); var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0); var (draws, drawMs, pipelines, spirvCompiles) = GuestGpu.Current.ReadAndResetPerfCounters(); + var (poolLeases, poolCachedBytes) = SharpEmu.Libs.Gpu.GuestDataPool.DiagnosticStats(); Console.Error.WriteLine( $"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " + $"presented_fps={presentedCount / elapsedSeconds:F1} " + - $"draws={draws} draw_ms={drawMs:F0} pipelines={pipelines} spirv={spirvCompiles}"); + $"draws={draws} draw_ms={drawMs:F0} pipelines={pipelines} spirv={spirvCompiles} " + + $"pool_leases={poolLeases} pool_cached_mb={poolCachedBytes / 1024.0 / 1024.0:F1}"); } private static readonly bool _flipPacingDisabled = string.Equals(