mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 12:48:39 +08:00
380 lines
14 KiB
C#
380 lines
14 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Diagnostics;
|
|
|
|
namespace SharpEmu.Libs.VideoOut;
|
|
|
|
/// <summary>
|
|
/// In-window performance HUD. The panel is rasterized on the CPU into a
|
|
/// small BGRA buffer each frame (embedded 5x7 font, no assets) and the
|
|
/// presenter blits it onto the swapchain image, so it needs no pipelines,
|
|
/// descriptors or blending state. Toggled with F1; SHARPEMU_OVERLAY=0
|
|
/// starts it hidden.
|
|
/// </summary>
|
|
public static class PerfOverlay
|
|
{
|
|
public const int PanelWidth = 376;
|
|
public const int PanelHeight = 176;
|
|
|
|
private const int GlyphColumns = 5;
|
|
private const int GlyphRows = 7;
|
|
private const int Scale = 2;
|
|
private const int CellWidth = (GlyphColumns + 1) * Scale;
|
|
private const int LineHeight = (GlyphRows + 2) * Scale;
|
|
private const int FrameHistorySize = 128;
|
|
|
|
private static volatile bool _enabled = !string.Equals(
|
|
Environment.GetEnvironmentVariable("SHARPEMU_OVERLAY"),
|
|
"0",
|
|
StringComparison.Ordinal);
|
|
|
|
private static long _lastPresentTimestamp;
|
|
private static long _sessionStartTimestamp;
|
|
private static readonly double[] _frameMilliseconds = new double[FrameHistorySize];
|
|
private static int _frameHistoryIndex;
|
|
private static long _presentedInWindow;
|
|
private static long _submittedInWindow;
|
|
private static long _drawsInWindow;
|
|
|
|
// Refreshed once per second so per-frame fills never allocate.
|
|
private static long _statsWindowStart = Stopwatch.GetTimestamp();
|
|
private static double _fps;
|
|
private static double _submittedFps;
|
|
private static double _drawsPerSecond;
|
|
private static double _averageFrameMs;
|
|
private static double _allocatedMbPerSecond;
|
|
private static long _lastAllocatedBytes = GC.GetTotalAllocatedBytes(precise: false);
|
|
private static int _lastGen0 = GC.CollectionCount(0);
|
|
private static int _lastGen1 = GC.CollectionCount(1);
|
|
private static int _lastGen2 = GC.CollectionCount(2);
|
|
private static int _gen0PerWindow;
|
|
private static int _gen1PerWindow;
|
|
private static int _gen2PerWindow;
|
|
private static double _cpuPercent;
|
|
private static TimeSpan _lastCpuTime = GetProcessCpuTime();
|
|
private static string _line1 = string.Empty;
|
|
private static string _line2 = string.Empty;
|
|
private static string _line3 = string.Empty;
|
|
private static string _line4 = string.Empty;
|
|
private static string _line5 = string.Empty;
|
|
|
|
public static bool Enabled => _enabled;
|
|
|
|
public static void Toggle() => _enabled = !_enabled;
|
|
|
|
/// <summary>Called by the presenter after each successful present.</summary>
|
|
public static void RecordPresent()
|
|
{
|
|
var now = Stopwatch.GetTimestamp();
|
|
Interlocked.CompareExchange(ref _sessionStartTimestamp, now, 0);
|
|
var last = _lastPresentTimestamp;
|
|
_lastPresentTimestamp = now;
|
|
Interlocked.Increment(ref _presentedInWindow);
|
|
if (last != 0)
|
|
{
|
|
var milliseconds = (now - last) * 1000.0 / Stopwatch.Frequency;
|
|
if (milliseconds < 1000.0)
|
|
{
|
|
_frameMilliseconds[_frameHistoryIndex] = milliseconds;
|
|
_frameHistoryIndex = (_frameHistoryIndex + 1) % FrameHistorySize;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>Called on every guest flip submission.</summary>
|
|
public static void RecordSubmit() => Interlocked.Increment(ref _submittedInWindow);
|
|
|
|
/// <summary>Called per translated draw/dispatch executed.</summary>
|
|
public static void RecordDraw() => Interlocked.Increment(ref _drawsInWindow);
|
|
|
|
/// <summary>
|
|
/// Rasterizes the panel into a BGRA byte span of PanelWidth x PanelHeight.
|
|
/// Runs on the render thread.
|
|
/// </summary>
|
|
public static void Fill(Span<byte> bgra, int pendingWork, int inFlightSubmissions)
|
|
{
|
|
RefreshStatsIfDue(pendingWork, inFlightSubmissions);
|
|
|
|
// Background: opaque dark slate.
|
|
for (var i = 0; i < bgra.Length; i += 4)
|
|
{
|
|
bgra[i] = 0x20;
|
|
bgra[i + 1] = 0x18;
|
|
bgra[i + 2] = 0x14;
|
|
bgra[i + 3] = 0xFF;
|
|
}
|
|
|
|
var y = 6;
|
|
DrawString(bgra, 8, y, _line1, 0x60, 0xFF, 0x60);
|
|
y += LineHeight;
|
|
DrawString(bgra, 8, y, _line2, 0xFF, 0xFF, 0xFF);
|
|
y += LineHeight;
|
|
DrawString(bgra, 8, y, _line3, 0xB0, 0xD0, 0xFF);
|
|
y += LineHeight;
|
|
DrawString(bgra, 8, y, _line4, 0xB0, 0xB0, 0xB0);
|
|
y += LineHeight;
|
|
DrawString(bgra, 8, y, _line5, 0xFF, 0xD0, 0x80);
|
|
y += LineHeight + 4;
|
|
DrawFrameGraph(bgra, 8, y, PanelWidth - 16, PanelHeight - y - 6);
|
|
}
|
|
|
|
private static void RefreshStatsIfDue(int pendingWork, int inFlightSubmissions)
|
|
{
|
|
var now = Stopwatch.GetTimestamp();
|
|
var elapsedTicks = now - _statsWindowStart;
|
|
if (elapsedTicks >= Stopwatch.Frequency)
|
|
{
|
|
var seconds = (double)elapsedTicks / Stopwatch.Frequency;
|
|
_statsWindowStart = now;
|
|
_fps = Interlocked.Exchange(ref _presentedInWindow, 0) / seconds;
|
|
_submittedFps = Interlocked.Exchange(ref _submittedInWindow, 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;
|
|
|
|
var gen0 = GC.CollectionCount(0);
|
|
var gen1 = GC.CollectionCount(1);
|
|
var gen2 = GC.CollectionCount(2);
|
|
_gen0PerWindow = gen0 - _lastGen0;
|
|
_gen1PerWindow = gen1 - _lastGen1;
|
|
_gen2PerWindow = gen2 - _lastGen2;
|
|
_lastGen0 = gen0;
|
|
_lastGen1 = gen1;
|
|
_lastGen2 = gen2;
|
|
|
|
var cpuTime = GetProcessCpuTime();
|
|
// Normalize across logical processors (Task Manager convention):
|
|
// raw process time / wall time reads 100% per fully busy core.
|
|
_cpuPercent = (cpuTime - _lastCpuTime).TotalSeconds / seconds * 100.0 /
|
|
Environment.ProcessorCount;
|
|
_lastCpuTime = cpuTime;
|
|
|
|
var drawsPerFrame = _fps > 0.5 ? _drawsPerSecond / _fps : 0;
|
|
var sessionStart = Interlocked.Read(ref _sessionStartTimestamp);
|
|
var elapsedSeconds = sessionStart == 0
|
|
? 0L
|
|
: (long)((now - sessionStart) / (double)Stopwatch.Frequency);
|
|
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";
|
|
_line2 = $"DRAWS {_drawsPerSecond:0}/S {drawsPerFrame:0}/F Q {pendingWork}+{inFlightSubmissions}";
|
|
_line3 = $"ALLOC {_allocatedMbPerSecond:0.0} MB/S GC {_gen0PerWindow}/{_gen1PerWindow}/{_gen2PerWindow}";
|
|
_line4 = $"CPU {_cpuPercent:0}% HEAP {GC.GetTotalMemory(false) / (1024 * 1024)} MB F1 HIDE";
|
|
_line5 = $"TIME {elapsedHours:00}:{elapsedMinutes:00}:{elapsedRemainingSeconds:00}";
|
|
}
|
|
}
|
|
|
|
private static TimeSpan GetProcessCpuTime()
|
|
{
|
|
var usage = Environment.CpuUsage;
|
|
return usage.UserTime + usage.PrivilegedTime;
|
|
}
|
|
|
|
private static void DrawFrameGraph(Span<byte> bgra, int x, int y, int width, int height)
|
|
{
|
|
if (height < 8)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Reference lines at 16.7ms and 33.3ms of a 40ms-tall scale.
|
|
const double maxMs = 40.0;
|
|
DrawHorizontalLine(bgra, x, y + height - (int)(16.7 / maxMs * height), width, 0x30, 0x60, 0x30);
|
|
DrawHorizontalLine(bgra, x, y + height - (int)(33.3 / maxMs * height), width, 0x30, 0x30, 0x60);
|
|
|
|
var barWidth = Math.Max(1, width / FrameHistorySize);
|
|
for (var i = 0; i < FrameHistorySize; i++)
|
|
{
|
|
var ms = _frameMilliseconds[(_frameHistoryIndex + i) % FrameHistorySize];
|
|
if (ms <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var barHeight = (int)Math.Min(height, ms / maxMs * height);
|
|
var barX = x + i * barWidth;
|
|
if (barX + barWidth > x + width)
|
|
{
|
|
break;
|
|
}
|
|
|
|
byte r, g, b;
|
|
if (ms <= 17.5)
|
|
{
|
|
(r, g, b) = ((byte)0x40, (byte)0xE0, (byte)0x40);
|
|
}
|
|
else if (ms <= 34.0)
|
|
{
|
|
(r, g, b) = ((byte)0xE0, (byte)0xC0, (byte)0x30);
|
|
}
|
|
else
|
|
{
|
|
(r, g, b) = ((byte)0xE8, (byte)0x40, (byte)0x40);
|
|
}
|
|
|
|
for (var py = 0; py < barHeight; py++)
|
|
{
|
|
var rowY = y + height - 1 - py;
|
|
for (var px = 0; px < barWidth; px++)
|
|
{
|
|
SetPixel(bgra, barX + px, rowY, r, g, b);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void DrawHorizontalLine(Span<byte> bgra, int x, int y, int width, byte r, byte g, byte b)
|
|
{
|
|
if (y < 0 || y >= PanelHeight)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var px = 0; px < width; px++)
|
|
{
|
|
SetPixel(bgra, x + px, y, r, g, b);
|
|
}
|
|
}
|
|
|
|
private static void DrawString(Span<byte> bgra, int x, int y, string text, byte r, byte g, byte b)
|
|
{
|
|
var penX = x;
|
|
foreach (var rawChar in text)
|
|
{
|
|
var c = char.ToUpperInvariant(rawChar);
|
|
if (c < ' ' || c > 'Z')
|
|
{
|
|
c = '?';
|
|
}
|
|
|
|
var glyph = Font.Slice((c - ' ') * GlyphColumns, GlyphColumns);
|
|
for (var column = 0; column < GlyphColumns; column++)
|
|
{
|
|
var bits = glyph[column];
|
|
for (var row = 0; row < GlyphRows; row++)
|
|
{
|
|
if ((bits & (1 << row)) == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (var sy = 0; sy < Scale; sy++)
|
|
{
|
|
for (var sx = 0; sx < Scale; sx++)
|
|
{
|
|
SetPixel(
|
|
bgra,
|
|
penX + column * Scale + sx,
|
|
y + row * Scale + sy,
|
|
r,
|
|
g,
|
|
b);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
penX += CellWidth;
|
|
if (penX + CellWidth > PanelWidth)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void SetPixel(Span<byte> bgra, int x, int y, byte r, byte g, byte b)
|
|
{
|
|
if ((uint)x >= PanelWidth || (uint)y >= PanelHeight)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var offset = (y * PanelWidth + x) * 4;
|
|
bgra[offset] = b;
|
|
bgra[offset + 1] = g;
|
|
bgra[offset + 2] = r;
|
|
bgra[offset + 3] = 0xFF;
|
|
}
|
|
|
|
// Classic 5x7 column-encoded font (bit 0 = top row), ASCII 32..90.
|
|
private static ReadOnlySpan<byte> Font =>
|
|
[
|
|
0x00, 0x00, 0x00, 0x00, 0x00, // ' '
|
|
0x00, 0x00, 0x5F, 0x00, 0x00, // '!'
|
|
0x00, 0x07, 0x00, 0x07, 0x00, // '"'
|
|
0x14, 0x7F, 0x14, 0x7F, 0x14, // '#'
|
|
0x24, 0x2A, 0x7F, 0x2A, 0x12, // '$'
|
|
0x23, 0x13, 0x08, 0x64, 0x62, // '%'
|
|
0x36, 0x49, 0x55, 0x22, 0x50, // '&'
|
|
0x00, 0x05, 0x03, 0x00, 0x00, // '''
|
|
0x00, 0x1C, 0x22, 0x41, 0x00, // '('
|
|
0x00, 0x41, 0x22, 0x1C, 0x00, // ')'
|
|
0x08, 0x2A, 0x1C, 0x2A, 0x08, // '*'
|
|
0x08, 0x08, 0x3E, 0x08, 0x08, // '+'
|
|
0x00, 0x50, 0x30, 0x00, 0x00, // ','
|
|
0x08, 0x08, 0x08, 0x08, 0x08, // '-'
|
|
0x00, 0x60, 0x60, 0x00, 0x00, // '.'
|
|
0x20, 0x10, 0x08, 0x04, 0x02, // '/'
|
|
0x3E, 0x51, 0x49, 0x45, 0x3E, // '0'
|
|
0x00, 0x42, 0x7F, 0x40, 0x00, // '1'
|
|
0x42, 0x61, 0x51, 0x49, 0x46, // '2'
|
|
0x21, 0x41, 0x45, 0x4B, 0x31, // '3'
|
|
0x18, 0x14, 0x12, 0x7F, 0x10, // '4'
|
|
0x27, 0x45, 0x45, 0x45, 0x39, // '5'
|
|
0x3C, 0x4A, 0x49, 0x49, 0x30, // '6'
|
|
0x01, 0x71, 0x09, 0x05, 0x03, // '7'
|
|
0x36, 0x49, 0x49, 0x49, 0x36, // '8'
|
|
0x06, 0x49, 0x49, 0x29, 0x1E, // '9'
|
|
0x00, 0x36, 0x36, 0x00, 0x00, // ':'
|
|
0x00, 0x56, 0x36, 0x00, 0x00, // ';'
|
|
0x00, 0x08, 0x14, 0x22, 0x41, // '<'
|
|
0x14, 0x14, 0x14, 0x14, 0x14, // '='
|
|
0x41, 0x22, 0x14, 0x08, 0x00, // '>'
|
|
0x02, 0x01, 0x51, 0x09, 0x06, // '?'
|
|
0x32, 0x49, 0x79, 0x41, 0x3E, // '@'
|
|
0x7E, 0x11, 0x11, 0x11, 0x7E, // 'A'
|
|
0x7F, 0x49, 0x49, 0x49, 0x36, // 'B'
|
|
0x3E, 0x41, 0x41, 0x41, 0x22, // 'C'
|
|
0x7F, 0x41, 0x41, 0x22, 0x1C, // 'D'
|
|
0x7F, 0x49, 0x49, 0x49, 0x41, // 'E'
|
|
0x7F, 0x09, 0x09, 0x09, 0x01, // 'F'
|
|
0x3E, 0x41, 0x49, 0x49, 0x7A, // 'G'
|
|
0x7F, 0x08, 0x08, 0x08, 0x7F, // 'H'
|
|
0x00, 0x41, 0x7F, 0x41, 0x00, // 'I'
|
|
0x20, 0x40, 0x41, 0x3F, 0x01, // 'J'
|
|
0x7F, 0x08, 0x14, 0x22, 0x41, // 'K'
|
|
0x7F, 0x40, 0x40, 0x40, 0x40, // 'L'
|
|
0x7F, 0x02, 0x0C, 0x02, 0x7F, // 'M'
|
|
0x7F, 0x04, 0x08, 0x10, 0x7F, // 'N'
|
|
0x3E, 0x41, 0x41, 0x41, 0x3E, // 'O'
|
|
0x7F, 0x09, 0x09, 0x09, 0x06, // 'P'
|
|
0x3E, 0x41, 0x51, 0x21, 0x5E, // 'Q'
|
|
0x7F, 0x09, 0x19, 0x29, 0x46, // 'R'
|
|
0x46, 0x49, 0x49, 0x49, 0x31, // 'S'
|
|
0x01, 0x01, 0x7F, 0x01, 0x01, // 'T'
|
|
0x3F, 0x40, 0x40, 0x40, 0x3F, // 'U'
|
|
0x1F, 0x20, 0x40, 0x20, 0x1F, // 'V'
|
|
0x3F, 0x40, 0x38, 0x40, 0x3F, // 'W'
|
|
0x63, 0x14, 0x08, 0x14, 0x63, // 'X'
|
|
0x07, 0x08, 0x70, 0x08, 0x07, // 'Y'
|
|
0x61, 0x51, 0x49, 0x45, 0x43, // 'Z'
|
|
];
|
|
}
|