diff --git a/src/SharpEmu.Libs/VideoOut/PerfOverlay.cs b/src/SharpEmu.Libs/VideoOut/PerfOverlay.cs
index c2c1b422..4ffc3a85 100644
--- a/src/SharpEmu.Libs/VideoOut/PerfOverlay.cs
+++ b/src/SharpEmu.Libs/VideoOut/PerfOverlay.cs
@@ -30,6 +30,7 @@ public static class PerfOverlay
StringComparison.Ordinal);
private static long _lastPresentTimestamp;
+ private static long _lastSubmitTimestamp;
private static long _sessionStartTimestamp;
private static readonly double[] _frameMilliseconds = new double[FrameHistorySize];
private static int _frameHistoryIndex;
@@ -40,8 +41,11 @@ public static class PerfOverlay
// Refreshed once per second so per-frame fills never allocate.
private static long _statsWindowStart = Stopwatch.GetTimestamp();
+ // Headline FPS tracks guest VideoOut flips (RecordSubmit), not host
+ // swapchain presents. Metal's free-running present timer hits ~120 Hz on
+ // ProMotion even while the guest is stalled on GPU waits.
private static double _fps;
- private static double _submittedFps;
+ private static double _presentFps;
private static double _drawsPerSecond;
private static double _averageFrameMs;
private static double _allocatedMbPerSecond;
@@ -64,24 +68,29 @@ public static class PerfOverlay
public static void Toggle() => _enabled = !_enabled;
- /// Called by the presenter after each successful present.
+ /// Called by the presenter after each successful host present.
public static void RecordPresent()
{
- var now = Stopwatch.GetTimestamp();
- Interlocked.CompareExchange(ref _sessionStartTimestamp, now, 0);
- var last = _lastPresentTimestamp;
- _lastPresentTimestamp = now;
+ Interlocked.CompareExchange(ref _sessionStartTimestamp, Stopwatch.GetTimestamp(), 0);
Interlocked.Increment(ref _presentedInWindow);
- if (last != 0)
- {
- var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
- _frameMilliseconds[_frameHistoryIndex] = milliseconds;
- _frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
- }
+ _lastPresentTimestamp = Stopwatch.GetTimestamp();
}
/// Called on every guest flip submission.
- public static void RecordSubmit() => Interlocked.Increment(ref _submittedInWindow);
+ public static void RecordSubmit()
+ {
+ var now = Stopwatch.GetTimestamp();
+ Interlocked.CompareExchange(ref _sessionStartTimestamp, now, 0);
+ Interlocked.Increment(ref _submittedInWindow);
+ var last = Interlocked.Exchange(ref _lastSubmitTimestamp, now);
+ if (last != 0)
+ {
+ var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
+ var index = _frameHistoryIndex;
+ _frameMilliseconds[index] = milliseconds;
+ _frameHistoryIndex = (index + 1) % FrameHistorySize;
+ }
+ }
/// Called per translated draw/dispatch executed.
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
@@ -128,27 +137,47 @@ public static class PerfOverlay
{
var seconds = (double)elapsedTicks / Stopwatch.Frequency;
_statsWindowStart = now;
- _fps = Interlocked.Exchange(ref _presentedInWindow, 0) / seconds;
- _submittedFps = Interlocked.Exchange(ref _submittedInWindow, 0) / seconds;
+ _fps = Interlocked.Exchange(ref _submittedInWindow, 0) / seconds;
+ _presentFps = Interlocked.Exchange(ref _presentedInWindow, 0) / seconds;
_drawsPerSecond = Interlocked.Exchange(ref _drawsInWindow, 0) / seconds;
- double totalMs = 0;
- var samples = 0;
- foreach (var ms in _frameMilliseconds)
- {
- if (ms > 0)
- {
- totalMs += ms;
- samples++;
- }
- }
-
- _averageFrameMs = samples > 0 ? totalMs / samples : 0;
-
var allocated = GC.GetTotalAllocatedBytes(precise: false);
_allocatedMbPerSecond = (allocated - _lastAllocatedBytes) / seconds / (1024.0 * 1024.0);
_lastAllocatedBytes = allocated;
+ // Headline MS must track *current* guest cadence. Averaging the whole
+ // 128-slot history kept a single 8s boot gap on screen for minutes
+ // (FPS 0 + 8000 MS) even after the stall ended.
+ var lastSubmit = Interlocked.Read(ref _lastSubmitTimestamp);
+ string msLabel;
+ if (_fps > 0)
+ {
+ var lastIndex = (_frameHistoryIndex - 1 + FrameHistorySize) % FrameHistorySize;
+ var lastInterval = _frameMilliseconds[lastIndex];
+ _averageFrameMs = lastInterval > 0 ? lastInterval : 1000.0 / _fps;
+ msLabel = $"{_averageFrameMs:0.0} MS";
+ }
+ else if (lastSubmit != 0)
+ {
+ // No guest flips this window: show stall age, but label it so it
+ // is not read as "the game is rendering 20s frames" during boot
+ // asset load (ALLOC high, DRAWS 0 after splash).
+ _averageFrameMs = (now - lastSubmit) * 1000.0 / Stopwatch.Frequency;
+ msLabel = _allocatedMbPerSecond > 50.0
+ ? $"LOAD {_averageFrameMs / 1000.0:0.0}S"
+ : $"STALL {_averageFrameMs / 1000.0:0.0}S";
+ }
+ else if (_presentFps > 0)
+ {
+ _averageFrameMs = 1000.0 / _presentFps;
+ msLabel = $"{_averageFrameMs:0.0} MS";
+ }
+ else
+ {
+ _averageFrameMs = 0;
+ msLabel = "0.0 MS";
+ }
+
var gen0 = GC.CollectionCount(0);
var gen1 = GC.CollectionCount(1);
var gen2 = GC.CollectionCount(2);
@@ -174,7 +203,7 @@ public static class PerfOverlay
var elapsedHours = elapsedSeconds / 3600;
var elapsedMinutes = elapsedSeconds / 60 % 60;
var elapsedRemainingSeconds = elapsedSeconds % 60;
- _line1 = $"FPS {_fps:0.0} FLIP {_submittedFps:0.0} {_averageFrameMs:0.0} MS";
+ _line1 = $"FPS {_fps:0.0} PRES {_presentFps:0.0} {msLabel}";
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
var heapMb = GC.GetTotalMemory(false) / (1024 * 1024);