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
@@ -129,6 +129,24 @@ public static partial class KernelMemoryCompatExports
private static readonly HashSet<string> _negativeStatCache = new(HostFsPathComparer);
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(HostFsPathComparer);
private static long _nextFileDescriptor = 2;
private static string _applicationTitleId = "UNKNOWN";
public static void ConfigureApplicationInfo(string? titleId)
{
var value = string.IsNullOrWhiteSpace(titleId) ? "UNKNOWN" : titleId.Trim();
Span<char> sanitized = value.Length <= 128
? stackalloc char[value.Length]
: new char[value.Length];
for (var index = 0; index < value.Length; index++)
{
var character = value[index];
sanitized[index] = char.IsAsciiLetterOrDigit(character) || character is '-' or '_'
? char.ToUpperInvariant(character)
: '_';
}
Volatile.Write(ref _applicationTitleId, new string(sanitized));
}
internal static int AllocateGuestFileDescriptor()
{
@@ -5350,7 +5368,7 @@ public static partial class KernelMemoryCompatExports
}
else
{
root = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs", "devlog", "app"));
root = Path.Combine(ResolveGameLogRoot(), "devlog", "app");
}
Directory.CreateDirectory(root);
@@ -5419,14 +5437,20 @@ public static partial class KernelMemoryCompatExports
}
else
{
root = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "logs", "hostapp"));
Environment.SetEnvironmentVariable(hostappVariableName, root);
root = Path.Combine(ResolveGameLogRoot(), "hostapp");
}
Directory.CreateDirectory(root);
return root;
}
private static string ResolveGameLogRoot() =>
Path.GetFullPath(Path.Combine(
AppContext.BaseDirectory,
"user",
"game_logs",
Volatile.Read(ref _applicationTitleId)));
private static string GetPerAppWritableRoot()
{
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
@@ -35,9 +35,13 @@ public static class KernelPthreadCompatExports
private static readonly bool _tracePthreadConds =
_tracePthreads ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
private static readonly bool _tracePthreadFastPath =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_FASTPATH"), "1", StringComparison.Ordinal);
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
private static long _nextSynchronizationWaiterId;
private static int _pthreadFastPathTraceWritten;
private static readonly ConcurrentDictionary<ulong, byte> _pthreadFastPathBusyTraced = new();
private sealed class PthreadMutexState
{
@@ -809,6 +813,7 @@ public static class KernelPthreadCompatExports
if (!TryResolveMutexState(ctx, mutexAddress, createIfZero: true, out var resolvedAddress, out var state))
{
TracePthreadFastPathBusy(tryOnly ? "trylock_missing" : "lock_missing", mutexAddress, resolvedAddress, null, KernelPthreadState.GetCurrentThreadHandle(), (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, null, KernelPthreadState.GetCurrentThreadHandle(), (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -861,6 +866,7 @@ public static class KernelPthreadCompatExports
var ownedResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
TracePthreadFastPathBusy(tryOnly ? "trylock_self" : "lock_self", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
return ownedResult;
}
@@ -949,6 +955,7 @@ public static class KernelPthreadCompatExports
if (tryOnly)
{
TracePthreadFastPathBusy("trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
@@ -1021,6 +1028,7 @@ public static class KernelPthreadCompatExports
{
if (state.RecursionCount <= 0)
{
TracePthreadFastPathUnlock(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
@@ -1187,17 +1195,23 @@ public static class KernelPthreadCompatExports
return 0;
}
if (_mutexStates.ContainsKey(mutexAddress))
var hasPointedHandle =
KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle) &&
pointedHandle != 0 &&
pointedHandle != mutexAddress;
if (_mutexStates.TryGetValue(mutexAddress, out var cachedState))
{
return mutexAddress;
return hasPointedHandle &&
_mutexStates.TryGetValue(pointedHandle, out var pointedState) &&
!ReferenceEquals(pointedState, cachedState)
? pointedHandle
: mutexAddress;
}
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle) && pointedHandle != 0)
if (hasPointedHandle && _mutexStates.ContainsKey(pointedHandle))
{
if (_mutexStates.ContainsKey(pointedHandle))
{
return pointedHandle;
}
return pointedHandle;
}
return mutexAddress;
@@ -1212,13 +1226,35 @@ public static class KernelPthreadCompatExports
return false;
}
var hasPointedHandle = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle);
if (_mutexStates.TryGetValue(mutexAddress, out state))
{
// `mutexAddress` is often the address of the guest's ScePthreadMutex
// variable rather than the handle itself, and that storage is
// reusable — a stack frame recycles the slot, or the guest assigns a
// different mutex to it. The slot therefore outranks anything cached
// under its address: keeping the stale entry would resolve a release
// onto the wrong mutex, leave the real one owned forever and wedge
// every waiter on it (Demon's Souls' Scream audio engine did exactly
// this and spun on scePthreadMutexTrylock).
if (hasPointedHandle &&
pointedHandle != 0 &&
pointedHandle != mutexAddress &&
_mutexStates.TryGetValue(pointedHandle, out var pointedState) &&
!ReferenceEquals(pointedState, state))
{
_mutexStates[mutexAddress] = pointedState;
resolvedAddress = pointedHandle;
state = pointedState;
return true;
}
resolvedAddress = mutexAddress;
return true;
}
if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle))
if (!hasPointedHandle)
{
return false;
}
@@ -2163,6 +2199,60 @@ public static class KernelPthreadCompatExports
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
}
private static void TracePthreadFastPathUnlock(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
ulong currentThreadId)
{
if (!_tracePthreadFastPath || Interlocked.Increment(ref _pthreadFastPathTraceWritten) > 16)
{
return;
}
Span<byte> objectBytes = stackalloc byte[0x50];
if (!ctx.Memory.TryRead(resolvedAddress, objectBytes))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_fastpath_unlock: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} read=failed");
return;
}
Span<ulong> words = stackalloc ulong[10];
for (var index = 0; index < words.Length; index++)
{
words[index] = BinaryPrimitives.ReadUInt64LittleEndian(objectBytes.Slice(index * sizeof(ulong), sizeof(ulong)));
}
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_fastpath_unlock: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
$"current=0x{currentThreadId:X16} owner=0x{state.OwnerThreadId:X16} recursion={state.RecursionCount} " +
$"q00=0x{words[0]:X16} q08=0x{words[1]:X16} q10=0x{words[2]:X16} q18=0x{words[3]:X16} " +
$"q20=0x{words[4]:X16} q28=0x{words[5]:X16} q30=0x{words[6]:X16} q38=0x{words[7]:X16} " +
$"q40=0x{words[8]:X16} q48=0x{words[9]:X16}");
}
private static void TracePthreadFastPathBusy(
string operation,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState? state,
ulong currentThreadId,
int result)
{
if (!_tracePthreadFastPath || !_pthreadFastPathBusyTraced.TryAdd(mutexAddress, 0))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_fastpath_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} " +
$"waiters={(state?.QueuedWaiterCount ?? 0)} result=0x{unchecked((uint)result):X8}");
}
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
{
if (!_tracePthreadConds)