mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 23:19:44 +08:00
Sdl backend (#670)
* [audio] added sdl audio backend and in-tree atrac9 decoder * [input] replaced per-platform pad readers with sdl gamepad input * [video] added sdl window and host display plumbing * [gui] added host display options and per-game render settings * [bink] synced host movie playback to the guest audio clock * [cpu] hooked windows write faults into guest image tracking * [perf] added guest and render profiling, reserved host cpu lanes * [kernel] fixed stale pthread mutex handle alias * [host] wired the sdl session, save-data paths and project references * [audio] hoisted ajm trace stackalloc out of its loop * [video] Add guest image sync setting * [build] Strip native symbols * reuse
This commit is contained in:
@@ -4383,6 +4383,14 @@ public static partial class AgcExports
|
||||
_tracedProducerlessWaits.Clear();
|
||||
}
|
||||
|
||||
if (!stale)
|
||||
{
|
||||
// Count before the deduplication below: the warning fires once
|
||||
// per label, so on its own it cannot say how often a queue
|
||||
// actually suspends.
|
||||
GpuWaitProfile.RecordSuspend(producer is not null);
|
||||
}
|
||||
|
||||
if (!stale && producer is null &&
|
||||
!_tracedProducerlessWaits.Add(
|
||||
(memory, waiter.WaitAddress)))
|
||||
@@ -5648,6 +5656,8 @@ public static partial class AgcExports
|
||||
|
||||
SharpEmu.Libs.Diagnostics.LoadProgressDiagnostics.TraceGpuWaitSnapshot(
|
||||
ctx.Memory);
|
||||
GpuWaitProfile.RecordMonitorPoll(resumed != 0);
|
||||
GpuWaitProfile.ReportIfDue(remaining);
|
||||
if (remaining == 0)
|
||||
{
|
||||
gpuState.WaitMonitorRunning = false;
|
||||
@@ -5841,6 +5851,7 @@ public static partial class AgcExports
|
||||
$"submission={waiter.SubmissionId} label=0x{waiter.WaitAddress:X16} " +
|
||||
$"resume=0x{waiter.ResumeAddress:X16} remaining_dwords={remainingDwords} " +
|
||||
$"waited_ms={waitedMilliseconds:F3}");
|
||||
GpuWaitProfile.RecordResume(waiter.WaitAddress, waitedMilliseconds);
|
||||
if (remainingDwords == 0)
|
||||
{
|
||||
state.IsSuspended = false;
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
/// <summary>
|
||||
/// Aggregate accounting for suspended WAIT_REG_MEM packets, enabled with
|
||||
/// SHARPEMU_PROFILE_GPU_WAIT=1.
|
||||
///
|
||||
/// The existing <c>agc.wait_suspended</c> warning is deduplicated per label, so
|
||||
/// a label that suspends every frame is reported once and then goes silent —
|
||||
/// which makes the log useless for judging whether GPU waits cost frame time.
|
||||
/// This counts every suspension and every resume, and reports how long the
|
||||
/// queues actually sat blocked.
|
||||
/// </summary>
|
||||
internal static class GpuWaitProfile
|
||||
{
|
||||
public static readonly bool Enabled = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GPU_WAIT"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static readonly double _reportSeconds =
|
||||
double.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GPU_WAIT_REPORT_S"),
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var seconds) && seconds > 0
|
||||
? seconds
|
||||
: 5.0;
|
||||
|
||||
private static readonly object _gate = new();
|
||||
private static readonly Dictionary<ulong, (long Count, double Milliseconds)> _byLabel = new();
|
||||
private static long _suspensions;
|
||||
private static long _resumes;
|
||||
private static long _producerless;
|
||||
private static long _monitorPolls;
|
||||
private static long _monitorEmptyPolls;
|
||||
private static double _totalWaitMilliseconds;
|
||||
private static double _maxWaitMilliseconds;
|
||||
private static long _windowStart = Stopwatch.GetTimestamp();
|
||||
|
||||
public static void RecordSuspend(bool hasProducer)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_suspensions++;
|
||||
if (!hasProducer)
|
||||
{
|
||||
_producerless++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordResume(ulong label, double waitedMilliseconds)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_resumes++;
|
||||
_totalWaitMilliseconds += waitedMilliseconds;
|
||||
if (waitedMilliseconds > _maxWaitMilliseconds)
|
||||
{
|
||||
_maxWaitMilliseconds = waitedMilliseconds;
|
||||
}
|
||||
|
||||
if (_byLabel.Count < 4096)
|
||||
{
|
||||
var existing = _byLabel.TryGetValue(label, out var entry) ? entry : default;
|
||||
_byLabel[label] = (existing.Count + 1, existing.Milliseconds + waitedMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called once per wake of the wait monitor. An empty poll means the monitor
|
||||
/// burned a wakeup without resuming anything, which is the cost of the
|
||||
/// backoff loop rather than of the wait itself.
|
||||
/// </summary>
|
||||
public static void RecordMonitorPoll(bool resumedAny)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_monitorPolls++;
|
||||
if (!resumedAny)
|
||||
{
|
||||
_monitorEmptyPolls++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReportIfDue(int remainingWaiters)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string line;
|
||||
lock (_gate)
|
||||
{
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
var elapsedTicks = now - _windowStart;
|
||||
if (elapsedTicks < _reportSeconds * Stopwatch.Frequency)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_windowStart = now;
|
||||
var seconds = elapsedTicks / (double)Stopwatch.Frequency;
|
||||
|
||||
// Total blocked time across all queues. Above 1000ms/s the queues are
|
||||
// overlapping their stalls, so compare it against the frame budget,
|
||||
// not against wall time.
|
||||
var top = _byLabel
|
||||
.OrderByDescending(entry => entry.Value.Milliseconds)
|
||||
.Take(5)
|
||||
.Select(entry =>
|
||||
$"0x{entry.Key:X}={entry.Value.Milliseconds / seconds:F0}ms/s" +
|
||||
$"/n{entry.Value.Count}")
|
||||
.ToArray();
|
||||
|
||||
line =
|
||||
$"[PERF][GPUWAIT] {seconds:F1}s suspend/s={_suspensions / seconds:F0} " +
|
||||
$"resume/s={_resumes / seconds:F0} producerless/s={_producerless / seconds:F0} " +
|
||||
$"blocked_ms/s={_totalWaitMilliseconds / seconds:F0} " +
|
||||
$"avg_ms={(_resumes > 0 ? _totalWaitMilliseconds / _resumes : 0):F2} " +
|
||||
$"max_ms={_maxWaitMilliseconds:F1} " +
|
||||
$"monitor_polls/s={_monitorPolls / seconds:F0} " +
|
||||
$"empty={(_monitorPolls > 0 ? _monitorEmptyPolls * 100.0 / _monitorPolls : 0):F0}% " +
|
||||
$"outstanding={remainingWaiters} top: {string.Join(" | ", top)}";
|
||||
|
||||
_suspensions = 0;
|
||||
_resumes = 0;
|
||||
_producerless = 0;
|
||||
_monitorPolls = 0;
|
||||
_monitorEmptyPolls = 0;
|
||||
_totalWaitMilliseconds = 0;
|
||||
_maxWaitMilliseconds = 0;
|
||||
_byLabel.Clear();
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(line);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user