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.
This commit is contained in:
Foued Attar
2026-08-03 20:18:23 +02:00
committed by GitHub
parent 9e10d7c44a
commit c086e32f3d
3 changed files with 18 additions and 2 deletions
+3 -1
View File
@@ -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); ReturnPooledEvaluationArrays(evaluation);
} }
+12
View File
@@ -23,6 +23,10 @@ internal static class GuestDataPool
public static void Trim() => ((BoundedByteArrayPool)Shared).Trim(); public static void Trim() => ((BoundedByteArrayPool)Shared).Trim();
/// <summary>Outstanding lease count and idle cached bytes, for leak diagnostics.</summary>
public static (int LeaseCount, ulong CachedBytes) DiagnosticStats() =>
((BoundedByteArrayPool)Shared).Stats();
private sealed class BoundedByteArrayPool : ArrayPool<byte> private sealed class BoundedByteArrayPool : ArrayPool<byte>
{ {
private readonly object _gate = new(); 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) private int GetAllocationLength(int minimumLength)
{ {
if (minimumLength <= 16) if (minimumLength <= 16)
@@ -1300,10 +1300,12 @@ public static class VideoOutExports
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0); var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0); var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
var (draws, drawMs, pipelines, spirvCompiles) = GuestGpu.Current.ReadAndResetPerfCounters(); var (draws, drawMs, pipelines, spirvCompiles) = GuestGpu.Current.ReadAndResetPerfCounters();
var (poolLeases, poolCachedBytes) = SharpEmu.Libs.Gpu.GuestDataPool.DiagnosticStats();
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " + $"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
$"presented_fps={presentedCount / 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( private static readonly bool _flipPacingDisabled = string.Equals(