mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-31 23:19:44 +08:00
[HLE] Fix guest-thread sync and boot for Unreal Engine titles (#102)
* [HLE] Fix guest-thread sync and boot for Unreal Engine titles Silent Hill: The Short Message (and other UE titles) now boot the full engine thread graph instead of hanging early. Four related fixes: - pthread cond/mutex semantics: retain a signal raised with no waiter as pending, and key block/wake on the state's identity rather than a resolved address that could differ between lock and unlock. This ends the ~1.5M-call cond_wait busy-spin. - Warm HLE type initializers and force-JIT their methods on a host thread at Freeze(). A .cctor or first-time JIT running on a guest thread's hijacked stack fail-fasts the CLR as "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed code". - Guest thread scheduling: pump after a wake so a readied thread actually runs, add a dispatcher thread for when every guest thread is parked, and make the pump-depth guard an atomic CAS. - Route mutex/rwlock lock/unlock off the non-blocking leaf-import fast path so a contended lock can deschedule its guest thread. Ported from the unreal-boot-fixes branch. * [HLE] Keep mutex/rwlock unlock on the leaf-import fast path The previous change routed all mutex/rwlock lock and unlock NIDs off the leaf fast path so a contended lock could deschedule its guest thread. But unlock never blocks, and taking it off the fast path made it slow enough that Demon's Souls' job workers livelocked in a guest spinlock (millions of mutex_unlock calls, no import progress, main thread stuck in sceKernelWaitEventFlag). Only *lock* needs to leave the leaf path. Restore the four unlock NIDs (mutex + rwlock) so guest spinlocks stay cheap, while lock/rd/wrlock remain off it for the blocking case Silent Hill needs. * [HLE] Gate pthread_mutex_lock guest-thread blocking (fixes Demon's Souls) Re-enabling cooperative deschedule on a contended pthread_mutex_lock regressed Demon's Souls: its job workers run on libSceFiber, and blocking a guest thread mid-fiber left sceFiberSwitch returning ESRCH followed by a null fiber-context deref (0xC0000005). Bisect confirmed the pthread change as the cause; the game reaches the same point as before it once the block is skipped. Gate the block behind SHARPEMU_MUTEX_LOCK_BLOCKING (off by default) so contended locks fall through to the synchronous host-thread wait. The rest of the pthread fixes (cond_wait pending signals, identity wake keys) are unaffected.
This commit is contained in:
@@ -3,18 +3,17 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class ModuleManager : IModuleManager
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
|
||||
|
||||
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||
private readonly object _registrationGate = new();
|
||||
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||
private bool _isFrozen;
|
||||
|
||||
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||
@@ -28,6 +27,12 @@ public sealed class ModuleManager : IModuleManager
|
||||
throw new InvalidOperationException("Module registration is frozen.");
|
||||
}
|
||||
|
||||
// Deduplicated: one assembly is reached through many types.
|
||||
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var registeredCount = 0;
|
||||
var instances = new Dictionary<Type, object>();
|
||||
|
||||
@@ -50,7 +55,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -76,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
|
||||
{
|
||||
_isFrozen = true;
|
||||
}
|
||||
|
||||
WarmHleTypeInitializers();
|
||||
}
|
||||
|
||||
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
|
||||
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
|
||||
private void WarmHleTypeInitializers()
|
||||
{
|
||||
Assembly[] assemblies;
|
||||
lock (_registrationGate)
|
||||
{
|
||||
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
|
||||
}
|
||||
|
||||
assemblies = WithGuestReachableDependencies(assemblies);
|
||||
var bclWarmed = WarmFrameworkTypeInitializers();
|
||||
|
||||
var warmed = 0;
|
||||
var failed = 0;
|
||||
var jitted = 0;
|
||||
var jitFailed = 0;
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||
}
|
||||
|
||||
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
|
||||
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type is null || type.ContainsGenericParameters)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||
warmed++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A throw here beats a guest-thread fail-fast later; swallow and continue.
|
||||
failed++;
|
||||
}
|
||||
|
||||
// Force-JIT (not execute) every method so no guest thread compiles one first.
|
||||
MethodBase[] members;
|
||||
try
|
||||
{
|
||||
members = type.GetConstructors(allMembers)
|
||||
.Concat<MethodBase>(type.GetMethods(allMembers))
|
||||
.ToArray();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var member in members)
|
||||
{
|
||||
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.PrepareMethod(member.MethodHandle);
|
||||
jitted++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
jitFailed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
|
||||
}
|
||||
|
||||
// Framework .cctors too (but not JIT — the BCL is too large).
|
||||
private static int WarmFrameworkTypeInitializers()
|
||||
{
|
||||
var warmed = 0;
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
var name = assembly.GetName().Name;
|
||||
if (name is null || !IsFrameworkAssembly(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Type[] types;
|
||||
try
|
||||
{
|
||||
types = assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var type in types)
|
||||
{
|
||||
if (type is null || type.ContainsGenericParameters)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||
warmed++;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return warmed;
|
||||
}
|
||||
|
||||
private static bool IsFrameworkAssembly(string assemblyName) =>
|
||||
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
|
||||
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
|
||||
|
||||
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
|
||||
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
|
||||
{
|
||||
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
|
||||
foreach (var assembly in scanned)
|
||||
{
|
||||
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
|
||||
}
|
||||
|
||||
foreach (var assembly in scanned)
|
||||
{
|
||||
AssemblyName[] references;
|
||||
try
|
||||
{
|
||||
references = assembly.GetReferencedAssemblies();
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var reference in references)
|
||||
{
|
||||
var name = reference.Name;
|
||||
if (name is null || !IsGuestReachableInterop(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var loaded = Assembly.Load(reference);
|
||||
result[loaded.FullName ?? name] = loaded;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.Values.ToArray();
|
||||
}
|
||||
|
||||
private static bool IsGuestReachableInterop(string assemblyName) =>
|
||||
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
|
||||
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
|
||||
|
||||
public bool TryGetFunction(string nid, out Delegate function)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||
@@ -109,7 +301,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||
{
|
||||
Log.Warning($"NID '{nid}' not found in dispatch table.");
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
@@ -117,7 +309,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if ((export.Target & context.TargetGeneration) == 0)
|
||||
{
|
||||
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user