mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-18 17:06:12 +08:00
Fix UnmanagedCallersOnly boot crash & Core Engine Improvements (CPU, HLE, AGC) (#81)
* [agc] WAIT_REG_MEM suspend/resume, draw packet fixes, new HLE exports, debug cleanup
Rebased onto upstream 79a7437 (par274/sharpemu, rewritten history).
- GpuWaitRegistry: DCBs suspended on unsatisfied WAIT_REG_MEM are re-polled
against guest memory on every submit; fixed 64-bit and standard packet parse
offsets, apply the mask, treat PM4 compare function 0 as "always".
- TryReadSubmittedDrawCount: accept the 5-dword ItDrawIndex2 form emitted by
DcbDrawIndex (count at +4); menu draws were silently discarded before.
- sceAgcDriverSubmitMultiDcbs: reversed ABI (rdi=address array, rsi=dword
sizes, rdx=count).
- VideoOut: vblank events, sceVideoOutGetFlipStatus, buffers registered via
sceVideoOutRegisterBuffers are valid flip targets.
- New HLE: libc stdio (fopen/fread/fseek/ftell/fclose/fgets), Dinkumware
_Getpctype ctype table, NpTrophy2 stubs, AMPR PAK sequential-read tracker,
MsgDialog lifecycle, NGS2 alt NIDs + dummy vtable for handle objects,
guarded memset intrinsic, abort()/strcasecmp null-arg recovery.
- Removed investigation-only code (INT3 breakpoints, qfont/mcpp dumps,
error-candidate printf traces, unconditional debug logs).
First rendered frame: Quake (PPSA01880) presents a 1920x1080 guest frame.
* Implemented a guarded native intrinsic (rep movsb) in DirectExecutionBackend to bypass HLE dispatch overhead, while preserving memory safety checks.
* [hle] clock_gettime clock ids, AudioOut2 canary fix, NID rebinds, new offline stubs
- clock_gettime (lLMT9vJAck0): support CLOCK_SECOND and the *_PRECISE/*_FAST
variants instead of returning EINVAL, which games treated as fatal and
retried in a tight loop.
- AudioOut2: context param writes shrunk to the guest-observed layout (the
old 0x80-byte reset smashed the stack canary at +0x60 and killed audio
init); ContextQueryMemory writes the single u64 the caller expects.
- NGS2: dropped wrong alt-NID aliases (they hash to sceImeUpdate,
sceMouseRead, sceSystemGestureUpdateAllTouchRecognizer - now bound in
their real libraries); added sceNgs2PanInit; fixed VoiceGetState NIDs.
- New verified stubs: sceUltInitialize, sceNpUniversalDataSystemDestroyHandle,
sceNpGetOnlineId, sceNpGetNpReachabilityState, sceImeKeyboardOpen,
sceImeKeyboardGetResourceId, sceMouseOpen, sceKernelAprGetFileSize.
- Import gateway: unwind guest workers at dispatch during backend teardown;
env-gated SHARPEMU_LOG_THREAD_MODE tracing.
* [cpu] Isolate guest execution on native worker threads
Guest entry stubs no longer run above CLR-managed frames: each run is handed
to a pooled raw OS thread whose loop is emitted native code. While guest code
executes there is not a single managed frame on the thread and it stays in
preemptive GC mode, so the GC never walks a frame chain interleaved with
guest stubs that carry no CLR unwind info (the ReversePInvokeBadTransition /
UnmanagedCallersOnly FailFast class of crashes on pumped guest threads).
- NativeGuestExecutor: CreateThread + emitted run loop (WaitForSingleObject,
UnmanagedCallersOnly prologue/epilogue, entry stub call, SetEvent). The
prologue rebinds guest TLS, the host-RSP slot, thread affinity and the
Active* ambient per run, so workers carry no guest identity and pool
freely; the orchestrating managed thread parks in a preemptive wait.
- All three entry sites route through RunGuestEntryStub: guest thread
entries, blocked-continuation resumes, and the main ExecuteEntry.
- Teardown stops workers before any executable stub or TLS index they
reference is freed; a worker that will not stop leaks its loop instead of
freeing running code.
- Kill switch: SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS=1 restores the inline
calli path.
This commit is contained in:
@@ -103,6 +103,15 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
|
||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||
// the managed import gateway means the VEH->managed reentry happened from
|
||||
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
||||
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
|
||||
@@ -19,6 +19,16 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayCalls++;
|
||||
_threadModeGatewayDepth++;
|
||||
if (!_threadModeGatewayFirstLogged)
|
||||
{
|
||||
_threadModeGatewayFirstLogged = true;
|
||||
TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}");
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
||||
@@ -36,6 +46,13 @@ public sealed partial class DirectExecutionBackend
|
||||
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayDepth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
||||
@@ -69,6 +86,12 @@ public sealed partial class DirectExecutionBackend
|
||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
||||
WriteCtxU64(contextRecord, 248, returnRip);
|
||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " +
|
||||
$"gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -204,6 +227,17 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
}
|
||||
// Backend teardown in progress: unwind this worker to the host instead of
|
||||
// dispatching. RunGuestThread parks it as Blocked with no wake handler, its
|
||||
// host thread exits, and RequestGuestThreadTeardown can finish before any
|
||||
// executable memory is freed.
|
||||
if (isGuestWorker &&
|
||||
_guestTeardownRequested &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "backend teardown"))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
return 0uL;
|
||||
}
|
||||
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
// Guest entry stubs must not run above CLR-managed frames on a CLR-created thread:
|
||||
// the suspension/stack-walk machinery then has to traverse a frame chain that is
|
||||
// interleaved with guest stubs carrying no CLR unwind info, and any window in which
|
||||
// the thread-mode bookkeeping disagrees with the actual stack (import dispatch, VEH
|
||||
// redirection) fail-fasts the runtime — the audio_output_thread ~7th-cycle
|
||||
// "attempted to call a UnmanagedCallersOnly method from managed code" crash.
|
||||
//
|
||||
// Guest execution therefore runs on dedicated raw OS threads whose run loop is
|
||||
// emitted native code. While guest code executes there is not a single managed
|
||||
// frame on the thread and it sits in preemptive mode, so the GC ignores it
|
||||
// entirely. The only managed activity on the thread is the reverse-P/Invoke import
|
||||
// dispatch and the per-run prologue/epilogue, which the CLR supports on foreign
|
||||
// threads (lazy attach, preemptive between calls). The managed orchestrator
|
||||
// (RunGuestThread / the main execution thread) parks in a preemptive wait for the
|
||||
// duration of the run, so its own frame chain is never walked across guest frames.
|
||||
private static readonly bool NativeGuestWorkersDisabled =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private readonly object _nativeWorkerGate = new();
|
||||
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
||||
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
||||
private bool _nativeWorkersDisposed;
|
||||
private int _nativeWorkerCreationFailedLogged;
|
||||
|
||||
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint CreateThread(
|
||||
nint lpThreadAttributes,
|
||||
nuint dwStackSize,
|
||||
nint lpStartAddress,
|
||||
nint lpParameter,
|
||||
uint dwCreationFlags,
|
||||
out uint lpThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||
|
||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||
// thread; falls back to the historical inline calli (guest frames above this
|
||||
// thread's managed frames) when workers are disabled or unavailable.
|
||||
//
|
||||
// Callers set the Active* thread-statics before emitting the stub and read the
|
||||
// yield/forced-exit flags right after this returns, so the worker outcome is
|
||||
// copied back into this thread's statics before returning.
|
||||
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
|
||||
{
|
||||
var worker = RentNativeGuestExecutor();
|
||||
if (worker is null)
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
try
|
||||
{
|
||||
var state = _activeGuestThreadState;
|
||||
var nativeReturn = worker.Run(
|
||||
_activeCpuContext!,
|
||||
state,
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
_activeEntryReturnSentinelRip,
|
||||
_activeGuestReturnSlotAddress,
|
||||
(nint)hostRspSlot,
|
||||
(nint)entryStub,
|
||||
state?.AffinityMask ?? 0,
|
||||
out var yieldRequested,
|
||||
out var yieldReason,
|
||||
out var forcedExit);
|
||||
_activeGuestThreadYieldRequested = yieldRequested;
|
||||
_activeGuestThreadYieldReason = yieldReason;
|
||||
_activeForcedGuestExit = forcedExit;
|
||||
return nativeReturn;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReturnNativeGuestExecutor(worker);
|
||||
}
|
||||
}
|
||||
|
||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||
{
|
||||
if (NativeGuestWorkersDisabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (_idleNativeWorkers.Count > 0)
|
||||
{
|
||||
return _idleNativeWorkers.Pop();
|
||||
}
|
||||
}
|
||||
var worker = NativeGuestExecutor.TryCreate(this);
|
||||
if (worker is null)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _nativeWorkerCreationFailedLogged, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Failed to create a native guest worker thread; falling back to inline guest execution.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
worker.Dispose();
|
||||
return null;
|
||||
}
|
||||
_allNativeWorkers.Add(worker);
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
|
||||
private void ReturnNativeGuestExecutor(NativeGuestExecutor worker)
|
||||
{
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (!_nativeWorkersDisposed)
|
||||
{
|
||||
_idleNativeWorkers.Push(worker);
|
||||
return;
|
||||
}
|
||||
}
|
||||
worker.Dispose();
|
||||
}
|
||||
|
||||
private void DisposeNativeGuestExecutors()
|
||||
{
|
||||
NativeGuestExecutor[] workers;
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nativeWorkersDisposed = true;
|
||||
workers = _allNativeWorkers.ToArray();
|
||||
_allNativeWorkers.Clear();
|
||||
_idleNativeWorkers.Clear();
|
||||
}
|
||||
foreach (var worker in workers)
|
||||
{
|
||||
worker.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// A pooled raw OS thread that executes guest entry stubs. The run loop is emitted
|
||||
// native code (no CLR unwind info required — nothing ever unwinds through it):
|
||||
//
|
||||
// loop: WaitForSingleObject(work);
|
||||
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
|
||||
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
|
||||
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
|
||||
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
|
||||
// SetEvent(done); goto loop;
|
||||
//
|
||||
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
|
||||
// the host-RSP slot, the Active* thread-statics and the GuestThreadExecution ambient
|
||||
// on every run, so a worker can be reused for any guest thread.
|
||||
private sealed unsafe class NativeGuestExecutor : IDisposable
|
||||
{
|
||||
private const uint LoopStubSize = 512u;
|
||||
private const uint WorkerStackReservation = 4u * 1024u * 1024u;
|
||||
|
||||
private static nint _waitForSingleObjectAddress;
|
||||
private static nint _setEventAddress;
|
||||
private static nint _exitThreadAddress;
|
||||
|
||||
private readonly DirectExecutionBackend _backend;
|
||||
private readonly AutoResetEvent _workAvailable = new(false);
|
||||
private readonly AutoResetEvent _workCompleted = new(false);
|
||||
private GCHandle _selfHandle;
|
||||
private void* _controlBlock;
|
||||
private void* _loopStub;
|
||||
private nint _threadHandle;
|
||||
private uint _nativeThreadId;
|
||||
|
||||
// Single in-flight run; publication is ordered by the work/done event pair.
|
||||
private CpuContext? _runContext;
|
||||
private GuestThreadState? _runState;
|
||||
private ulong _runGuestThreadHandle;
|
||||
private ulong _runSentinelRip;
|
||||
private ulong _runReturnSlotAddress;
|
||||
private nint _runHostRspSlot;
|
||||
private nint _runEntryStub;
|
||||
private ulong _runAffinityMask;
|
||||
private int _runNativeResult;
|
||||
private bool _runYieldRequested;
|
||||
private string? _runYieldReason;
|
||||
private bool _runForcedExit;
|
||||
private bool _runPrologueFailed;
|
||||
|
||||
// Prologue -> epilogue carry, only touched on the worker thread.
|
||||
private DirectExecutionBackend? _prevBackend;
|
||||
private CpuContext? _prevContext;
|
||||
private ulong _prevSentinel;
|
||||
private ulong _prevReturnSlot;
|
||||
private bool _prevForcedExit;
|
||||
private bool _prevYieldRequested;
|
||||
private string? _prevYieldReason;
|
||||
private GuestThreadState? _prevState;
|
||||
private ulong _prevGuestThreadHandle;
|
||||
private nint _prevHostRspSlot;
|
||||
private int _prevHostThreadId;
|
||||
private bool _entered;
|
||||
|
||||
private NativeGuestExecutor(DirectExecutionBackend backend)
|
||||
{
|
||||
_backend = backend;
|
||||
}
|
||||
|
||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||
{
|
||||
if (!EnsureKernel32Exports())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var executor = new NativeGuestExecutor(backend);
|
||||
if (!executor.Initialize())
|
||||
{
|
||||
executor.Dispose();
|
||||
return null;
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
|
||||
private static bool EnsureKernel32Exports()
|
||||
{
|
||||
if (_exitThreadAddress != 0)
|
||||
{
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||
}
|
||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||
if (kernel32 == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
||||
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
||||
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||
}
|
||||
|
||||
private bool Initialize()
|
||||
{
|
||||
_selfHandle = GCHandle.Alloc(this);
|
||||
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
||||
if (_controlBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
||||
if (_loopStub == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
|
||||
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
|
||||
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
|
||||
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
|
||||
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
|
||||
|
||||
byte* code = (byte*)_loopStub;
|
||||
int offset = 0;
|
||||
void Emit(byte value) => code[offset++] = value;
|
||||
void EmitMovRcxImm64(ulong value)
|
||||
{
|
||||
Emit(0x48); Emit(0xB9);
|
||||
*(ulong*)(code + offset) = value;
|
||||
offset += sizeof(ulong);
|
||||
}
|
||||
void EmitMovRaxImm64(ulong value)
|
||||
{
|
||||
Emit(0x48); Emit(0xB8);
|
||||
*(ulong*)(code + offset) = value;
|
||||
offset += sizeof(ulong);
|
||||
}
|
||||
void EmitCallRax()
|
||||
{
|
||||
Emit(0xFF); Emit(0xD0);
|
||||
}
|
||||
void EmitSetDoneEvent()
|
||||
{
|
||||
EmitMovRcxImm64((ulong)doneHandle);
|
||||
EmitMovRaxImm64((ulong)_setEventAddress);
|
||||
EmitCallRax();
|
||||
}
|
||||
|
||||
// Thread entry leaves RSP ≡ 8 (mod 16); after this every call below happens
|
||||
// at RSP ≡ 0 with shadow space available.
|
||||
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x28); // sub rsp, 0x28
|
||||
int loopStart = offset;
|
||||
EmitMovRcxImm64((ulong)workHandle);
|
||||
Emit(0xBA); // mov edx, INFINITE
|
||||
*(uint*)(code + offset) = 0xFFFFFFFFu;
|
||||
offset += sizeof(uint);
|
||||
EmitMovRaxImm64((ulong)_waitForSingleObjectAddress);
|
||||
EmitCallRax();
|
||||
EmitMovRaxImm64((ulong)_controlBlock);
|
||||
Emit(0x83); Emit(0x38); Emit(0x00); // cmp dword [rax], 0
|
||||
Emit(0x0F); Emit(0x85); // jne stop
|
||||
int stopJump = offset;
|
||||
offset += sizeof(int);
|
||||
EmitMovRcxImm64((ulong)executorHandle);
|
||||
EmitMovRaxImm64((ulong)prologuePtr);
|
||||
EmitCallRax(); // rax = entry stub, or 0 on failure
|
||||
Emit(0x48); Emit(0x85); Emit(0xC0); // test rax, rax
|
||||
Emit(0x0F); Emit(0x84); // je skipEntry
|
||||
int skipJump = offset;
|
||||
offset += sizeof(int);
|
||||
EmitCallRax(); // guest entry stub -> eax
|
||||
int skipEntryOffset = offset;
|
||||
Emit(0x89); Emit(0xC2); // mov edx, eax
|
||||
EmitMovRcxImm64((ulong)executorHandle);
|
||||
EmitMovRaxImm64((ulong)epiloguePtr);
|
||||
EmitCallRax();
|
||||
EmitSetDoneEvent();
|
||||
Emit(0xE9); // jmp loop
|
||||
*(int*)(code + offset) = loopStart - (offset + sizeof(int));
|
||||
offset += sizeof(int);
|
||||
int stopOffset = offset;
|
||||
// Signal done so a Run() racing teardown cannot park forever; a stopped
|
||||
// worker is never re-rented, so the stale signal is unobservable.
|
||||
EmitSetDoneEvent();
|
||||
Emit(0x31); Emit(0xC9); // xor ecx, ecx
|
||||
EmitMovRaxImm64((ulong)_exitThreadAddress);
|
||||
EmitCallRax();
|
||||
Emit(0xCC); // int3 (ExitThread never returns)
|
||||
*(int*)(code + stopJump) = stopOffset - (stopJump + sizeof(int));
|
||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||
|
||||
uint oldProtect = 0;
|
||||
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||
_threadHandle = CreateThread(
|
||||
0,
|
||||
WorkerStackReservation,
|
||||
(nint)_loopStub,
|
||||
0,
|
||||
StackSizeParamIsAReservation,
|
||||
out _nativeThreadId);
|
||||
if (_threadHandle == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"worker_created native_tid={_nativeThreadId} loop=0x{(ulong)_loopStub:X16}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int Run(
|
||||
CpuContext context,
|
||||
GuestThreadState? state,
|
||||
ulong guestThreadHandle,
|
||||
ulong sentinelRip,
|
||||
ulong returnSlotAddress,
|
||||
nint hostRspSlot,
|
||||
nint entryStub,
|
||||
ulong affinityMask,
|
||||
out bool yieldRequested,
|
||||
out string? yieldReason,
|
||||
out bool forcedExit)
|
||||
{
|
||||
_runContext = context;
|
||||
_runState = state;
|
||||
_runGuestThreadHandle = guestThreadHandle;
|
||||
_runSentinelRip = sentinelRip;
|
||||
_runReturnSlotAddress = returnSlotAddress;
|
||||
_runHostRspSlot = hostRspSlot;
|
||||
_runEntryStub = entryStub;
|
||||
_runAffinityMask = affinityMask;
|
||||
_runPrologueFailed = true;
|
||||
_runYieldRequested = false;
|
||||
_runYieldReason = null;
|
||||
_runForcedExit = false;
|
||||
_workAvailable.Set();
|
||||
_workCompleted.WaitOne();
|
||||
_runContext = null;
|
||||
_runState = null;
|
||||
yieldRequested = _runYieldRequested;
|
||||
yieldReason = _runYieldReason;
|
||||
forcedExit = _runForcedExit;
|
||||
if (_runPrologueFailed)
|
||||
{
|
||||
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
|
||||
}
|
||||
return _runNativeResult;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static nint RunPrologue(nint executorHandle)
|
||||
{
|
||||
try
|
||||
{
|
||||
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
|
||||
return executor.EnterRun();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Native guest worker prologue failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static void RunEpilogue(nint executorHandle, int nativeResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
|
||||
executor.ExitRun(nativeResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Native guest worker epilogue failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nint EnterRun()
|
||||
{
|
||||
var backend = _backend;
|
||||
_prevBackend = _activeExecutionBackend;
|
||||
_prevContext = _activeCpuContext;
|
||||
_prevSentinel = _activeEntryReturnSentinelRip;
|
||||
_prevReturnSlot = _activeGuestReturnSlotAddress;
|
||||
_prevForcedExit = _activeForcedGuestExit;
|
||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||
_prevState = _activeGuestThreadState;
|
||||
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||
_entered = true;
|
||||
_activeExecutionBackend = backend;
|
||||
_activeCpuContext = _runContext;
|
||||
_activeEntryReturnSentinelRip = _runSentinelRip;
|
||||
_activeGuestReturnSlotAddress = _runReturnSlotAddress;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
_activeGuestThreadState = _runState;
|
||||
backend.BindTlsBase(_runContext!);
|
||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
if (_runState is { } state)
|
||||
{
|
||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||
}
|
||||
if (_runAffinityMask != 0)
|
||||
{
|
||||
backend.ApplyGuestThreadAffinity(_runAffinityMask);
|
||||
}
|
||||
_runPrologueFailed = false;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"worker_enter guest=0x{_runGuestThreadHandle:X16} stub=0x{(ulong)_runEntryStub:X16}");
|
||||
}
|
||||
return _runEntryStub;
|
||||
}
|
||||
|
||||
private void ExitRun(int nativeResult)
|
||||
{
|
||||
_runNativeResult = nativeResult;
|
||||
_runYieldRequested = _activeGuestThreadYieldRequested;
|
||||
_runYieldReason = _activeGuestThreadYieldReason;
|
||||
_runForcedExit = _activeForcedGuestExit;
|
||||
if (!_entered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_entered = false;
|
||||
if (_runState is { } state)
|
||||
{
|
||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||
_activeExecutionBackend = _prevBackend;
|
||||
_activeCpuContext = _prevContext;
|
||||
_activeEntryReturnSentinelRip = _prevSentinel;
|
||||
_activeGuestReturnSlotAddress = _prevReturnSlot;
|
||||
_activeForcedGuestExit = _prevForcedExit;
|
||||
_activeGuestThreadYieldRequested = _prevYieldRequested;
|
||||
_activeGuestThreadYieldReason = _prevYieldReason;
|
||||
_activeGuestThreadState = _prevState;
|
||||
_prevBackend = null;
|
||||
_prevContext = null;
|
||||
_prevState = null;
|
||||
_prevYieldReason = null;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_controlBlock != null)
|
||||
{
|
||||
*(int*)_controlBlock = 1;
|
||||
}
|
||||
try
|
||||
{
|
||||
_workAvailable.Set();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
var exited = _threadHandle == 0;
|
||||
if (_threadHandle != 0)
|
||||
{
|
||||
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
||||
CloseHandle(_threadHandle);
|
||||
_threadHandle = 0;
|
||||
}
|
||||
if (!exited)
|
||||
{
|
||||
// The worker is still parked in guest code (teardown should have unwound
|
||||
// it first). Leak the stub, control block, events and GC handle rather
|
||||
// than have the thread execute freed memory.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} did not stop; leaking its run loop.");
|
||||
return;
|
||||
}
|
||||
if (_loopStub != null)
|
||||
{
|
||||
VirtualFree(_loopStub, 0u, 32768u);
|
||||
_loopStub = null;
|
||||
}
|
||||
if (_controlBlock != null)
|
||||
{
|
||||
VirtualFree(_controlBlock, 0u, 32768u);
|
||||
_controlBlock = null;
|
||||
}
|
||||
if (_selfHandle.IsAllocated)
|
||||
{
|
||||
_selfHandle.Free();
|
||||
}
|
||||
_workAvailable.Dispose();
|
||||
_workCompleted.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -424,6 +424,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private void ThreadMain()
|
||||
{
|
||||
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_guestThreadHandle);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"runner_start guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
@@ -434,6 +438,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return;
|
||||
}
|
||||
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
|
||||
TraceThreadMode($"runner_run guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_work?.Invoke();
|
||||
@@ -446,6 +455,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"runner_stop guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||
}
|
||||
}
|
||||
@@ -469,6 +482,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
|
||||
|
||||
// Once set, guest worker threads are unwound to the host at their next import
|
||||
// dispatch and Pump refuses to start new ones. This must happen before the
|
||||
// runtime frees trampolines or guest memory: workers that keep running native
|
||||
// guest code during teardown execute freed pages (observed as an execute-AV in
|
||||
// a MEM_FREE module region plus a CLR "UnmanagedCallersOnly" fatal from a Pump
|
||||
// thread entering a freed stub).
|
||||
private volatile bool _guestTeardownRequested;
|
||||
|
||||
private int _readyGuestThreadCount;
|
||||
|
||||
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
||||
@@ -567,6 +588,33 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private static int _nestedVehTraceCount;
|
||||
|
||||
// SHARPEMU_LOG_THREAD_MODE=1 — GC thread-mode corruption investigation. Traces
|
||||
// every managed<->guest transition per guest-thread run cycle so the last event
|
||||
// before a ReversePInvokeBadTransition FailFast identifies the corrupting path.
|
||||
private static readonly bool LogThreadMode =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_THREAD_MODE"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static long _threadModeCycleCounter;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _threadModeCycleId;
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _threadModeGatewayDepth;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _threadModeGatewayCalls;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _threadModeGatewayFirstLogged;
|
||||
|
||||
private static void TraceThreadMode(string message)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[THREADMODE] {message} cycle={_threadModeCycleId} tid={GetCurrentThreadId()} managed={Environment.CurrentManagedThreadId}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
private const uint MEM_COMMIT = 4096u;
|
||||
|
||||
private const uint MEM_RESERVE = 8192u;
|
||||
@@ -625,7 +673,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private unsafe static int CallNativeEntry(void* entry)
|
||||
{
|
||||
var nativeEntry = (delegate* unmanaged[Cdecl]<int>)entry;
|
||||
return nativeEntry();
|
||||
if (!LogThreadMode)
|
||||
{
|
||||
return nativeEntry();
|
||||
}
|
||||
|
||||
_threadModeGatewayFirstLogged = false;
|
||||
var dispatchesBefore = _threadModeGatewayCalls;
|
||||
TraceThreadMode($"native_enter entry=0x{(ulong)entry:X16}");
|
||||
var result = nativeEntry();
|
||||
TraceThreadMode($"native_exit result=0x{result:X8} dispatches={_threadModeGatewayCalls - dispatchesBefore}");
|
||||
return result;
|
||||
}
|
||||
|
||||
private unsafe static void WriteCtxU64(void* contextRecord, int offset, ulong value)
|
||||
@@ -824,6 +882,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
||||
_entryReturnSentinelRip = 0uL;
|
||||
_forcedGuestExit = false;
|
||||
_guestTeardownRequested = false;
|
||||
_importLoopSignatureCount = 0;
|
||||
_importLoopSignatureWriteIndex = 0;
|
||||
_importLoopPatternHits = 0;
|
||||
@@ -1937,6 +1996,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
// Native pre-filter: these exception codes are raised while the thread can be in
|
||||
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
|
||||
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
|
||||
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
|
||||
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||
// code" — this is why no managed throw (even one with a catch handler) ever
|
||||
// survived inside the emulator. Continue the handler search without touching
|
||||
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||
// returned CONTINUE_SEARCH for them.
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes = [0xE0434352u, MSVC_CPP_EXCEPTION, 0xC0000409u, 0xC00000FDu];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
|
||||
EmitByte(code, ref offset, 0x74); // je pass
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
}
|
||||
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||
EmitByte(code, ref offset, 0xC3); // ret
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
|
||||
@@ -2621,6 +2710,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
public void Pump(CpuContext callerContext, string reason)
|
||||
{
|
||||
_ = callerContext;
|
||||
if (_guestTeardownRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var runSynchronously = string.Equals(reason, "entry_return", StringComparison.Ordinal);
|
||||
WakeExpiredBlockedGuestThreads();
|
||||
if (Volatile.Read(ref _readyGuestThreadCount) == 0)
|
||||
@@ -3145,6 +3238,45 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
continuationThread.Join();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parks every guest worker thread before the caller starts freeing executable
|
||||
/// memory. Workers unwind to the host at their next import dispatch (see the
|
||||
/// teardown check in DispatchImport); this waits for their host threads to
|
||||
/// finish within <paramref name="timeoutMs"/>. Returns false when at least one
|
||||
/// worker is still running — the caller must then leak its executable stubs
|
||||
/// rather than free memory a live thread may still execute.
|
||||
/// </summary>
|
||||
private bool RequestGuestThreadTeardown(int timeoutMs)
|
||||
{
|
||||
_guestTeardownRequested = true;
|
||||
Thread[] hostThreads;
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
_readyGuestThreads.Clear();
|
||||
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
|
||||
hostThreads = _guestThreads.Values
|
||||
.Select(static thread => thread.HostThread)
|
||||
.Where(static host => host is not null && host != Thread.CurrentThread && host.IsAlive)
|
||||
.Cast<Thread>()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var deadline = Environment.TickCount64 + timeoutMs;
|
||||
var allStopped = true;
|
||||
foreach (var host in hostThreads)
|
||||
{
|
||||
var remaining = (int)Math.Max(1L, deadline - Environment.TickCount64);
|
||||
if (!host.Join(remaining) && host.IsAlive)
|
||||
{
|
||||
allStopped = false;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Guest worker host thread '{host.Name}' still running after teardown wait.");
|
||||
}
|
||||
}
|
||||
|
||||
return allStopped;
|
||||
}
|
||||
|
||||
private void ClearGuestThreads()
|
||||
{
|
||||
GuestContinuationRunner[] runners;
|
||||
@@ -3424,6 +3556,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ApplyGuestThreadAffinity(thread.AffinityMask);
|
||||
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||
_activeGuestThreadState = thread;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
|
||||
TraceThreadMode(
|
||||
$"cycle_start name='{thread.Name}' guest=0x{thread.ThreadHandle:X16} reason={reason} " +
|
||||
$"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
LastError = null;
|
||||
@@ -3498,6 +3637,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"cycle_end name='{thread.Name}' state={thread.State} " +
|
||||
$"imports={Interlocked.Read(ref thread.ImportCount)} " +
|
||||
$"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}");
|
||||
}
|
||||
_activeGuestThreadState = previousGuestThreadState;
|
||||
Volatile.Write(ref thread.HostThreadId, 0);
|
||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||
@@ -3572,173 +3718,175 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
||||
var previousYieldReason = _activeGuestThreadYieldReason;
|
||||
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"entry_setup name='{name}' entry=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " +
|
||||
$"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
_activeCpuContext = context;
|
||||
_activeEntryReturnSentinelRip = 0;
|
||||
_activeGuestReturnSlotAddress = 0;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
|
||||
int offset = 0;
|
||||
ptr2[offset++] = 83;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 87;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 84;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 87;
|
||||
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 34;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 236;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 189;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 199;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 198;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 194;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 193;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = entryPoint;
|
||||
offset += 8;
|
||||
ptr2[offset++] = byte.MaxValue;
|
||||
ptr2[offset++] = 208;
|
||||
int sentinelOffset = offset + 4;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 139;
|
||||
ptr2[offset++] = 34;
|
||||
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 92;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 91;
|
||||
ptr2[offset++] = 195;
|
||||
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
|
||||
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
||||
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
|
||||
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
|
||||
{
|
||||
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
return GuestNativeCallExitReason.Blocked;
|
||||
}
|
||||
if (ActiveForcedGuestExit)
|
||||
{
|
||||
reason = LastError ?? "guest thread forced exit";
|
||||
return GuestNativeCallExitReason.ForcedExit;
|
||||
}
|
||||
reason = $"returned 0x{nativeReturn:X8}";
|
||||
return GuestNativeCallExitReason.Returned;
|
||||
}
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
reason = "access violation: " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reason = ex.GetType().Name + ": " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
|
||||
RestoreActiveExecutionThread(
|
||||
previousActiveBackend,
|
||||
previousActiveContext,
|
||||
previousSentinel,
|
||||
previousReturnSlotAddress,
|
||||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
}
|
||||
}
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
_activeCpuContext = context;
|
||||
_activeEntryReturnSentinelRip = 0;
|
||||
_activeGuestReturnSlotAddress = 0;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
|
||||
int offset = 0;
|
||||
ptr2[offset++] = 83;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 87;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 84;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 87;
|
||||
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 34;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 236;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 189;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 199;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 198;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 194;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 193;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = entryPoint;
|
||||
offset += 8;
|
||||
ptr2[offset++] = byte.MaxValue;
|
||||
ptr2[offset++] = 208;
|
||||
int sentinelOffset = offset + 4;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 139;
|
||||
ptr2[offset++] = 34;
|
||||
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 92;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 91;
|
||||
ptr2[offset++] = 195;
|
||||
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
|
||||
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
||||
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
|
||||
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
|
||||
{
|
||||
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
return GuestNativeCallExitReason.Blocked;
|
||||
}
|
||||
if (ActiveForcedGuestExit)
|
||||
{
|
||||
reason = LastError ?? "guest thread forced exit";
|
||||
return GuestNativeCallExitReason.ForcedExit;
|
||||
}
|
||||
reason = $"returned 0x{nativeReturn:X8}";
|
||||
return GuestNativeCallExitReason.Returned;
|
||||
}
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
reason = "access violation: " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reason = ex.GetType().Name + ": " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
finally
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
|
||||
RestoreActiveExecutionThread(
|
||||
previousActiveBackend,
|
||||
previousActiveContext,
|
||||
previousSentinel,
|
||||
previousReturnSlotAddress,
|
||||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe GuestNativeCallExitReason ExecuteGuestContinuationEntry(
|
||||
CpuContext context,
|
||||
@@ -3768,6 +3916,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
||||
var previousYieldReason = _activeGuestThreadYieldReason;
|
||||
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"continuation_setup name='{name}' resume=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " +
|
||||
$"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
@@ -3836,12 +3990,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
@@ -4117,7 +4271,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
int num6 = -1;
|
||||
try
|
||||
{
|
||||
num6 = CallNativeEntry(ptr);
|
||||
num6 = RunGuestEntryStub(ptr, num2);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}");
|
||||
PumpUntilGuestThreadsIdle(context, "entry_return");
|
||||
}
|
||||
@@ -4144,6 +4298,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
LastError = "Detected repeating import loop and forced guest unwind to host.";
|
||||
}
|
||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||
RequestGuestThreadTeardown(3000);
|
||||
return false;
|
||||
}
|
||||
if (num6 == 0)
|
||||
@@ -4158,6 +4313,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
LastError = $"Guest entry point returned non-zero: {num6}";
|
||||
}
|
||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||
RequestGuestThreadTeardown(3000);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
@@ -4482,6 +4638,18 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
if (!RequestGuestThreadTeardown(2000))
|
||||
{
|
||||
// A guest worker is still executing native code; freeing the trampolines,
|
||||
// exception-handler stubs, or GC handles under it turns process exit into
|
||||
// an execute-AV / CLR fatal. Leak them — the process is going away anyway.
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Skipping executable stub teardown: guest worker threads are still running.");
|
||||
return;
|
||||
}
|
||||
// Native guest workers park idle once every guest thread has unwound; stop
|
||||
// them before any executable stub or TLS index they reference is freed.
|
||||
DisposeNativeGuestExecutors();
|
||||
ClearImportHandlerTrampolines();
|
||||
_importEntries = Array.Empty<ImportStubEntry>();
|
||||
_runtimeSymbolsByName.Clear();
|
||||
|
||||
@@ -8,9 +8,14 @@ namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioOut2Exports
|
||||
{
|
||||
private const int AudioOut2ContextParamSize = 0x80;
|
||||
// Sized from guest evidence, not SDK headers: Quake keeps its
|
||||
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
|
||||
// and an earlier 0x80-byte ResetParam write zeroed that canary
|
||||
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
|
||||
// the whole audio init). Stay well below 0x60 and only write the prefix we
|
||||
// populate.
|
||||
private const int AudioOut2ContextParamSize = 0x30;
|
||||
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||
private static long _nextContextHandle = 1;
|
||||
private static long _nextUserHandle = 1;
|
||||
private static int _nextPortId;
|
||||
@@ -59,20 +64,13 @@ public static class AudioOut2Exports
|
||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||
memoryInfo.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||
|
||||
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Ime;
|
||||
|
||||
public static class ImeExports
|
||||
{
|
||||
// Quake (KEX) calls this from its main loop and from the audio bring-up path with
|
||||
// an event-handler pointer. No IME session ever exists here, so report success
|
||||
// without invoking the handler ("no pending IME events"). This NID was previously
|
||||
// misbound as an sceNgs2VoiceControl alias, which fed the game NGS2 errors.
|
||||
[SysAbiExport(
|
||||
Nid = "-4GCfYdNF1s",
|
||||
ExportName = "sceImeUpdate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeUpdate(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eaFXjfJv3xs",
|
||||
ExportName = "sceImeKeyboardOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeKeyboardOpen(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dKadqZFgKKQ",
|
||||
ExportName = "sceImeKeyboardGetResourceId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeKeyboardGetResourceId(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
@@ -149,6 +149,19 @@ public static class KernelAprCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Success stub: the argument layout is unknown and callers tolerate the
|
||||
// empty answer (Quake streams fine), so no output payload is written until
|
||||
// the real signature is reversed.
|
||||
[SysAbiExport(
|
||||
Nid = "WvEu7yl3Ivg",
|
||||
ExportName = "sceKernelAprGetFileSize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelAprGetFileSize(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
|
||||
{
|
||||
Span<byte> result = stackalloc byte[sizeof(ulong)];
|
||||
|
||||
@@ -19,10 +19,15 @@ public static class KernelRuntimeCompatExports
|
||||
internal const int ClockProf = 2;
|
||||
internal const int ClockMonotonic = 4;
|
||||
internal const int ClockUptime = 5;
|
||||
internal const int ClockUptimePrecise = 7;
|
||||
internal const int ClockUptimeFast = 8;
|
||||
internal const int ClockRealtimePrecise = 9;
|
||||
internal const int ClockMonotonicPrecise = 11;
|
||||
internal const int ClockRealtimeFast = 10;
|
||||
internal const int ClockMonotonicPrecise = 11;
|
||||
internal const int ClockMonotonicFast = 12;
|
||||
internal const int ClockSecond = 13;
|
||||
internal const int ClockThreadCputimeId = 14;
|
||||
internal const int ClockProcTime = 15;
|
||||
private const int Efault = 14;
|
||||
private const int Einval = 22;
|
||||
private const ulong TlsErrnoOffset = 0x40;
|
||||
@@ -723,10 +728,24 @@ public static class KernelRuntimeCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
// CLOCK_SECOND is FreeBSD's cached whole-second realtime clock (Quake's
|
||||
// audio_output_thread polls it and treated the previous EINVAL as a fatal
|
||||
// init failure, exiting and getting respawned in a loop).
|
||||
case ClockSecond:
|
||||
seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
nanoseconds = 0;
|
||||
return true;
|
||||
|
||||
case ClockMonotonic:
|
||||
case ClockMonotonicPrecise:
|
||||
case ClockMonotonicFast:
|
||||
case ClockUptime:
|
||||
case ClockUptimePrecise:
|
||||
case ClockUptimeFast:
|
||||
// Per-thread/process CPU time approximated with the monotonic clock; games
|
||||
// use these for profiling deltas where monotonicity matters, not absolutes.
|
||||
case ClockThreadCputimeId:
|
||||
case ClockProcTime:
|
||||
GetProcessMonotonicTime(out seconds, out nanoseconds);
|
||||
return true;
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Mouse;
|
||||
|
||||
public static class MouseExports
|
||||
{
|
||||
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
||||
// as an sceNgs2VoiceGetState alias.
|
||||
[SysAbiExport(
|
||||
Nid = "x8qnXqh-tiM",
|
||||
ExportName = "sceMouseRead",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMouse")]
|
||||
public static int MouseRead(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RaqxZIf6DvE",
|
||||
ExportName = "sceMouseOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMouse")]
|
||||
public static int MouseOpen(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
@@ -203,13 +203,12 @@ public static class Ngs2Exports
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-4GCfYdNF1s",
|
||||
ExportName = "sceNgs2VoiceControl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceControlAlt(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||
|
||||
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
|
||||
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
|
||||
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
|
||||
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
|
||||
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
|
||||
// their real libraries; the NIDs below are verified against the hash.
|
||||
[SysAbiExport(
|
||||
Nid = "AbYvTOZ8Pts",
|
||||
ExportName = "sceNgs2VoiceRunCommands",
|
||||
@@ -219,25 +218,25 @@ public static class Ngs2Exports
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-TOuuAQ-buE",
|
||||
ExportName = "sceNgs2VoiceRunCommands",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceRunCommandsAlt(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "x8qnXqh-tiM",
|
||||
ExportName = "sceNgs2VoiceGetState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "wPJGwI2RM2I",
|
||||
Nid = "rEh728kXk3w",
|
||||
ExportName = "sceNgs2VoiceGetStateFlags",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xa8oL9dmXkM",
|
||||
ExportName = "sceNgs2PanInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i0VnXM-C9fc",
|
||||
ExportName = "sceNgs2SystemRender",
|
||||
|
||||
@@ -33,6 +33,30 @@ public static class NpManagerExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Offline profile: the online id payload is left untouched and the call
|
||||
// reports success, matching the other offline NpManager stubs here.
|
||||
[SysAbiExport(
|
||||
Nid = "XDncXQIJUSk",
|
||||
ExportName = "sceNpGetOnlineId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpGetOnlineId(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "e-ZuhGEoeC4",
|
||||
ExportName = "sceNpGetNpReachabilityState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpGetNpReachabilityState(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VfRSmPmj8Q8",
|
||||
ExportName = "sceNpRegisterStateCallback",
|
||||
|
||||
@@ -76,4 +76,14 @@ public static class NpUniversalDataSystemExports
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AUIHb7jUX3I",
|
||||
ExportName = "sceNpUniversalDataSystemDestroyHandle",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpUniversalDataSystem")]
|
||||
public static int NpUniversalDataSystemDestroyHandle(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,4 +83,15 @@ public static class SystemGestureExports
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "wPJGwI2RM2I",
|
||||
ExportName = "sceSystemGestureUpdateAllTouchRecognizer",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSystemGesture")]
|
||||
public static int SystemGestureUpdateAllTouchRecognizer(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Ult;
|
||||
|
||||
public static class UltExports
|
||||
{
|
||||
// Games initialize the ULT (user-level thread) runtime before spinning up
|
||||
// their job systems; an unresolved import here (0x80020002) makes engines
|
||||
// treat the whole task system as unavailable. The emulator schedules guest
|
||||
// pthreads natively, so accepting the initialization is sufficient.
|
||||
[SysAbiExport(
|
||||
Nid = "hZIg1EWGsHM",
|
||||
ExportName = "sceUltInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUlt")]
|
||||
public static int UltInitialize(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user