Compare commits

...

11 Commits

Author SHA1 Message Date
Digote 4bd42795c7 [logging] Migrate HLE diagnostics to SharpEmuLog (#80)
Signed-off-by: Digote <45742711+Digote@users.noreply.github.com>
2026-07-13 12:11:27 +03:00
Mike Saito c03ca32a02 fix(vfs): harden getdirentries and APR filepath resolution (#77)
* fix(vfs): defer host cursor commit in getdirentries

Follow-up VFS hardening task applying the same guest-writes-first discipline established in the time subsystem to directory enumeration.

Deferred Commit in KernelGetdirentriesCore:

- Reordered guest output so the 512-byte dirent buffer is written first via TryWriteCompat, basep is updated second (when non-null) via TryWriteUInt64Compat, and directory.NextIndex is advanced only after both guest writes succeed.

- Removed the early basep write at method entry that could mutate guest memory before buffer validation and advance the host cursor before a successful dirent delivery, causing permanent entry loss on MEMORY_FAULT at bufferAddress.

- EOF handling: when currentIndex >= Entries.Length, write basep with the final offset and return 0 without mutating NextIndex, matching FreeBSD getdirentries(2) semantics and preventing infinite retry loops.

KernelGetdents path: basePointerAddress is passed as 0, so the transaction collapses to buffer write then host cursor advance with no basep side effect.

Out of scope: coalesced {id, size} writes in sceKernelAprResolveFilepathsToIdsAndFileSizes; NetCtl connected-state stubs.

Files: KernelMemoryCompatExports.cs

* fix(vfs): resolve-first bulk commit in APR filepath resolution

Refactored sceKernelAprResolveFilepathsToIdsAndFileSizes to stop writing ids and sizes into guest memory one element at a time.

- Removed the uint.MaxValue placeholder write at the start of each loop iteration.

- Path resolution and file size lookup now fill host-side buffers first; on EFAULT or NOT_FOUND the guest ids/sizes arrays are left untouched.

- ids and sizes are packed into contiguous byte buffers and written with one TryWriteCompat call per output array instead of separate TryWriteUInt32Compat / TryWriteUInt64Compat per index.

- AmprFileRegistry.Register is called only after guest writes succeed.

- AmprFileRegistry.ComputeFileId is internal so ids can be computed without registering paths during the resolve loop.

Files: KernelMemoryCompatExports.cs, AmprFileRegistry.cs
2026-07-13 12:10:20 +03:00
kuba 6e2878f2ff Add commit hash to video window title (#93) 2026-07-13 11:21:41 +03:00
ParantezTech 2e09b015bc Merge branch 'main' of https://github.com/par274/sharpemu 2026-07-13 01:10:08 +03:00
ParantezTech 7b9efd1539 [readme] update screenshot for dreaming sarah 2026-07-13 01:09:45 +03:00
Kaotic fbf2c2d00a [GUI] Add detachable console window (#91)
Add a Split button to detach the emulator console into a resizable window.

The detached console keeps search, auto-scroll, copy, and clear controls. The main console hides while detached and restores when the split window closes if it was previously open.
2026-07-13 00:16:21 +03:00
kuba 8c1507777c [agc] Reset transparent Chowdren effect-layer fills (#83)
Treat the exact untextured transparent-black premultiplied fill used by Chowdren as an overwrite. This prevents Dreaming Sarah fog and vignette render targets from accumulating across frames; SHARPEMU_DISABLE_TRANSPARENT_FILL_CLEAR=1 restores prior behavior.
2026-07-12 19:26:48 +03:00
Berk 8ebc43e758 [github] Fix placeholder formatting in game compatibility template (#84) 2026-07-12 18:46:03 +03:00
j92580498-max 5aadb7495a Libs: add libSceDiscMap HLE exports (ported from Kyty) (#79)
Port the libSceDiscMap stubs from Kyty (InoriRus/Kyty, MIT) into the
SysAbiExport model. Disc-installed titles probe these NIDs on most file
accesses to decide whether a read must be redirected to the disc drive;
answering that every request is already resident on internal storage
keeps I/O on the regular file system path instead of failing with
unresolved-import errors.

- sceDiscMapIsRequestOnHDD (lbQKqsERhtE): validates args, writes 1 to
  the result pointer, returns 0
- fJgP+wqifno / ioKMruft1ek: zero-fill the three output pointers,
  return 0 (names not present in ps5_names.txt; kept as descriptive
  Unknown exports like the existing sceKernelUnknown* convention)
- DISC_MAP_ERROR_INVALID_ARGUMENT (0x81100001) on null pointers,
  matching the documented libSceDiscMap error range
- optional tracing via SHARPEMU_LOG_DISCMAP=1

Co-authored-by: j92580498-max <252151737+j92580498-max@users.noreply.github.com>
2026-07-12 18:24:11 +03:00
Foued Attar cdee77521e 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.
2026-07-12 18:04:12 +03:00
RedBlackAka d2fca37716 [GUI] Basic hotkey and full-screen support (#75) 2026-07-12 12:54:37 +03:00
32 changed files with 1630 additions and 241 deletions
@@ -88,7 +88,7 @@ body:
attributes:
label: Last Milestone / First Error
description: Paste the **last useful milestone line** or the **first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop** from the log. `DEBUG`/`TRACE` lines do not count unless a real error follows them.
placeholder: e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ...
placeholder: "e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ..."
validations:
required: true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+4 -1
View File
@@ -216,7 +216,10 @@
}
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
@@ -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();
+1 -1
View File
@@ -140,7 +140,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
+4 -1
View File
@@ -65,7 +65,10 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
+150
View File
@@ -0,0 +1,150 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using System.Collections.Specialized;
namespace SharpEmu.GUI;
public sealed class ConsoleWindow : Window
{
private readonly AvaloniaList<LogLine> _sourceLines;
private readonly AvaloniaList<LogLine> _visibleLines = new();
private readonly ListBox _list;
private readonly TextBox _searchBox;
private readonly CheckBox _autoScrollCheck;
public ConsoleWindow(
AvaloniaList<LogLine> lines,
Action clear,
bool autoScroll)
{
_sourceLines = lines;
Title = "SharpEmu Console";
Width = 980;
Height = 620;
MinWidth = 520;
MinHeight = 320;
Background = new SolidColorBrush(Color.Parse("#0D1017"));
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
_searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) };
_autoScrollCheck = new CheckBox
{
Content = "Auto-scroll",
IsChecked = autoScroll,
FontSize = 12,
Margin = new Thickness(0, 0, 12, 0),
VerticalAlignment = VerticalAlignment.Center,
};
var copyButton = new Button
{
Classes = { "ghost" },
Content = "Copy",
Padding = new Thickness(10, 4),
Margin = new Thickness(0, 0, 8, 0),
};
var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) };
copyButton.Click += async (_, _) => await CopyAsync();
clearButton.Click += (_, _) => clear();
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
_list = new ListBox
{
Classes = { "console" },
ItemsSource = _visibleLines,
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
{
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
return text;
}),
};
Content = new Grid
{
Margin = new Thickness(12),
RowDefinitions = new RowDefinitions("Auto,*"),
Children =
{
new Grid
{
Margin = new Thickness(0, 0, 0, 8),
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
Children =
{
new TextBlock
{
Classes = { "sectionTitle" },
Text = "CONSOLE",
VerticalAlignment = VerticalAlignment.Center,
},
_searchBox.WithGridColumn(1),
_autoScrollCheck.WithGridColumn(2),
copyButton.WithGridColumn(3),
clearButton.WithGridColumn(4),
},
},
_list.WithGridRow(1),
},
};
lines.CollectionChanged += OnLinesChanged;
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
RefreshVisibleLines();
}
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshVisibleLines();
if (_autoScrollCheck.IsChecked == true)
{
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
}
}
private void RefreshVisibleLines()
{
var query = _searchBox.Text ?? string.Empty;
_visibleLines.Clear();
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
? _sourceLines
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
}
private async Task CopyAsync()
{
if (_visibleLines.Count == 0 || Clipboard is null)
{
return;
}
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
}
}
file static class GridExtensions
{
public static T WithGridColumn<T>(this T control, int column) where T : Control
{
Grid.SetColumn(control, column);
return control;
}
public static T WithGridRow<T>(this T control, int row) where T : Control
{
Grid.SetRow(control, row);
return control;
}
}
+9 -6
View File
@@ -14,7 +14,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="PreferSystemChrome"
ExtendClientAreaTitleBarHeightHint="44"
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
KeyDown="OnKeyDown">
<Grid RowDefinitions="44,*,32">
@@ -159,15 +160,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto, Auto" Margin="16,12,16,8">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." MaxWidth="5500" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." Width="320" />
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="3" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="4" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="5" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
Padding="10,4" />
</Grid>
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
+57 -1
View File
@@ -8,6 +8,7 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Avalonia.VisualTree;
@@ -41,6 +42,7 @@ public partial class MainWindow : Window
private GuiSettings _settings = new();
private EmulatorProcess? _emulator;
private ConsoleWindow? _consoleWindow;
private StreamWriter? _fileLog;
private readonly SndPreviewPlayer _sndPreview = new();
private string? _emulatorExePath;
@@ -97,8 +99,9 @@ public partial class MainWindow : Window
StopButton.Click += (_, _) => StopEmulator();
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true;
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
SelectLogFilePathButton.Click += async (_, _) => await SelectFilePathAsync();
TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled();
DiscordToggle.IsCheckedChanged += (_, _) =>
@@ -292,6 +295,34 @@ public partial class MainWindow : Window
}
}
private void OnKeyDown(object sender, KeyEventArgs args)
{
args.Handled = true;
switch (args.Key)
{
case Key.F11:
OnWindowFullScreen(this, new RoutedEventArgs());
break;
default:
args.Handled = false;
break;
}
}
private void OnWindowFullScreen(object sender, RoutedEventArgs args)
{
if (WindowState == WindowState.FullScreen)
{
WindowState = WindowState.Normal;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
}
else
{
WindowState = WindowState.FullScreen;
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
}
}
private void OnWindowClosing()
{
ReadControlsIntoSettings();
@@ -300,6 +331,7 @@ public partial class MainWindow : Window
_gamepadTimer.Stop();
_sndPreview.Stop();
_discord?.Dispose();
_consoleWindow?.Close();
_emulator?.Dispose();
DropFileLog();
}
@@ -1441,4 +1473,28 @@ public partial class MainWindow : Window
var text = string.Join(Environment.NewLine, _consoleLines.Select(line => line.Text));
await Clipboard.SetTextAsync(text);
}
private void ShowConsoleWindow()
{
if (_consoleWindow is { } window)
{
window.Activate();
return;
}
ConsoleSearchBox.Text = string.Empty;
ConsoleToggle.IsChecked = false;
ConsolePanel.IsVisible = false;
_consoleWindow = new ConsoleWindow(
_consoleLines,
() => { _consoleLines.Clear(); _allConsoleLines.Clear(); },
AutoScrollCheck.IsChecked == true);
_consoleWindow.Closed += (_, _) =>
{
_consoleWindow = null;
ConsoleToggle.IsChecked = true;
ConsolePanel.IsVisible = true;
};
_consoleWindow.Show(this);
}
}
+6 -4
View File
@@ -3,11 +3,13 @@
using System.Buffers.Binary;
using System.Linq;
using SharpEmu.Logging;
namespace SharpEmu.HLE;
public sealed class Aerolib : ISymbolCatalog
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
@@ -108,14 +110,14 @@ public sealed class Aerolib : ISymbolCatalog
if (resourceName == null)
{
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
Log.Error("Embedded resource 'aerolib.bin' not found");
return;
}
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
Log.Error("Failed to open embedded resource stream");
return;
}
@@ -145,11 +147,11 @@ public sealed class Aerolib : ISymbolCatalog
_byExportName[name] = symbol;
}
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
}
}
+6 -3
View File
@@ -3,11 +3,14 @@
using System.Collections.Concurrent;
using System.Reflection;
using SharpEmu.Logging;
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);
@@ -47,7 +50,7 @@ public sealed class ModuleManager : IModuleManager
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue;
}
@@ -106,7 +109,7 @@ public sealed class ModuleManager : IModuleManager
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
{
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
Log.Warning($"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;
@@ -114,7 +117,7 @@ public sealed class ModuleManager : IModuleManager
if ((export.Target & context.TargetGeneration) == 0)
{
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
Log.Warning($"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;
+4
View File
@@ -12,6 +12,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Aerolib\aerolib.bin" />
</ItemGroup>
+5 -1
View File
@@ -1,6 +1,10 @@
{
"version": 2,
"dependencies": {
"net10.0": {}
"net10.0": {
"sharpemu.logging": {
"type": "Project"
}
}
}
}
+51 -1
View File
@@ -3296,10 +3296,60 @@ public static class AgcExports
globalMemoryBindings,
vertexInputs,
renderTargets,
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()));
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
textures,
vertexInputs,
pixelEvaluation.InitialScalarRegisters));
return true;
}
private static readonly bool _transparentFillClearEnabled = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRANSPARENT_FILL_CLEAR"),
"1",
StringComparison.Ordinal);
/// <summary>
/// Chowdren resets its effect layers with an untextured transparent-black
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
/// Treat precisely that draw shape as an overwrite.
/// </summary>
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
VulkanGuestRenderState renderState,
IReadOnlyList<TranslatedImageBinding> textures,
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
IReadOnlyList<uint> pixelUserData)
{
if (!_transparentFillClearEnabled ||
textures.Count != 0 ||
vertexInputs.Count != 0 ||
pixelUserData.Count < 4 ||
renderState.Blend is not
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
})
{
return renderState;
}
for (var index = 0; index < 4; index++)
{
if ((pixelUserData[index] & 0x7FFF_FFFFu) != 0)
{
return renderState;
}
}
return renderState with
{
Blend = renderState.Blend with { Enable = false },
};
}
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
+1 -1
View File
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
return _hostPathsById.TryGetValue(id, out hostPath!);
}
private static uint ComputeFileId(string guestPath)
internal static uint ComputeFileId(string guestPath)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
+11 -13
View File
@@ -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);
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.DiscMap;
/// <summary>
/// HLE implementation of libSceDiscMap, ported from Kyty's LibDiscMap.cpp
/// (https://github.com/InoriRus/Kyty, MIT). Titles installed to internal
/// storage query these entry points on nearly every file access to decide
/// whether a read must be redirected to the Blu-ray drive. Reporting every
/// request as already resident on HDD lets the regular file system path
/// service the read.
/// </summary>
public static class DiscMapExports
{
private const int DiscMapErrorInvalidArgument = unchecked((int)0x81100001);
private const int DiscMapErrorLocationNotMapped = unchecked((int)0x81100002);
private const int DiscMapErrorFileNotFound = unchecked((int)0x81100003);
private const int DiscMapErrorNoBitmapInfo = unchecked((int)0x81100004);
[SysAbiExport(
Nid = "lbQKqsERhtE",
ExportName = "sceDiscMapIsRequestOnHDD",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapIsRequestOnHDD(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var offset = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
var resultAddress = ctx[CpuRegister.Rcx];
if (pathAddress == 0 || resultAddress == 0)
{
return DiscMapErrorInvalidArgument;
}
if (!ctx.TryWriteInt32(resultAddress, 1))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceDiscMap(ctx, "sceDiscMapIsRequestOnHDD", pathAddress, offset, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fJgP+wqifno",
ExportName = "sceDiscMapUnknownFJgP",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
[SysAbiExport(
Nid = "ioKMruft1ek",
ExportName = "sceDiscMapUnknownIoKM",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
private static int WriteMappingTriple(CpuContext ctx, string exportName)
{
var pathAddress = ctx[CpuRegister.Rdi];
var offset = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
var flagsAddress = ctx[CpuRegister.Rcx];
var outAddress1 = ctx[CpuRegister.R8];
var outAddress2 = ctx[CpuRegister.R9];
if (pathAddress == 0 || flagsAddress == 0 || outAddress1 == 0 || outAddress2 == 0)
{
return DiscMapErrorInvalidArgument;
}
if (!ctx.TryWriteUInt64(flagsAddress, 0) ||
!ctx.TryWriteUInt64(outAddress1, 0) ||
!ctx.TryWriteUInt64(outAddress2, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceDiscMap(ctx, exportName, pathAddress, offset, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceDiscMap(CpuContext ctx, string exportName, ulong pathAddress, ulong offset, ulong size)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCMAP"), "1", StringComparison.Ordinal))
{
return;
}
var path = ctx.TryReadNullTerminatedUtf8(pathAddress, 1024, out var value)
? value
: $"<unreadable 0x{pathAddress:X16}>";
Console.Error.WriteLine($"[HLE][DISCMAP] {exportName} path={path} offset=0x{offset:X} size=0x{size:X}");
}
}
+46
View File
@@ -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)];
@@ -1689,15 +1689,15 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var entryCount = (int)count;
Span<uint> localIds = count <= 256 ? stackalloc uint[entryCount] : new uint[entryCount];
Span<ulong> localSizes = count <= 128 ? stackalloc ulong[entryCount] : new ulong[entryCount];
var resolvedGuestPaths = new string[entryCount];
var resolvedHostPaths = new string[entryCount];
for (ulong i = 0; i < count; i++)
{
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var index = (int)i;
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
@@ -1712,23 +1712,47 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
var fileId = AmprFileRegistry.ComputeFileId(guestPath);
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
localIds[index] = fileId;
localSizes[index] = fileSize;
resolvedGuestPaths[index] = guestPath;
resolvedHostPaths[index] = hostPath;
}
Span<byte> sizePayload = count <= 64 ? stackalloc byte[entryCount * sizeof(ulong)] : new byte[entryCount * sizeof(ulong)];
for (ulong i = 0; i < count; i++)
{
BinaryPrimitives.WriteUInt64LittleEndian(sizePayload[(int)(i * sizeof(ulong))..], localSizes[(int)i]);
}
if (!TryWriteCompat(ctx, sizesAddress, sizePayload))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (idsAddress != 0)
{
Span<byte> idPayload = count <= 128 ? stackalloc byte[entryCount * sizeof(uint)] : new byte[entryCount * sizeof(uint)];
for (ulong i = 0; i < count; i++)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
BinaryPrimitives.WriteUInt32LittleEndian(idPayload[(int)(i * sizeof(uint))..], localIds[(int)i]);
}
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
if (!TryWriteCompat(ctx, idsAddress, idPayload))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
for (var i = 0; i < entryCount; i++)
{
AmprFileRegistry.Register(resolvedGuestPaths[i], resolvedHostPaths[i]);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -6350,19 +6374,19 @@ public static class KernelMemoryCompatExports
}
var currentIndex = directory.NextIndex;
if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (currentIndex >= directory.Entries.Length)
{
if (basePointerAddress != 0 &&
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var entryName = directory.Entries[currentIndex];
directory.NextIndex = currentIndex + 1;
var entryBytes = Encoding.UTF8.GetBytes(entryName);
var nameLength = Math.Min(entryBytes.Length, 255);
@@ -6381,6 +6405,13 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (basePointerAddress != 0 &&
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
directory.NextIndex = currentIndex + 1;
ctx[CpuRegister.Rax] = 512;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -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;
+33
View File
@@ -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;
}
}
+14 -15
View File
@@ -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",
+24
View File
@@ -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;
}
}
+23
View File
@@ -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));
}
}
@@ -64,7 +64,7 @@ public static class VideoOutExports
private static long _vblankSignalCount;
private static long _flipSubmitCount;
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version, string? emulatorCommitSha)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title))
@@ -79,9 +79,10 @@ public static class VideoOutExports
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
lock (_stateGate)
{
_windowTitle = $"SharpEmu - {application}{versionSuffix}";
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
}
}
+6
View File
@@ -98,6 +98,12 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
}
}