mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
cdee77521e
* [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.
583 lines
18 KiB
C#
583 lines
18 KiB
C#
// 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();
|
|
}
|
|
}
|
|
}
|