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:
Berk
2026-07-28 03:33:26 +03:00
committed by GitHub
parent b4cc5f88ca
commit 2b6bd5a532
111 changed files with 9846 additions and 4479 deletions
+11
View File
@@ -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;
+160
View File
@@ -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);
}
}