Compare commits

...

12 Commits

Author SHA1 Message Date
Gutemberg Ribeiro 37b3e787de [Cpu] Software-emulate Intel SHA-1 instructions under Rosetta (signal-safe)
Rosetta 2 does not expose Intel SHA-NI to translated x86 processes, so a
guest using sha1msg1/sha1msg2/sha1nexte/sha1rnds4 hits #UD and aborts
(PPSA22102 crashes almost immediately). Decode those four opcodes on the
SIGILL recovery path, evaluate them in software (Sha1InstructionEmulator),
write the result back into the guest's XMM registers, and step past.

The XMM state is bridged through the macOS/Linux signal<->CONTEXT plumbing:
the handler copies the mcontext XMM block to the Win64 CONTEXT the shared
recovery logic uses and back, but only for SIGILL (the sole signal whose
recovery may touch XMM) — not on every demand-paging SIGSEGV.

Critically, the recovery path is now allocation-free. The original decode
allocated managed arrays (new byte[]) *inside the signal handler*; a fault
can interrupt the GC mid-operation, and PPSA22102's tight 99-instruction
SHA-1 loop re-entered the allocator hard enough to corrupt the thread's
allocation context — detonating later as a hard "Invalid Program: attempted
to call a UnmanagedCallersOnly method" at the next managed allocation
(MetalVideoPresenter.CreateBlackFrame). The Iced decoder/reader/buffer are
now thread-static and reused; operand reads use stackalloc via a new
Span<byte> TryReadHostBytes overload.

SHA-1 math independently verified against a reference implementation (4
message-schedule/round vectors + all four round functions). Result:
PPSA22102 goes from crash-at-~0 to 66M imports, past the splash-hide, into
the shared 0-DCB wait wall (no draws yet, but no crash). void Terrarium,
Dream Sara, Silent Hill unregressed. 378 Libs tests pass.
2026-07-19 05:47:23 +01:00
Gutemberg Ribeiro 528985cda0 Merge main into gpu-bootstrap-stall; migrate SyncOnAddress to in-place blocking
Addresses the review merge-blocker on #412: main gained
sceKernelSyncOnAddressWait/Wake (#422) after this branch's fork point, built
on the cooperative RequestCurrentThreadBlock/WakeBlockedThreads API that
phase 6 deletes — the PR-head CI was green while the merge-base run failed
to compile.

- sceKernelSyncOnAddressWait now blocks in place on a per-address gate with
  the standard GuestThreadBlocking pattern (sliced wait, shutdown unwind,
  exception checkpoint, park-state note), keeping the original design's
  wake-generation counter and 100ms self-heal bound. The generation check
  happens under the gate and the wake bumps before pulsing, so the
  register-vs-park race stays closed. Wake-one degrades to wake-all;
  resumed waiters re-evaluate their own condition (futex semantics).
- The separate host-fallback path collapses into the single in-place path.
- #419's lock-file removal on main merges cleanly with ours; #424's new
  pthread semaphore exports sit on our in-place KernelWaitSema unchanged —
  its new PthreadSemaphoreSemanticsTests pass against the rework.

Full solution builds; 431 tests pass (371 Libs incl. #424's semantics
suite, 27 Metal golden, 33 source-gen).
2026-07-19 04:49:05 +01:00
Gutemberg Ribeiro de0816e1b0 [HLE] Implement Hades' high-frequency unresolved AGC DCB builders
Hades boots with ~1.3k unresolved-import hits that corrupt its GPU command
stream — the two hot ones are DCB writers whose garbage return values
derail the game's command-buffer cursor:

- sceAgcDcbSetShRegisterDirect (pFLArOT53+w, ~1.1k calls/boot): single
  SET_SH_REG builder; register packed in rsi (low 16 = offset, high dword =
  byte offset of the dword within a multi-dword write), value in edx. Emits
  the same 3-dword packet as the plural sceAgcCbSetShRegistersDirect path.
- sceAgcDcbWaitOnAddressGetSize (43WJ08sSugE, ~235 calls): size probe for
  the existing sceAgcDcbWaitRegMem writer; mirrors its dword computation
  (7 standard, 6/9 for the 32/64-bit polled forms).
- pthread_rename_np (9vyP6Z7bqzc): POSIX alias of scePthreadRename.

Hades unresolved-import hits drop 1339 -> 6 (two unknown NIDs not in the
public name catalog plus one sceNpTrophy2GetTrophyInfoArray call). Still no
draws — the DCB stream is now well-formed, which unblocks diagnosing why
the game never reaches its render loop. 349 Libs tests pass.
2026-07-19 04:47:25 +01:00
Gutemberg Ribeiro c583728e1b [Gpu] Metal HUD: arm Apple's Performance HUD at launch via SHARPEMU_METAL_HUD=1
Cmd+F1 set developerHUDProperties correctly but the HUD never appeared: the
Performance HUD infrastructure only attaches to a process when it is armed
before the Metal device initializes, so toggling properties on a live layer
is inert when launched without it.

SHARPEMU_METAL_HUD=1 now sets MTL_HUD_ENABLED=1 (the documented launch-time
mechanism; setenv lands in the native environ before device creation) so the
HUD shows from startup and Cmd+F1 toggles it per-layer. When toggled on
without the env var, log a hint pointing at the launch flag instead of
failing silently.
2026-07-19 04:33:03 +01:00
Gutemberg Ribeiro 22a7ca97ed [HLE] Resolve Lunar Lander's remaining unresolved imports
With the threading rework letting Lunar Lander Beyond run ~8x further
(800K -> 6.7M imports), five previously-unreached imports surfaced. All
resolved -> zero unresolved NIDs:

- unlink (VAzswvTOCzI): POSIX alias of sceKernelUnlink.
- sceAgcDriverGetEqContextId (Zw7uUVPulbw): returns a single stable driver
  eq-context id (1); events are keyed on (equeue, ident, filter) so one id
  satisfies the contract.
- sceImeKeyboardGetInfo (VkqLPArfFdc): zeroes the info struct (no keyboard
  connected) and returns OK.
- sceSysmoduleGetModuleInfoForUnwind (4fU5yvOkVG4): delegates to the
  existing sceKernelGetModuleInfoForUnwind (same contract, Gen5 module
  surface).
- _is_signal_return (crb5j7mkk1c): libc unwinder predicate; guest signal
  returns use no guest-visible trampoline, so always false.

Lunar now reaches its next wall (a C++ exception-unwind loop, 0 DCBs) with
no unresolved imports; the other games are unaffected (additive exports).
349 Libs tests pass.
2026-07-19 04:11:05 +01:00
Gutemberg Ribeiro 3d4a8803f5 [Core] Phase 6: purge the cooperative scheduling machinery
Final phase of the 1:1 threading rework: delete everything the in-place
blocking model made unreachable.

Removed:
- Pump / PumpUntilGuestThreadsIdle's pump step / the pump-depth guard, and
  the two background dispatcher threads that existed to drain the ready
  queue (one now survives solely as the env-gated guest-thread snapshot
  logger, sleeping 100ms and only when enabled).
- WakeBlockedThreads / WakeExpiredBlockedGuestThreads and every wake-key.
- The ready queue (_readyGuestThreads/_readyGuestThreadCount) and the
  Ready-claim protocol (TryClaimReadyGuestThreadLocked,
  DispatchReadyGuestThreads). TryStartThread now claims the new thread and
  schedules it directly onto its dedicated runner.
- Continuation-block capture and resume: RequestCurrentThreadBlock,
  TryConsumeCurrentThreadBlock, IGuestThreadBlockWaiter,
  DelegateGuestThreadBlockWaiter, ExecuteBlockedGuestThreadContinuation,
  RegisterBlockedGuestThreadContinuation, the import-boundary yield-to-host
  consumers, and the GuestThreadState Blocked* fields.
- ResumeBlockedNestedGuestCallback (the nested-callback Thread.Sleep(1)
  spin) — nested guest callbacks can no longer exit "Blocked".
- TryRaiseGuestException's parked-continuation hijack path; every live
  target now queues, delivered at the import-return safe point or a
  wait-loop checkpoint (phase 5).
- Pump calls in sceKernelUsleep/nanosleep, and the obsolete
  GuestThreadBlockWaiterRepresentationTests.

RESULT — the purge did not just hold the line, it moved it. Two of the
long-stuck early-boot games advanced immediately because their main threads
had been wedging inside this machinery (usleep's Pump on every poll
iteration), exactly as diagnosed:
- Silent Hill f: presenter starts for the FIRST TIME ("Metal VideoOut
  presenting 3840x2160"), 2.4M -> 4.6M imports; next wall is its TLS poll.
- Lunar Lander Beyond: 800K -> 6.7M imports (~8x); next wall is an
  unresolved NID (Zw7uUVPulbw) + clock poll loop.
Rest of the matrix: Hades 36.5M, Dead Cells 10.7M, void Terrarium (deeper
into AGC work; the old run inflated import counts with WaitEventFlag
timeout spam), Dream Sara, Astro Bot — all healthy, zero stall watchdogs,
zero unrecovered crashes across all eight. 349 Libs tests pass.
2026-07-18 23:37:17 +01:00
Gutemberg Ribeiro 5099816eb9 [Kernel] Phase 5: guest exception delivery to in-place-parked threads
Fifth phase of the 1:1 threading rework: keep IL2CPP stop-the-world suspend
(sceKernelRaiseException-style delivery via TryRaiseGuestException) working
now that blocked threads park in place.

Previously a blocked thread sat State=Blocked with an idle runner, and
delivery hijacked its saved continuation on that runner. In the new model a
parked thread keeps its executor busy inside the HLE wait and never reaches
the import-return safe point where queued exceptions are consumed — a
stop-the-world suspend targeting it would hang the collector.

Mechanism (the SA_RESTART-style "signal interrupts a blocking wait" a real
kernel provides):
- TryRaiseGuestException queues the exception as before and additionally
  flags the target via GuestThreadBlocking.RequestInterrupt.
- Every sliced wait loop calls GuestThreadBlocking.Checkpoint while holding
  its gate; on a flagged thread it releases the gate, runs the registered
  deliverer on the target's OWN host thread (preserving the native-thread
  identity/TLS the handler registered), re-acquires the gate, and re-checks
  the wait predicate. Never runs guest code while holding an HLE gate — the
  handler may re-enter the same primitive.
- The deliverer (DeliverPendingGuestExceptionInPlaceForCurrentThread) reuses
  the existing safe-point delivery; the default continuation makes the
  exception context fall back to the live import-entry registers, which are
  the interrupted state.
- Checkpoint has an IsEmpty fast path: zero cost unless an exception is
  actually pending (none of the current 8-game matrix fires any).

Join needed no change (host-side poll on thread state works unchanged);
shutdown was wired in phase 1 (RequestShutdown unwinds parked threads).

Verified (Metal, headless): void 6.9M imports, Hades 36.4M, Dead Cells,
Dream Sara, Astro 6.8M — no regressions, zero unrecovered crashes (one run
hit the known pre-existing flaky TBB functor race; clean on rerun). All 351
Libs tests pass.
2026-07-18 23:14:28 +01:00
Gutemberg Ribeiro c17c6ed303 [Kernel] Phase 4: rwlocks block in place; park-state diagnostics
Fourth phase of the 1:1 threading rework. pthread rwlocks were the last
primitive on the cooperative path; with them converted, nothing calls
RequestCurrentThreadBlock any more, so the continuation/wake-key machinery
(including the nested-callback Thread.Sleep(1) spin the primary thread used)
is unreachable by construction — the main thread now parks in place inside
HLE calls like every other thread.

- PthreadRwlockLockCore: drop the RequestCurrentThreadBlock branches bolted
  in front of the existing Monitor.Wait loops; writers and readers park in
  place on SyncRoot (sliced for teardown). Removed RwlockWaiter,
  TryAcquireBlockedRwlock, the per-lock WakeKey, and the unlock's
  WakeBlockedThreads dispatch.
- Diagnostics: in-place-parked threads previously reported state=Running
  block=none, blinding the stall watchdog. GuestThreadBlocking now tracks
  what each parked thread waits on (recorded only on the contended slow
  path); the stall dump's per-thread block= field falls back to it, and a
  new "Stall in-place blocks" line lists every parked thread — covering the
  primary thread, which has no _guestThreads entry.

The new diagnostics immediately paid for themselves on Silent Hill f: all
38 parked threads are workers (cond_wait/WaitEqueue); the main thread is
NOT parked in any primitive — it spins in guest code calling the
scePthreadGetspecific leaf (leaf fast-path calls are neither counted nor
traced), i.e. it polls a TLS slot that is never filled. That relocates the
game's wall from "scheduler wedge" to "missing produced value," to be
chased in the bootstrap-cohort investigation.

Verified (Metal, headless): void 5.6M imports, Hades 34.4M, Dead Cells
10.1M, Dream Sara, Astro 6.8M (rwlock-heavy: zero writer-conflict reports),
Silent Hill / Lunar stable — zero unrecovered crashes across all eight.
All 351 Libs tests pass.
2026-07-18 22:52:16 +01:00
Gutemberg Ribeiro ca67322243 [Kernel] Phase 3: event queues block in place
Third phase of the 1:1 threading rework. sceKernelWaitEqueue now blocks the
guest thread's host thread in place on the shared queue gate, unifying the
former split paths (cooperative RequestCurrentThreadBlock for infinite waits,
Monitor loop for timed waits) into one.

- Drain matching events first (DequeueEvents); if none, park on
  Monitor.Wait(_eventQueueGate), re-checking for a pending event on each wake
  until one is delivered, the queue is deleted, or the timeout lapses.
  Monitor releases the gate and parks atomically, so an EnqueueEvent /
  TriggerDisplayEvent / TriggerUserEvent PulseAll cannot be lost between the
  emptiness check and the park. kqueue/kevent semantics: usec timeout,
  infinite when the arg pointer is null, zero degrades to an instant poll.
- WakeEventQueue now pulses the gate (waking in-place waiters) instead of
  WakeBlockedThreads on a per-handle wake key. DeleteEqueue pulses so a
  parked waiter observes the deletion.
- Removed the EqueueWaiter IGuestThreadBlockWaiter, ResumeWaitEqueue,
  HasPendingEvents, and the per-handle wake-key cache.

Waits are sliced (GuestThreadBlocking.WaitSliceMilliseconds) so teardown
unwinds parked threads; the woken thread drains its own events under the gate.

Verified (Metal, headless, vs baseline): void 7.0M imports (0 stalls),
Dream Sara / Dead Cells / Hades (~36.6M) / Astro / Silent Hill / Lunar all
unregressed, zero stall watchdogs and zero unrecovered crashes across all
eight. All 351 Libs tests pass. Silent Hill's IoDispatcher/IoService now
block in place but the game's early-boot wall (main thread parked at a leaf
import) is unchanged — that is phase 4's target.
2026-07-18 22:32:00 +01:00
Gutemberg Ribeiro 19dc71ba22 [Kernel] Phase 2: semaphores + event flags block in place
Second phase of the 1:1 threading rework. Semaphores and event flags now
block the guest thread's host thread in place on their existing gate object,
dropping the RequestCurrentThreadBlock cooperative path, the Pump-driven
fallback loop, and the WakeBlockedThreads wake-key hand-off.

- sceKernelWaitSema / sem_wait: park on Monitor.Wait(gate); SignalSema/Cancel
  pulse. Monitor releases the gate and parks atomically, so a signal issued
  the instant before the park cannot be lost. Semantics follow FreeBSD
  ksem/sem_wait (acquire when count>=need, else sleep). The separate
  WaitSemaphoreOnHostThread fallback is folded into the one path.
- sceKernelWaitEventFlag: park on Monitor.Wait(gate), re-evaluating the
  AND/OR pattern with optional clear on each wake (reusing the existing
  IsSatisfied/ApplyClearMode/TryCompleteSatisfiedWait helpers); Set/Cancel
  pulse. Removes the cooperative predicate/resume closures, the scheduler
  Pump fallback, and the RE-era wait-object/frame-chain diagnostics that
  only existed to debug the old lost-wake path.

Waits are sliced (GuestThreadBlocking.WaitSliceMilliseconds) purely so
teardown unwinds parked threads. Net removal of the WakeKey field, the
per-handle wake-key formatting, and ~200 lines of dead machinery.

Verified (Metal, headless, vs baseline): void 6.3M imports (unregressed, 0
stalls), Dream Sara / Dead Cells / Hades / Astro / Silent Hill / Lunar all
unregressed — Dead Cells advanced 5.2M->10.3M and Hades holds ~35M/45s.
Zero unrecovered crashes across all eight games (Astro's one fault is the
pre-existing recovered TBB functor-construction race). All 351 Libs tests
pass. Lunar's 800K stall is unchanged — its producer-never-runs root is
deeper (equeue/main-thread, phases 3-4), not semaphore mechanics.
2026-07-18 21:50:24 +01:00
Gutemberg Ribeiro 2d72d0e4fc Remove NuGet lock file from the repo
src/SharpEmu.CLI/packages.lock.json was the only lock file left and nothing
enforces it: CI does not restore with --locked-mode, and the committed lock
was generated with a newer SDK than global.json pins (ILLink.Tasks 10.0.8 vs
the 10.0.3 that SDK 10.0.103 produces), so anyone on the pinned SDK who ran
a locked-mode restore failed on an inconsistency they could not fix without
rewriting the file. Package versions stay centralized in
Directory.Packages.props. Drop RestorePackagesWithLockFile and gitignore
future regenerations.
2026-07-18 21:24:41 +01:00
Gutemberg Ribeiro de45605e10 [Kernel] Phase 1: pthread mutex/cond block in place on host primitives
First phase of replacing the cooperative continuation/wake-key scheduling
with 1:1 in-place blocking. Guest threads already own a dedicated host
thread (GuestExecutionRunner); the continuation layer on top of them split
block-and-wake into two racy steps and produced lost wakeups (Hades: 514/611
cond-timeout re-acquires never woken).

pthread mutexes and condition variables now block the guest thread's host
thread inside the HLE call:

- Mutex lock parks on Monitor.Wait(state); unlock pulses. Monitor releases
  the lock and parks atomically, so a wake cannot be lost. Waiter queues,
  wake keys, grant/complete hand-offs and the FIFO reservation protocol are
  deleted (barging on wake matches real POSIX mutexes).
- Cond wait registers as a waiter, releases the mutex, and parks on
  SyncRoot; signal/broadcast count pending signals (no-op with no waiters,
  per POSIX) and pulse. The mutex is re-acquired in place on every return
  path. Timed waits compute the deadline locally — no Timer callbacks.
- Waits are sliced (50ms) purely so teardown can unwind parked threads:
  new GuestThreadBlocking.RequestShutdown(), signaled from
  RequestHostShutdown. Deliberately NOT signaled from the Execute() finally:
  Execute runs once per module initializer during boot, and signaling there
  poisoned every contended lock after module init (caught in verification).

Net -416/+104 lines in the pthread compat layer.

Verified (Metal backend, 45-50s headless each, vs pre-change baseline):
void Terrarium 4.9M imports/presenter (unregressed), Dream Sara presenter
(unregressed), Dead Cells 5.2M/presenter (unregressed), Astro Bot 6.9M
(baseline 4.8-5.3M), Lunar Lander presents, Silent Hill f unchanged, and
Hades 37.4M imports in 45s vs 24.9M in 60s (~2x faster) — zero crashes,
zero stall watchdogs, zero shutdown-path misfires across all seven. All
351 Libs tests pass.
2026-07-18 21:16:08 +01:00
22 changed files with 1128 additions and 2202 deletions
+1
View File
@@ -42,3 +42,4 @@ ehthumbs.db
.vs/
.idea/
packages.lock.json
@@ -0,0 +1,81 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Numerics;
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// The four Intel SHA-extension operations used by SHA-1 code. This is the arithmetic half of
/// the direct-execution SIGILL fallback used when an x86-64 guest runs under Rosetta 2, which
/// does not expose Intel SHA instructions to translated processes.
/// </summary>
public static class Sha1InstructionEmulator
{
public static Sha1Vector MessageSchedule1(Sha1Vector destination, Sha1Vector source) => new(
destination.Lane0 ^ source.Lane2,
destination.Lane1 ^ source.Lane3,
destination.Lane2 ^ destination.Lane0,
destination.Lane3 ^ destination.Lane1);
public static Sha1Vector MessageSchedule2(Sha1Vector destination, Sha1Vector source)
{
var lane3 = BitOperations.RotateLeft(destination.Lane3 ^ source.Lane2, 1);
return new Sha1Vector(
BitOperations.RotateLeft(destination.Lane0 ^ lane3, 1),
BitOperations.RotateLeft(destination.Lane1 ^ source.Lane0, 1),
BitOperations.RotateLeft(destination.Lane2 ^ source.Lane1, 1),
lane3);
}
public static Sha1Vector NextE(Sha1Vector destination, Sha1Vector source) => new(
source.Lane0,
source.Lane1,
source.Lane2,
unchecked(source.Lane3 + BitOperations.RotateLeft(destination.Lane3, 30)));
public static Sha1Vector FourRounds(Sha1Vector destination, Sha1Vector source, byte function)
{
uint a = destination.Lane3;
uint b = destination.Lane2;
uint c = destination.Lane1;
uint d = destination.Lane0;
uint e = 0;
uint constant = (function & 3) switch
{
0 => 0x5A82_7999u,
1 => 0x6ED9_EBA1u,
2 => 0x8F1B_BCDCu,
_ => 0xCA62_C1D6u,
};
for (var round = 0; round < 4; round++)
{
uint choose = (function & 3) switch
{
0 => (b & c) ^ (~b & d),
2 => (b & c) ^ (b & d) ^ (c & d),
_ => b ^ c ^ d,
};
uint word = round switch
{
0 => source.Lane3,
1 => source.Lane2,
2 => source.Lane1,
_ => source.Lane0,
};
uint next = unchecked(choose + BitOperations.RotateLeft(a, 5) + word + e + constant);
e = d;
d = c;
c = BitOperations.RotateLeft(b, 30);
b = a;
a = next;
}
return new Sha1Vector(d, c, b, a);
}
}
/// <summary>The four little-endian 32-bit lanes of an XMM register.</summary>
public readonly record struct Sha1Vector(uint Lane0, uint Lane1, uint Lane2, uint Lane3);
@@ -1113,7 +1113,12 @@ public sealed partial class DirectExecutionBackend
}
}
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer) =>
TryReadHostBytes(address, buffer.AsSpan());
// Span overload so signal-handler recovery paths can read into a stackalloc
// buffer instead of allocating managed arrays inside the handler.
private unsafe static bool TryReadHostBytes(ulong address, Span<byte> buffer)
{
if (address < 65536)
{
@@ -1137,7 +1142,7 @@ public sealed partial class DirectExecutionBackend
try
{
Marshal.Copy((nint)address, buffer, 0, buffer.Length);
new ReadOnlySpan<byte>((void*)address, buffer.Length).CopyTo(buffer);
return true;
}
catch
@@ -8,21 +8,21 @@ using SharpEmu.Core.Cpu.Emulation;
namespace SharpEmu.Core.Cpu.Native;
// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
// Software fallback for unsupported guest CPU instructions.
//
// Guest code runs natively, so the guest and host share the same virtual address space and the
// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
// one of these extensions it raises #UD instead of executing the opcode; without this the title
// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
// does not fully model.
// the OS to continue. BMI1/BMI2/ABM GPR operations and the Intel SHA-1 XMM operations are handled;
// anything else falls through to the existing diagnostics unchanged.
public sealed partial class DirectExecutionBackend
{
// Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
// with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
private const int CTX_EFLAGS = 68;
private const int CTX_XMM0 = 0x1A0;
// STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
private const uint StatusIllegalInstruction = 0xC000001Du;
@@ -34,6 +34,8 @@ public sealed partial class DirectExecutionBackend
private static int _bmiSoftwareFallbackAnnounced;
private static long _bmiInstructionsEmulated;
private static int _sha1SoftwareFallbackAnnounced;
private static long _sha1InstructionsEmulated;
private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
{
@@ -42,6 +44,11 @@ public sealed partial class DirectExecutionBackend
return false;
}
if (TryRecoverSha1Instruction(contextRecord, rip, in instruction))
{
return true;
}
if (instruction.Op0Kind != OpKind.Register ||
!TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
{
@@ -72,6 +79,139 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe bool TryRecoverSha1Instruction(
void* contextRecord,
ulong rip,
in Instruction instruction)
{
if ((!OperatingSystem.IsWindows() && !_posixVectorContextAvailable) ||
instruction.Mnemonic is not (Mnemonic.Sha1msg1 or Mnemonic.Sha1msg2 or
Mnemonic.Sha1nexte or Mnemonic.Sha1rnds4) ||
instruction.Op0Kind != OpKind.Register ||
!TryGetXmmIndex(instruction.Op0Register, out var destinationIndex) ||
!TryReadSha1VectorOperand(contextRecord, in instruction, 1, out var source))
{
return false;
}
var destination = ReadXmm(contextRecord, destinationIndex);
Sha1Vector result;
switch (instruction.Mnemonic)
{
case Mnemonic.Sha1msg1:
result = Sha1InstructionEmulator.MessageSchedule1(destination, source);
break;
case Mnemonic.Sha1msg2:
result = Sha1InstructionEmulator.MessageSchedule2(destination, source);
break;
case Mnemonic.Sha1nexte:
result = Sha1InstructionEmulator.NextE(destination, source);
break;
case Mnemonic.Sha1rnds4:
if (instruction.Op2Kind != OpKind.Immediate8)
{
return false;
}
result = Sha1InstructionEmulator.FourRounds(destination, source, instruction.Immediate8);
break;
default:
return false;
}
WriteXmm(contextRecord, destinationIndex, result);
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
if (!_posixSignalWarmup)
{
Interlocked.Increment(ref _sha1InstructionsEmulated);
if (Interlocked.Exchange(ref _sha1SoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks Intel SHA instructions used by the guest; " +
"emulating SHA-1 instructions in software.");
}
}
return true;
}
private unsafe bool TryReadSha1VectorOperand(
void* contextRecord,
in Instruction instruction,
int operandIndex,
out Sha1Vector value)
{
switch (instruction.GetOpKind(operandIndex))
{
case OpKind.Register:
if (TryGetXmmIndex(instruction.GetOpRegister(operandIndex), out var sourceIndex))
{
value = ReadXmm(contextRecord, sourceIndex);
return true;
}
break;
case OpKind.Memory:
if (TryComputeMemoryAddress(contextRecord, in instruction, out var address))
{
Span<byte> bytes = stackalloc byte[16];
if (TryReadHostBytes(address, bytes))
{
value = new Sha1Vector(
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(0, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(4, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(8, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(12, 4)));
return true;
}
}
break;
}
value = default;
return false;
}
private static unsafe Sha1Vector ReadXmm(void* contextRecord, int index)
{
uint* lanes = (uint*)((byte*)contextRecord + CTX_XMM0 + index * 16);
return new Sha1Vector(lanes[0], lanes[1], lanes[2], lanes[3]);
}
private static unsafe void WriteXmm(void* contextRecord, int index, Sha1Vector value)
{
uint* lanes = (uint*)((byte*)contextRecord + CTX_XMM0 + index * 16);
lanes[0] = value.Lane0;
lanes[1] = value.Lane1;
lanes[2] = value.Lane2;
lanes[3] = value.Lane3;
}
private static bool TryGetXmmIndex(Register register, out int index)
{
switch (register)
{
case Register.XMM0: index = 0; return true;
case Register.XMM1: index = 1; return true;
case Register.XMM2: index = 2; return true;
case Register.XMM3: index = 3; return true;
case Register.XMM4: index = 4; return true;
case Register.XMM5: index = 5; return true;
case Register.XMM6: index = 6; return true;
case Register.XMM7: index = 7; return true;
case Register.XMM8: index = 8; return true;
case Register.XMM9: index = 9; return true;
case Register.XMM10: index = 10; return true;
case Register.XMM11: index = 11; return true;
case Register.XMM12: index = 12; return true;
case Register.XMM13: index = 13; return true;
case Register.XMM14: index = 14; return true;
case Register.XMM15: index = 15; return true;
default: index = 0; return false;
}
}
private unsafe bool TryEvaluate(
void* contextRecord,
in Instruction instruction,
@@ -194,19 +334,37 @@ public sealed partial class DirectExecutionBackend
}
}
// A managed allocation inside a signal handler is unsafe: the fault can
// interrupt the GC mid-operation, and re-entering the allocator corrupts
// the thread's allocation context (observed as a hard "Invalid Program" at
// the next managed alloc, amplified by tight SHA-1 recovery loops). These
// thread-static objects are created once per thread and reused so the
// recovery path never allocates on the hot path.
[ThreadStatic]
private static byte[]? _decodeBuffer;
[ThreadStatic]
private static ByteArrayCodeReader? _decodeReader;
[ThreadStatic]
private static Decoder? _decoder;
private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
{
var buffer = _decodeBuffer ??= new byte[MaxInstructionBytes];
var reader = _decodeReader ??= new ByteArrayCodeReader(buffer);
var decoder = _decoder ??= Decoder.Create(64, reader);
// Try the full instruction window first, then shrink so a fault near the end of a mapped
// page (where fewer than 15 bytes are readable) still decodes.
// page (where fewer than 15 bytes are readable) still decodes. The buffer is reused, so
// clear the tail past the readable window to keep decoding deterministic.
foreach (var attempt in DecodeWindowSizes)
{
var buffer = new byte[attempt];
if (!TryReadHostBytes(rip, buffer))
if (!TryReadHostBytes(rip, buffer.AsSpan(0, attempt)))
{
continue;
}
var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
buffer.AsSpan(attempt).Clear();
reader.Position = 0;
decoder.IP = rip;
decoder.Decode(out instruction);
if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
@@ -246,8 +404,8 @@ public sealed partial class DirectExecutionBackend
}
var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
var buffer = new byte[byteCount];
if (!TryReadHostBytes(address, buffer))
Span<byte> buffer = stackalloc byte[8];
if (!TryReadHostBytes(address, buffer[..byteCount]))
{
return false;
}
@@ -630,27 +630,6 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
}
}
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(
out var blockReason,
out var blockContinuation,
out var hasBlockContinuation,
out var blockWakeKey,
out var blockWaiter,
out var blockDeadlineTimestamp) &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
{
if (hasBlockContinuation)
{
RegisterBlockedGuestThreadContinuation(
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockWaiter,
blockDeadlineTimestamp);
}
cpuContext[CpuRegister.Rax] = 0uL;
}
if (flag || flag2 || flag3)
{
Console.Error.WriteLine($"[LOADER][TRACE] ImportRet#{num}: nid={importStubEntry.Nid} result={orbisGen2Result} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
@@ -1345,35 +1324,13 @@ public sealed partial class DirectExecutionBackend
}
}
var consumedThreadBlock = GuestThreadExecution.TryConsumeCurrentThreadBlock(
out var blockReason,
out var blockContinuation,
out var hasBlockContinuation,
out var blockWakeKey,
out var blockWaiter,
out var blockDeadlineTimestamp);
if (consumedThreadBlock &&
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
{
if (hasBlockContinuation)
{
RegisterBlockedGuestThreadContinuation(
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockWaiter,
blockDeadlineTimestamp);
}
cpuContext[CpuRegister.Rax] = 0uL;
}
if (probeLeafReturn)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] leaf-return-probe-exit nid={importStubEntry.Nid} " +
$"original=0x{returnRip:X16} final=0x{*(ulong*)(argPackPtr + 96):X16} " +
$"rsp=0x{leafStackPointer:X16} active_slot=0x{ActiveGuestReturnSlotAddress:X16} " +
$"block={consumedThreadBlock} yield={ActiveGuestThreadYieldRequested}");
$"yield={ActiveGuestThreadYieldRequested}");
}
result = cpuContext[CpuRegister.Rax];
@@ -50,6 +50,15 @@ public sealed unsafe partial class DirectExecutionBackend
private const int LinuxUcontextGregsOffset = 40;
private const int LinuxGregsErrOffset = 19 * 8;
// XMM0 starts after the exception/thread states and the floating-point header on Darwin.
// Linux mcontext_t instead stores a pointer to an FXSAVE-compatible _libc_fpstate after
// gregs[23], where XMM0 begins at byte 160.
private const int DarwinMcontextFloatStateOffset = 16 + 21 * 8;
private const int DarwinFloatStateXmmOffset = 168;
private const int LinuxFpregsPointerOffset = 23 * 8;
private const int LinuxFxsaveXmmOffset = 160;
private const int XmmRegisterBlockSize = 16 * 16;
// Byte offsets of the general registers relative to GetPosixRegisterBase,
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
@@ -70,6 +79,8 @@ public sealed unsafe partial class DirectExecutionBackend
[ThreadStatic]
private static int _posixSignalHandlerDepth;
[ThreadStatic]
private static bool _posixVectorContextAvailable;
private void SetupPosixExceptionHandler()
{
@@ -121,8 +132,10 @@ public sealed unsafe partial class DirectExecutionBackend
{
byte* fakeUcontext = stackalloc byte[512];
new Span<byte>(fakeUcontext, 512).Clear();
byte* fakeMcontext = stackalloc byte[512];
new Span<byte>(fakeMcontext, 512).Clear();
// Large enough for the Darwin mcontext64 XMM block the handler now
// round-trips (exception+thread state + float state = 708 bytes).
byte* fakeMcontext = stackalloc byte[768];
new Span<byte>(fakeMcontext, 768).Clear();
if (OperatingSystem.IsMacOS())
{
*(byte**)(fakeUcontext + DarwinUcontextMcontextOffset) = fakeMcontext;
@@ -154,6 +167,33 @@ public sealed unsafe partial class DirectExecutionBackend
record.ExceptionInformation[1] = 0x70000;
_ = TryHandleLazyCommittedPage(&record, 0, 0);
ChainPreviousPosixAction(0, 0, 0);
// Rosetta can deliver the first unsupported SHA instruction before the
// runtime is in a safe state to JIT the recovery path. Decode and execute
// each supported form once now, while signal handlers are not installed.
byte[][] shaWarmupOpcodes =
{
new byte[] { 0x0F, 0x38, 0xC9, 0xC1 }, // sha1msg1 xmm0, xmm1
new byte[] { 0x0F, 0x38, 0xCA, 0xC1 }, // sha1msg2 xmm0, xmm1
new byte[] { 0x0F, 0x38, 0xC8, 0xC1 }, // sha1nexte xmm0, xmm1
new byte[] { 0x0F, 0x3A, 0xCC, 0xC1, 0x00 }, // sha1rnds4 xmm0, xmm1, 0
};
_posixVectorContextAvailable = true;
try
{
foreach (byte[] opcode in shaWarmupOpcodes)
{
fixed (byte* instructionBytes = opcode)
{
WriteCtxU64(contextRecord, CTX_RIP, (ulong)instructionBytes);
_ = TryRecoverIllegalInstruction(contextRecord, (ulong)instructionBytes);
}
}
}
finally
{
_posixVectorContextAvailable = false;
}
}
finally
{
@@ -251,6 +291,15 @@ public sealed unsafe partial class DirectExecutionBackend
{
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
}
// Only bridge the XMM block for SIGILL, the sole signal whose recovery
// may emulate an XMM instruction (Intel SHA). Copying 256 bytes in and
// out of the mcontext on every SIGSEGV (the hot demand-paging path) is
// pure overhead and needless surface for corruption.
byte* xmmRegisters = signal == PosixSigIll ? GetPosixXmmBase(registers) : null;
if (xmmRegisters != null)
{
CopyXmmRegisterBlock(contextRecord + CTX_XMM0, xmmRegisters);
}
EXCEPTION_RECORD record = default;
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
@@ -294,13 +343,21 @@ public sealed unsafe partial class DirectExecutionBackend
// every fault anyway, and recovering here avoids dumping the full
// VectoredHandler diagnostics for each recoverable trap.
int disposition = 0;
if (_posixRawRecoveryEnabled)
_posixVectorContextAvailable = xmmRegisters != null;
try
{
disposition = TryRecoverUnresolvedSentinel(&pointers);
if (_posixRawRecoveryEnabled)
{
disposition = TryRecoverUnresolvedSentinel(&pointers);
}
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
{
disposition = backend.VectoredHandler(&pointers);
}
}
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
finally
{
disposition = backend.VectoredHandler(&pointers);
_posixVectorContextAvailable = false;
}
if (traceSignal)
{
@@ -317,6 +374,10 @@ public sealed unsafe partial class DirectExecutionBackend
{
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
}
if (xmmRegisters != null)
{
CopyXmmRegisterBlock(xmmRegisters, contextRecord + CTX_XMM0);
}
return true;
}
@@ -335,6 +396,25 @@ public sealed unsafe partial class DirectExecutionBackend
return (byte*)ucontext + LinuxUcontextGregsOffset;
}
private static byte* GetPosixXmmBase(byte* registers)
{
if (OperatingSystem.IsMacOS())
{
return registers + DarwinMcontextFloatStateOffset + DarwinFloatStateXmmOffset;
}
byte* fpregs = *(byte**)(registers + LinuxFpregsPointerOffset);
return fpregs == null ? null : fpregs + LinuxFxsaveXmmOffset;
}
private static void CopyXmmRegisterBlock(byte* destination, byte* source)
{
for (var offset = 0; offset < XmmRegisterBlockSize; offset += sizeof(ulong))
{
*(ulong*)(destination + offset) = *(ulong*)(source + offset);
}
}
private static ulong GetPosixFaultAddress(nint siginfo, byte* registers)
{
ulong address = siginfo != 0 ? *(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset) : 0;
File diff suppressed because it is too large Load Diff
+116
View File
@@ -0,0 +1,116 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Support for HLE synchronization primitives that block the guest thread's
/// host thread in place (inside the HLE call, on a host primitive) instead of
/// capturing a continuation and re-scheduling through the cooperative wake-key
/// machinery. In-place blocking makes block-and-wake atomic — the host
/// primitive owns the race — which removes the lost-wakeup window the
/// continuation path had between block registration and wake delivery.
/// </summary>
public static class GuestThreadBlocking
{
/// <summary>
/// Upper bound on a single host wait while a guest thread is parked. Waits
/// are sliced so parked threads observe <see cref="ShutdownRequested"/>
/// promptly at teardown; a wake via Monitor.Pulse still lands immediately.
/// </summary>
public const int WaitSliceMilliseconds = 50;
private static volatile bool _shutdownRequested;
// Guest thread handle -> what it is parked on. Populated only while a
// thread is blocked (the slow path), read by the stall watchdog so
// in-place-blocked threads are not reported as opaque "Running" threads.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, string> _blockDescriptions = new();
/// <summary>True once emulator teardown has begun; parked guest threads unwind.</summary>
public static bool ShutdownRequested => _shutdownRequested;
/// <summary>Called by the execution backend when guest execution is being torn down.</summary>
public static void RequestShutdown() => _shutdownRequested = true;
/// <summary>Records what the given guest thread is about to park on (diagnostics only).</summary>
public static void NoteBlocked(ulong guestThreadHandle, string description)
{
if (guestThreadHandle != 0)
{
_blockDescriptions[guestThreadHandle] = description;
}
}
/// <summary>Clears the parked-state note recorded by <see cref="NoteBlocked"/>.</summary>
public static void NoteUnblocked(ulong guestThreadHandle)
{
if (guestThreadHandle != 0)
{
_blockDescriptions.TryRemove(guestThreadHandle, out _);
}
}
/// <summary>What the thread is parked on, or null if it is not parked in place.</summary>
public static string? DescribeBlock(ulong guestThreadHandle) =>
_blockDescriptions.TryGetValue(guestThreadHandle, out var description) ? description : null;
/// <summary>All currently parked threads (diagnostics; covers the primary thread too).</summary>
public static KeyValuePair<ulong, string>[] SnapshotBlockDescriptions() => _blockDescriptions.ToArray();
// Interrupt delivery for threads parked in place. A thread blocked inside
// an HLE wait keeps its executor busy, so it never reaches the import-return
// safe point where queued guest exceptions (IL2CPP stop-the-world suspend)
// are delivered. When an exception is queued for such a thread, its handle
// is flagged here; each sliced wait loop calls Checkpoint, which — on the
// thread's OWN host thread, with the wait's gate released — runs the
// registered deliverer (the same safe-point delivery used at import
// boundaries), then the loop re-checks its predicate and re-parks. This is
// the SA_RESTART-style "signal on top of a blocking wait" a real kernel
// provides. Dormant unless an exception is actually pending (empty-check
// fast path), so it adds no cost to normal blocking.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, byte> _interrupted = new();
/// <summary>Set by the backend: delivers any exception queued for the current guest thread, in place.</summary>
public static Action? DeliverInterruptForCurrentThread { get; set; }
/// <summary>Flags a parked guest thread to deliver a queued exception at its next wait checkpoint.</summary>
public static void RequestInterrupt(ulong guestThreadHandle)
{
if (guestThreadHandle != 0)
{
_interrupted[guestThreadHandle] = 0;
}
}
/// <summary>
/// Called from every sliced wait loop while it holds <paramref name="gate"/>. If an
/// exception is pending for the current guest thread, releases the gate, delivers it on
/// this host thread, then re-acquires the gate so the loop re-checks its predicate.
/// </summary>
public static void Checkpoint(ulong guestThreadHandle, object gate)
{
if (_interrupted.IsEmpty || guestThreadHandle == 0 || !_interrupted.TryRemove(guestThreadHandle, out _))
{
return;
}
var deliver = DeliverInterruptForCurrentThread;
if (deliver is null)
{
return;
}
// Never run guest code (the handler) while holding an HLE gate — the
// handler may re-enter this same primitive. Release across delivery.
Monitor.Exit(gate);
try
{
deliver();
}
finally
{
Monitor.Enter(gate);
}
}
}
-213
View File
@@ -30,13 +30,6 @@ public readonly record struct GuestThreadSnapshot(
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
/// its return value becomes the guest's RAX for the resumed call.
/// </summary>
public interface IGuestThreadBlockWaiter
{
int Resume();
bool TryWake();
}
public interface IGuestThreadScheduler
{
bool SupportsGuestContextTransfer { get; }
@@ -57,10 +50,6 @@ public interface IGuestThreadScheduler
out ulong returnValue,
out string? error);
void Pump(CpuContext callerContext, string reason);
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
/// <summary>
/// Applies a new guest scheduling priority to a live thread, mapping it
/// onto the host thread if one is running. Returns false when the thread
@@ -152,46 +141,12 @@ public readonly record struct GuestCpuContinuation(
public static class GuestThreadExecution
{
private sealed class DelegateGuestThreadBlockWaiter : IGuestThreadBlockWaiter
{
private readonly Func<int> _resume;
private readonly Func<bool> _tryWake;
public DelegateGuestThreadBlockWaiter(Func<int> resume, Func<bool> tryWake)
{
_resume = resume;
_tryWake = tryWake;
}
public int Resume() => _resume();
public bool TryWake() => _tryWake();
}
[ThreadStatic]
private static ulong _currentGuestThreadHandle;
[ThreadStatic]
private static ulong _currentFiberAddress;
[ThreadStatic]
private static string? _pendingBlockReason;
[ThreadStatic]
private static bool _pendingBlockContinuationValid;
[ThreadStatic]
private static GuestCpuContinuation _pendingBlockContinuation;
[ThreadStatic]
private static string? _pendingBlockWakeKey;
[ThreadStatic]
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
[ThreadStatic]
private static long _pendingBlockDeadlineTimestamp;
[ThreadStatic]
private static bool _pendingEntryExit;
@@ -231,12 +186,6 @@ public static class GuestThreadExecution
{
var previous = _currentGuestThreadHandle;
_currentGuestThreadHandle = threadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
@@ -252,12 +201,6 @@ public static class GuestThreadExecution
public static void RestoreGuestThread(ulong previousThreadHandle)
{
_currentGuestThreadHandle = previousThreadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
@@ -281,123 +224,6 @@ public static class GuestThreadExecution
_currentFiberAddress = previousFiberAddress;
}
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
public static bool RequestCurrentThreadBlock(
CpuContext? context,
string reason,
string? wakeKey = null,
IGuestThreadBlockWaiter? waiter = null,
long blockDeadlineTimestamp = 0)
{
if (!IsGuestThread)
{
return false;
}
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
_pendingBlockWaiter = waiter;
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
{
_pendingBlockContinuation = continuation;
_pendingBlockContinuationValid = true;
}
else
{
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
}
return true;
}
// Compatibility bridge for exports that still describe blocked work as a
// resume/wake delegate pair. New hot paths should provide an
// IGuestThreadBlockWaiter directly to avoid allocating closures.
public static bool RequestCurrentThreadBlock(
CpuContext? context,
string reason,
string? wakeKey,
Func<int> resumeHandler,
Func<bool> wakeHandler,
long blockDeadlineTimestamp = 0) =>
RequestCurrentThreadBlock(
context,
reason,
wakeKey,
new DelegateGuestThreadBlockWaiter(resumeHandler, wakeHandler),
blockDeadlineTimestamp);
public static bool TryConsumeCurrentThreadBlock(out string reason)
{
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out _,
out _,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out IGuestThreadBlockWaiter? waiter)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out waiter,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out IGuestThreadBlockWaiter? waiter,
out long blockDeadlineTimestamp)
{
reason = _pendingBlockReason ?? string.Empty;
if (string.IsNullOrEmpty(reason))
{
continuation = default;
hasContinuation = false;
wakeKey = string.Empty;
waiter = null;
blockDeadlineTimestamp = 0;
return false;
}
continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason;
waiter = _pendingBlockWaiter;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null;
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
_pendingBlockWakeKey = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
return true;
}
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
{
if (timeout <= TimeSpan.Zero)
@@ -417,45 +243,6 @@ public static class GuestThreadExecution
return now + Math.Max(1, ticks);
}
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
{
if (!TryGetCurrentImportCallFrame(out var frame) ||
frame.ReturnRip < 65536 ||
frame.ResumeRsp == 0 ||
frame.ReturnSlotAddress == 0)
{
continuation = default;
return false;
}
continuation = new GuestCpuContinuation(
frame.ReturnRip,
frame.ResumeRsp,
frame.ReturnSlotAddress,
context.Rflags,
context.FsBase,
context.GsBase,
0,
context[CpuRegister.Rcx],
context[CpuRegister.Rdx],
context[CpuRegister.Rbx],
context[CpuRegister.Rbp],
context[CpuRegister.Rsi],
context[CpuRegister.Rdi],
context[CpuRegister.R8],
context[CpuRegister.R9],
context[CpuRegister.R10],
context[CpuRegister.R11],
context[CpuRegister.R12],
context[CpuRegister.R13],
context[CpuRegister.R14],
context[CpuRegister.R15],
context.FpuControlWord,
context.Mxcsr,
RestoreFullFpuState: false);
return true;
}
public static void RequestCurrentEntryExit(string reason, int status)
{
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
+78
View File
@@ -1775,6 +1775,58 @@ public static partial class AgcExports
return ReturnPointer(ctx, commandAddress);
}
// Single-register variant of the SET_SH_REG builders: the register rides
// in rsi as a packed struct (low 16 bits = register offset, high dword =
// byte offset of this dword within a multi-dword register write) and the
// value in edx. Emits the same 3-dword SET_SH_REG packet the plural
// sceAgcCbSetShRegistersDirect path produces per run. Hades calls this
// ~1k times per boot; leaving it unresolved corrupted its DCB stream.
[SysAbiExport(
Nid = "pFLArOT53+w",
ExportName = "sceAgcDcbSetShRegisterDirect",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbSetShRegisterDirect(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
var packedRegister = ctx[CpuRegister.Rsi];
var value = (uint)ctx[CpuRegister.Rdx];
if (commandBufferAddress == 0)
{
return ReturnPointer(ctx, 0);
}
var offset = (uint)(packedRegister & 0xFFFFu) + (uint)((packedRegister >> 32) >> 2);
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 3, out var commandAddress) ||
!TryWriteUInt32(ctx, commandAddress, Pm4(3, ItSetShReg, 0)) ||
!TryWriteUInt32(ctx, commandAddress + 4, offset & 0xFFFFu) ||
!TryWriteUInt32(ctx, commandAddress + 8, value))
{
return ReturnPointer(ctx, 0);
}
TraceAgc($"agc.dcb_set_sh_register_direct buf=0x{commandBufferAddress:X16} reg=0x{offset:X4} value=0x{value:X8}");
return ReturnPointer(ctx, commandAddress);
}
// Size probe for the wait-on-address writer below: same argument prefix
// minus the command buffer, returns the byte size the writer will emit so
// the game can reserve DCB space (7 dwords for a standard WAIT_REG_MEM,
// 6/9 for the 32/64-bit polled-NOP forms).
[SysAbiExport(
Nid = "43WJ08sSugE",
ExportName = "sceAgcDcbWaitOnAddressGetSize",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbWaitOnAddressGetSize(CpuContext ctx)
{
var size = (uint)(ctx[CpuRegister.Rdi] & 0xFF);
var operation = (uint)(ctx[CpuRegister.Rdx] & 0xFF);
var packetDwords = operation is 2 or 3 ? 7u : size == 0 ? 6u : 9u;
ctx[CpuRegister.Rax] = packetDwords * sizeof(uint);
return (int)ctx[CpuRegister.Rax];
}
[SysAbiExport(
Nid = "VmW0Tdpy420",
ExportName = "sceAgcDcbWaitRegMem",
@@ -2624,6 +2676,32 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Hands the game the driver-side context id its GPU event-queue packets
// reference. We key events purely on (equeue, ident, filter), so a single
// stable id satisfies the contract; the game only checks the call
// succeeded and threads the id back through later driver calls.
[SysAbiExport(
Nid = "Zw7uUVPulbw",
ExportName = "sceAgcDriverGetEqContextId",
Target = Generation.Gen5,
LibraryName = "libSceAgcDriver")]
public static int DriverGetEqContextId(CpuContext ctx)
{
var contextIdAddress = ctx[CpuRegister.Rdi];
if (contextIdAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryWriteUInt32(ctx, contextIdAddress, 1))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc($"agc.driver_get_eq_context_id out=0x{contextIdAddress:X16} -> 1");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "DL2RXaXOy88",
ExportName = "sceAgcDriverDeleteEqEvent",
@@ -228,6 +228,17 @@ internal static partial class MetalVideoPresenter
private static void RunWindowLoop()
{
// The Metal Performance HUD attaches to a process at Metal init;
// toggling developerHUDProperties on a live layer is inert when the
// HUD was never armed. SHARPEMU_METAL_HUD=1 arms it the documented way
// (setenv lands in the native environ before the device initializes),
// showing the HUD from launch; Cmd+F1 then toggles it per-layer.
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_METAL_HUD"), "1", StringComparison.Ordinal))
{
Environment.SetEnvironmentVariable("MTL_HUD_ENABLED", "1");
_metalHudVisible = true;
}
MetalNative.EnsureFrameworksLoaded();
_device = MetalNative.MTLCreateSystemDefaultDevice();
@@ -506,6 +517,14 @@ internal static partial class MetalVideoPresenter
Console.Error.WriteLine(
$"[LOADER][INFO] Metal Performance HUD {(_metalHudVisible ? "shown" : "hidden")} (Cmd+F1).");
if (_metalHudVisible &&
!string.Equals(Environment.GetEnvironmentVariable("MTL_HUD_ENABLED"), "1", StringComparison.Ordinal))
{
// The per-layer property only takes effect when the HUD attached at
// Metal init; without that the toggle is a no-op on some macOS builds.
Console.Error.WriteLine(
"[LOADER][INFO] If the HUD does not appear, relaunch with SHARPEMU_METAL_HUD=1 (arms Apple's HUD at startup).");
}
}
private static unsafe nint CreateRenderTimerTarget()
+21
View File
@@ -43,4 +43,25 @@ public static class ImeExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// No hardware keyboard is ever connected; zero the caller's info struct so
// it reads as "not connected" rather than uninitialized stack.
[SysAbiExport(
Nid = "VkqLPArfFdc",
ExportName = "sceImeKeyboardGetInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceIme")]
public static int ImeKeyboardGetInfo(CpuContext ctx)
{
var infoAddress = ctx[CpuRegister.Rsi] != 0 ? ctx[CpuRegister.Rsi] : ctx[CpuRegister.Rdi];
if (infoAddress != 0)
{
Span<byte> info = stackalloc byte[0x40];
info.Clear();
_ = ctx.Memory.TryWrite(infoAddress, info);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -5,14 +5,12 @@ using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
namespace SharpEmu.Libs.Kernel;
public static class KernelEventFlagCompatExports
{
private const int MaxEventFlagNameLength = 31;
private const int HostWaitPumpMilliseconds = 1;
private const uint AttrThreadFifo = 0x01;
private const uint AttrThreadPriority = 0x02;
private const uint AttrSingle = 0x10;
@@ -25,8 +23,7 @@ public static class KernelEventFlagCompatExports
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
private static long _nextEventFlagHandle = 1;
// Cached once: gating every call site avoids building the interpolated
// trace string (and FormatFrameChain/FormatGuestWaitObject) when disabled.
// Cached once: gating every call site avoids building the interpolated trace string when disabled.
private static readonly bool _traceEventFlag = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal);
@@ -128,11 +125,11 @@ public static class KernelEventFlagCompatExports
lock (state.Gate)
{
state.Bits |= pattern;
// Wake threads parked in-place on the gate; each re-checks its pattern.
Monitor.PulseAll(state.Gate);
if (_traceEventFlag) TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -229,134 +226,67 @@ public static class KernelEventFlagCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Monitor.Enter(state.Gate);
try
// A zero-microsecond timeout degrades to an instant poll because the
// deadline is already in the past.
var hostDeadlineMs = timeoutAddress != 0
? Environment.TickCount64 + (timeoutUsec == 0
? 0L
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
: long.MaxValue;
lock (state.Gate)
{
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{
if (_traceEventFlag) TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, immediateWaitResult);
}
// Timed waits block on a deadline instead of returning TIMED_OUT
// immediately; a zero-microsecond timeout still degrades to an
// instant poll because the deadline is already in the past.
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
var hostDeadlineMs = timeoutAddress != 0
? Environment.TickCount64 + (timeoutUsec == 0
? 0L
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
: long.MaxValue;
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId;
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
var satisfied = false;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
() =>
// In-place block on the flag gate. Monitor.Wait releases the gate
// and parks atomically, so a concurrent SetEventFlag's PulseAll
// cannot be lost between the satisfy check and the park. On wake
// the predicate is re-evaluated (Set semantics on FreeBSD-style
// event flags: OR/AND over the bit pattern, optional clear).
state.WaitingThreads++;
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{guestThreadHandle:X16} ret=0x{returnRip:X16}");
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitEventFlag");
try
{
while (true)
{
if (satisfied)
if (GuestThreadBlocking.ShutdownRequested)
{
return (int)blockedWaitResult;
if (timeoutAddress != 0) _ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
// Deadline expiry: report timeout with the current bits.
if (timeoutAddress != 0)
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var wokeResult))
{
if (timeoutAddress != 0) _ = TryWriteUInt32(ctx, timeoutAddress, 0);
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, wokeResult);
}
var remaining = hostDeadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
},
() =>
{
if (!TryPrepareBlockedWait(
ctx,
state,
pattern,
waitMode,
resultAddress,
out var preparedResult))
{
return false;
}
blockedWaitResult = preparedResult;
satisfied = true;
return true;
},
deadline);
if (_traceEventFlag) TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock)
{
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
state.WaitingThreads++;
if (_traceEventFlag) TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
var releaseWaiter = true;
try
{
while (true)
{
Monitor.Exit(state.Gate);
try
{
scheduler.Pump(ctx, "sceKernelWaitEventFlag");
}
finally
{
Monitor.Enter(state.Gate);
}
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var pumpedWaitResult))
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return SetReturn(ctx, pumpedWaitResult);
}
var remaining = hostDeadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(state.Gate, (int)Math.Min(remaining, HostWaitPumpMilliseconds));
}
}
finally
{
if (releaseWaiter)
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
}
GuestThreadBlocking.Checkpoint(guestThreadHandle, state.Gate);
_ = Monitor.Wait(state.Gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
}
}
state.WaitingThreads++;
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
finally
{
Monitor.Exit(state.Gate);
finally
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
}
}
}
@@ -455,41 +385,6 @@ public static class KernelEventFlagCompatExports
return true;
}
private static bool TryPrepareBlockedWait(
CpuContext ctx,
EventFlagState state,
ulong pattern,
uint waitMode,
ulong resultAddress,
out OrbisGen2Result result)
{
lock (state.Gate)
{
result = OrbisGen2Result.ORBIS_GEN2_OK;
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
else
{
ApplyClearMode(state, pattern, waitMode);
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
if (_traceEventFlag) TraceEventFlag(
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
return true;
}
}
private static string GetEventFlagWakeKey(ulong handle) =>
$"event_flag:0x{handle:X16}";
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits);
@@ -519,19 +414,6 @@ public static class KernelEventFlagCompatExports
return true;
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
@@ -584,95 +466,4 @@ public static class KernelEventFlagCompatExports
? frame.ReturnRip
: 0UL;
private static string FormatFrameChain(CpuContext ctx)
{
Span<ulong> returns = stackalloc ulong[4];
var count = 0;
var frame = ctx[CpuRegister.Rbp];
for (var index = 0; index < returns.Length && frame != 0; index++)
{
if (!ctx.TryReadUInt64(frame, out var nextFrame) ||
!ctx.TryReadUInt64(frame + sizeof(ulong), out var returnAddress))
{
break;
}
returns[count++] = returnAddress;
if (nextFrame <= frame)
{
break;
}
frame = nextFrame;
}
return count switch
{
0 => "none",
1 => $"0x{returns[0]:X16}",
2 => $"0x{returns[0]:X16},0x{returns[1]:X16}",
3 => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16}",
_ => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16},0x{returns[3]:X16}",
};
}
private static string FormatGuestWaitObject(CpuContext ctx)
{
var r12 = ctx[CpuRegister.R12];
var r13 = ctx[CpuRegister.R13];
var objectAddress = r12 != 0
? r12
: r13 >= 0xA8
? r13 - 0xA8
: 0;
var builder = new StringBuilder(256);
builder.Append($"r12=0x{r12:X16} r13=0x{r13:X16}");
if (objectAddress == 0)
{
return builder.ToString();
}
builder.Append($" obj=0x{objectAddress:X16}");
AppendUInt32(builder, ctx, objectAddress + 0x58, "o58");
AppendUInt32(builder, ctx, objectAddress + 0x5C, "o5C");
AppendUInt64(builder, ctx, objectAddress + 0x60, "o60");
AppendByte(builder, ctx, objectAddress + 0x6C, "state6C");
AppendByte(builder, ctx, objectAddress + 0x6D, "o6D");
AppendByte(builder, ctx, objectAddress + 0xA0, "waitA0");
AppendByte(builder, ctx, objectAddress + 0xA1, "stateA1");
AppendByte(builder, ctx, objectAddress + 0xA2, "oA2");
AppendUInt64(builder, ctx, objectAddress + 0xA8, "eventA8");
if (r13 != 0)
{
AppendUInt64(builder, ctx, r13, "r13_0");
AppendUInt64(builder, ctx, r13 + 8, "r13_8");
}
return builder.ToString();
}
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadByte(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
}
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadUInt32(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
}
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadUInt64(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
}
}
@@ -93,19 +93,6 @@ public static class KernelEventQueueCompatExports
}
}
private sealed class EqueueWaiter : IGuestThreadBlockWaiter
{
public required CpuContext Ctx { get; init; }
public required ulong Handle { get; init; }
public required ulong EventsAddress { get; init; }
public required int EventCapacity { get; init; }
public required ulong OutCountAddress { get; init; }
public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress);
public bool TryWake() => HasPendingEvents(Handle);
}
[SysAbiExport(
Nid = "D0OdFMjp46I",
ExportName = "sceKernelCreateEqueue",
@@ -149,10 +136,10 @@ public static class KernelEventQueueCompatExports
_eventQueues.Remove(handle);
_pendingEvents.Remove(handle);
_registeredEvents.Remove(handle);
// Wake any thread parked on this queue so it observes the deletion.
Monitor.PulseAll(_eventQueueGate);
}
_wakeKeys.TryRemove(handle, out _);
TraceEventQueue(ctx, "delete", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -412,59 +399,81 @@ public static class KernelEventQueueCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (timeoutAddress == 0 &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEqueue",
GetEventQueueWakeKey(handle),
new EqueueWaiter
{
Ctx = ctx,
Handle = handle,
EventsAddress = eventsAddress,
EventCapacity = eventCapacity,
OutCountAddress = outCountAddress,
}))
// No events ready: block this host thread in place on the queue gate.
// Monitor.Wait releases the gate and parks atomically, so an
// EnqueueEvent/TriggerDisplayEvent PulseAll issued the instant after
// the emptiness check cannot be lost. kqueue/kevent semantics: sleep
// until an event matching a registration is delivered or the timeout
// (usec, infinite when the arg pointer is null) lapses; a zero timeout
// degrades to an instant poll.
long deadline;
if (timeoutAddress == 0)
{
TraceEventQueue(ctx, "wait-block", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
deadline = long.MaxValue;
}
else if (timeoutUsec == 0)
{
deadline = 0;
}
else
{
deadline = Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L);
}
if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw))
TraceEventQueue(ctx, "wait-block", handle);
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitEqueue");
try
{
var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL;
var deadline = Environment.TickCount64 +
Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue));
lock (_eventQueueGate)
{
while (!HasPendingEvents(handle))
while (true)
{
var remaining = deadline - Environment.TickCount64;
if (remaining <= 0)
if ((_pendingEvents.TryGetValue(handle, out var queue) && queue.Count != 0) ||
!_eventQueues.Contains(handle) ||
GuestThreadBlocking.ShutdownRequested)
{
break;
}
Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100));
var remaining = deadline - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
break;
}
var slice = timeoutAddress == 0
? GuestThreadBlocking.WaitSliceMilliseconds
: (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds);
GuestThreadBlocking.Checkpoint(guestThreadHandle, _eventQueueGate);
_ = Monitor.Wait(_eventQueueGate, slice);
}
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (deliveredCount > 0)
{
TraceEventQueue(ctx, "wait-timed-deliver", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (deliveredCount > 0)
{
TraceEventQueue(ctx, "wait-deliver", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (timeoutAddress != 0)
{
TraceEventQueue(ctx, "wait-timeout", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
// Reached only on queue deletion or teardown; the guest sees zero events.
TraceEventQueue(ctx, "wait", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -798,32 +807,6 @@ public static class KernelEventQueueCompatExports
return triggered;
}
private static int ResumeWaitEqueue(
CpuContext ctx,
ulong handle,
ulong eventsAddress,
int eventCapacity,
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return deliveredCount > 0
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
private static bool HasPendingEvents(ulong handle)
{
lock (_eventQueueGate)
{
return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0;
}
}
private static void QueueOrUpdateEvent(
KernelEventDeque queue,
KernelQueuedEvent queuedEvent)
@@ -841,16 +824,16 @@ public static class KernelEventQueueCompatExports
};
}
// Wake keys are formatted once per handle: WakeEventQueue runs on every event
// enqueue (vblank/flip edges included), so formatting there is steady string churn.
private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
private static string GetEventQueueWakeKey(ulong handle) =>
_wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
// Wake threads parked in-place on the queue gate; each re-checks for a
// matching pending event. The handle is unused (all queues share one gate)
// but kept in the signature so call sites read intent-fully.
private static void WakeEventQueue(ulong handle)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle));
_ = handle;
lock (_eventQueueGate)
{
Monitor.PulseAll(_eventQueueGate);
}
}
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
@@ -1819,6 +1819,15 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// POSIX alias; same Orbis result convention as the other posix-named
// file exports in this module (mkdir/rmdir/open).
[SysAbiExport(
Nid = "VAzswvTOCzI",
ExportName = "unlink",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixUnlink(CpuContext ctx) => KernelUnlink(ctx);
[SysAbiExport(
Nid = "AUXVxWeJU-A",
ExportName = "sceKernelUnlink",
@@ -37,55 +37,35 @@ public static class KernelPthreadCompatExports
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
private static long _nextSynchronizationWaiterId;
// Blocking model: waiters block their own host thread in place via
// Monitor.Wait on the state object (mutexes) or SyncRoot (condvars).
// Block-and-wake is therefore atomic — no waiter queues, wake keys, or
// continuation hand-offs, and no lost-wakeup window between a thread
// deciding to block and registering as blocked.
private sealed class PthreadMutexState
{
public ulong OwnerThreadId { get; set; }
public int RecursionCount { get; set; }
public int Type { get; set; } = MutexTypeErrorCheck;
public int Protocol { get; set; }
public LinkedList<PthreadMutexWaiter> Waiters { get; } = new();
}
private sealed class PthreadMutexWaiter
{
public required ulong ThreadId { get; init; }
public required string WakeKey { get; init; }
public required bool Cooperative { get; init; }
public LinkedListNode<PthreadMutexWaiter>? Node { get; set; }
public int Granted;
// Threads currently blocked in PthreadMutexLockCore; destroy reports BUSY while nonzero.
public int WaiterCount { get; set; }
}
private sealed class PthreadCondState
{
public object SyncRoot { get; } = new();
public LinkedList<PthreadCondWaiter> WaiterQueue { get; } = new();
public ulong SignalEpoch { get; set; }
public int Waiters { get; set; }
}
private sealed class PthreadCondWaiter
{
public required ulong ThreadId { get; init; }
public required PthreadMutexState MutexState { get; init; }
public required string WakeKey { get; init; }
public required bool Cooperative { get; init; }
public bool PosixErrors { get; init; }
public LinkedListNode<PthreadCondWaiter>? Node { get; set; }
public PthreadMutexWaiter? MutexWaiter { get; set; }
public Timer? TimeoutTimer { get; set; }
// 0 = waiting, 1 = signaled, 2 = timed out.
public int CompletionState { get; set; }
// Signals produced but not yet consumed by a waiter. A signal only
// increments this when an unserved waiter exists (POSIX: signaling an
// empty condvar is a no-op), so stale signals cannot accumulate.
public int SignalsPending { get; set; }
}
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
static KernelPthreadCompatExports()
{
RunSynchronizationSelfChecks();
}
[SysAbiExport(
Nid = "aI+OeCz8xrQ",
ExportName = "scePthreadSelf",
@@ -139,6 +119,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "9vyP6Z7bqzc",
ExportName = "pthread_rename_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadRenameNp(CpuContext ctx) => PthreadRename(ctx);
[SysAbiExport(
Nid = "GBUY7ywdULE",
ExportName = "scePthreadRename",
@@ -623,7 +610,7 @@ public static class KernelPthreadCompatExports
lock (state)
{
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.Waiters.Count != 0)
if (state.OwnerThreadId != 0 || state.RecursionCount != 0 || state.WaiterCount != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
@@ -653,10 +640,6 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
var canCooperativelyBlock = !tryOnly &&
GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
PthreadMutexWaiter? waiter = null;
lock (state)
{
if (state.OwnerThreadId == currentThreadId)
@@ -696,14 +679,7 @@ public static class KernelPthreadCompatExports
}
}
// pthread_mutex_trylock succeeds whenever the mutex is not currently
// held; unlike the blocking lock it does not queue behind waiters
// (POSIX gives it no fairness obligation). Gating trylock on an empty
// wait queue is wrong and, worse, lets a single stale/undrainable
// waiter wedge a spin-on-trylock loop forever even though the mutex
// is free (owner==0). The blocking lock still honours FIFO so real
// blocked waiters are not starved by a barging locker.
if (state.OwnerThreadId == 0 && (tryOnly || state.Waiters.Count == 0))
if (state.OwnerThreadId == 0)
{
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
@@ -717,24 +693,39 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
waiter = EnqueueMutexWaiterLocked(state, currentThreadId, canCooperativelyBlock);
}
if (canCooperativelyBlock && waiter is not null &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_mutex_lock",
waiter.WakeKey,
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
() => TryGrantBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
{
// Contended: block this host thread in place until the owner
// releases. Monitor.Wait atomically releases the state lock and
// parks, so an unlock's PulseAll cannot be missed. Waits are
// sliced only so teardown can unwind parked threads.
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_mutex_lock");
state.WaiterCount++;
try
{
while (state.OwnerThreadId != 0)
{
if (GuestThreadBlocking.ShutdownRequested)
{
TracePthreadMutex(ctx, "lock-shutdown", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
GuestThreadBlocking.Checkpoint(currentThreadId, state);
_ = Monitor.Wait(state, GuestThreadBlocking.WaitSliceMilliseconds);
}
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
}
finally
{
state.WaiterCount--;
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var hostResult = WaitForHostMutexLock(state, waiter!);
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, hostResult);
return hostResult;
}
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
@@ -751,7 +742,6 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
string? nextWakeKey = null;
lock (state)
{
if (state.RecursionCount <= 0)
@@ -770,18 +760,13 @@ public static class KernelPthreadCompatExports
if (state.RecursionCount == 0)
{
state.OwnerThreadId = 0;
nextWakeKey = state.Waiters.First?.Value.Cooperative == true
? state.Waiters.First.Value.WakeKey
: null;
Monitor.PulseAll(state);
if (state.WaiterCount != 0)
{
Monitor.PulseAll(state);
}
}
}
if (nextWakeKey is not null)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(nextWakeKey, 1);
}
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1198,7 +1183,7 @@ public static class KernelPthreadCompatExports
lock (state.SyncRoot)
{
if (state.WaiterQueue.Count != 0)
if (state.Waiters != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
@@ -1263,94 +1248,80 @@ public static class KernelPthreadCompatExports
}
}
var cooperative = GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _);
var waiter = new PthreadCondWaiter
{
ThreadId = currentThreadId,
MutexState = mutexState,
Cooperative = cooperative,
PosixErrors = posixErrors,
WakeKey = cooperative
? $"pthread_cond_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
: string.Empty,
};
var signaled = false;
lock (state.SyncRoot)
{
waiter.Node = state.WaiterQueue.AddLast(waiter);
state.Waiters++;
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, (int)OrbisGen2Result.ORBIS_GEN2_OK);
// POSIX atomicity: we are registered as a waiter (Waiters++ under
// SyncRoot) before the mutex is released, so a signal issued the
// instant the mutex unlocks already counts us and lands in
// SignalsPending — checked before the first Monitor.Wait. No
// window exists where a wake can be lost.
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
RemoveCondWaiterLocked(state, waiter);
state.Waiters--;
TracePthreadCond("wait-unlock-fail", condAddress, mutexAddress, state, timed, unlockResult);
return unlockResult;
}
if (cooperative && timed)
{
waiter.TimeoutTimer = new Timer(
static callbackState =>
{
var (condState, condWaiter) = ((PthreadCondState, PthreadCondWaiter))callbackState!;
CompleteCondWaiter(condState, condWaiter, timedOut: true);
},
(state, waiter),
GetCondWaitTimeout(timeoutUsec),
Timeout.InfiniteTimeSpan);
}
}
if (cooperative &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
waiter.WakeKey,
() => CompleteBlockedCondWait(ctx, condAddress, mutexAddress, state, waiter),
() => TryGrantCondWaiterMutex(waiter)))
{
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Non-guest callers have no resumable CPU continuation. Park only
// those host-side compatibility callers, preserving the same FIFO
// mutex reacquisition rules as cooperative guest waiters.
lock (state.SyncRoot)
{
var deadline = timed
? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec))
: long.MaxValue;
while (waiter.CompletionState == 0)
GuestThreadBlocking.NoteBlocked(currentThreadId, timed ? "pthread_cond_timedwait" : "pthread_cond_wait");
try
{
if (!timed)
while (state.SignalsPending == 0 && !GuestThreadBlocking.ShutdownRequested)
{
Monitor.Wait(state.SyncRoot);
continue;
}
var remaining = timed
? GetRemainingTimeout(deadline)
: TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
if (timed && remaining <= TimeSpan.Zero)
{
break;
}
var remaining = GetRemainingTimeout(deadline);
if (remaining <= TimeSpan.Zero || !Monitor.Wait(state.SyncRoot, remaining))
{
CompleteCondWaiterLocked(state, waiter, timedOut: true);
break;
if (remaining > TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds))
{
remaining = TimeSpan.FromMilliseconds(GuestThreadBlocking.WaitSliceMilliseconds);
}
GuestThreadBlocking.Checkpoint(currentThreadId, state.SyncRoot);
_ = Monitor.Wait(state.SyncRoot, remaining);
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
if (state.SignalsPending > 0)
{
state.SignalsPending--;
signaled = true;
}
state.Waiters--;
if (state.SignalsPending > state.Waiters)
{
// A timed-out waiter left a signal unconsumed with nobody
// remaining to take it; drop it so a future wait does not
// observe a phantom wake (signals on an empty condvar are
// no-ops on real hardware).
state.SignalsPending = state.Waiters;
}
}
if (waiter.MutexWaiter is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
// POSIX guarantees the mutex is re-acquired on every return path,
// signaled or timed out. Blocks in place like any other locker.
_ = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
_ = WaitForHostMutexLock(mutexState, waiter.MutexWaiter);
var waitResult = waiter.CompletionState == 2
? CondTimedOutResult(waiter)
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
TracePthreadCond(waiter.CompletionState == 2 ? "wait-exit-timeout" : "wait-exit", condAddress, mutexAddress, state, timed, waitResult);
var waitResult = signaled || !timed
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: CondTimedOutResult(posixErrors);
TracePthreadCond(signaled ? "wait-exit" : "wait-exit-timeout", condAddress, mutexAddress, state, timed, waitResult);
return waitResult;
}
@@ -1366,305 +1337,36 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
List<PthreadCondWaiter>? completedWaiters = null;
lock (state.SyncRoot)
{
state.SignalEpoch++;
for (var node = state.WaiterQueue.First; node is not null;)
if (broadcast)
{
var next = node.Next;
var waiter = node.Value;
if (waiter.CompletionState == 0 && CompleteCondWaiterLocked(state, waiter, timedOut: false))
{
(completedWaiters ??= new List<PthreadCondWaiter>()).Add(waiter);
if (!broadcast)
{
break;
}
}
state.SignalsPending = state.Waiters;
}
else if (state.SignalsPending < state.Waiters)
{
// Only count a signal an unserved waiter can consume; signaling
// an empty condvar is a no-op per POSIX.
state.SignalsPending++;
}
node = next;
if (state.Waiters != 0)
{
Monitor.PulseAll(state.SyncRoot);
}
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
if (completedWaiters is not null)
{
foreach (var waiter in completedWaiters)
{
WakeCooperativeWaiter(waiter);
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static PthreadMutexWaiter EnqueueMutexWaiterLocked(
PthreadMutexState state,
ulong threadId,
bool cooperative,
string? wakeKey = null)
{
// A guest thread can have at most one pending acquisition on a mutex —
// it is either running or blocked on exactly one wait. If a waiter for
// this thread is still queued when it comes back for a fresh
// acquisition, that entry is a stale leftover the thread abandoned
// (most often a cond_timedwait timeout whose re-acquire hand-off was
// lost). Stale entries clog the FIFO head with waiters no thread is
// blocked on, so the unlock hand-off wakes a dead wake-key and the
// mutex wedges permanently (observed deadlocking Hades: several
// re-acquire waiters from one thread piled ahead of a live locker).
// Prune any prior entry for this thread before enqueueing the new one.
if (threadId != 0)
{
for (var node = state.Waiters.First; node is not null;)
{
var next = node.Next;
if (node.Value.ThreadId == threadId)
{
state.Waiters.Remove(node);
node.Value.Node = null;
}
node = next;
}
}
var waiter = new PthreadMutexWaiter
{
ThreadId = threadId,
Cooperative = cooperative,
WakeKey = cooperative
? wakeKey ?? $"pthread_mutex_waiter:{Interlocked.Increment(ref _nextSynchronizationWaiterId)}"
: string.Empty,
};
waiter.Node = state.Waiters.AddLast(waiter);
return waiter;
}
[Conditional("DEBUG")]
private static void RunSynchronizationSelfChecks()
{
var mutex = new PthreadMutexState();
PthreadMutexWaiter first;
PthreadMutexWaiter second;
lock (mutex)
{
first = EnqueueMutexWaiterLocked(mutex, 0x101, cooperative: false);
second = EnqueueMutexWaiterLocked(mutex, 0x202, cooperative: false);
Debug.Assert(!TryGrantMutexWaiterLocked(mutex, second), "A mutex waiter bypassed FIFO order.");
Debug.Assert(TryGrantMutexWaiterLocked(mutex, first), "The FIFO mutex head was not granted.");
Debug.Assert(mutex.OwnerThreadId == first.ThreadId && mutex.RecursionCount == 1, "Mutex ownership was not transferred atomically.");
mutex.OwnerThreadId = 0;
mutex.RecursionCount = 0;
Debug.Assert(TryGrantMutexWaiterLocked(mutex, second), "The second mutex waiter was not granted after release.");
}
var cond = new PthreadCondState();
var condMutex = new PthreadMutexState();
var condWaiter = new PthreadCondWaiter
{
ThreadId = 0x303,
MutexState = condMutex,
WakeKey = string.Empty,
Cooperative = false,
};
lock (cond.SyncRoot)
{
condWaiter.Node = cond.WaiterQueue.AddLast(condWaiter);
cond.Waiters++;
Debug.Assert(CompleteCondWaiterLocked(cond, condWaiter, timedOut: false), "A condition waiter was not completed.");
Debug.Assert(cond.WaiterQueue.Count == 0 && cond.Waiters == 0 && condWaiter.MutexWaiter is not null, "Condition completion did not atomically queue mutex reacquisition.");
}
}
private static bool TryGrantMutexWaiterLocked(PthreadMutexState state, PthreadMutexWaiter waiter)
{
if (Volatile.Read(ref waiter.Granted) != 0)
{
return true;
}
if (state.OwnerThreadId != 0 ||
waiter.Node is null ||
!ReferenceEquals(state.Waiters.First, waiter.Node))
{
return false;
}
state.Waiters.Remove(waiter.Node);
waiter.Node = null;
state.OwnerThreadId = waiter.ThreadId;
state.RecursionCount = 1;
Volatile.Write(ref waiter.Granted, 1);
Monitor.PulseAll(state);
return true;
}
private static int WaitForHostMutexLock(PthreadMutexState state, PthreadMutexWaiter waiter)
{
lock (state)
{
while (!TryGrantMutexWaiterLocked(state, waiter))
{
Monitor.Wait(state);
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryGrantBlockedMutexLock(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
PthreadMutexWaiter waiter)
{
var granted = false;
lock (state)
{
granted = TryGrantMutexWaiterLocked(state, waiter);
}
TracePthreadMutex(
ctx,
granted ? "lock-reserve" : "lock-reserve-busy",
mutexAddress,
resolvedAddress,
state,
waiter.ThreadId,
granted ? (int)OrbisGen2Result.ORBIS_GEN2_OK : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return granted;
}
private static int CompleteBlockedMutexLock(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
PthreadMutexWaiter waiter)
{
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
if (Volatile.Read(ref waiter.Granted) == 1)
{
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TracePthreadMutex(ctx, "lock-resume-ungranted", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
private static bool CompleteCondWaiterLocked(
PthreadCondState state,
PthreadCondWaiter waiter,
bool timedOut)
{
if (waiter.CompletionState != 0)
{
return false;
}
waiter.CompletionState = timedOut ? 2 : 1;
RemoveCondWaiterLocked(state, waiter);
waiter.TimeoutTimer?.Dispose();
waiter.TimeoutTimer = null;
lock (waiter.MutexState)
{
waiter.MutexWaiter = EnqueueMutexWaiterLocked(
waiter.MutexState,
waiter.ThreadId,
waiter.Cooperative,
waiter.WakeKey);
}
Monitor.PulseAll(state.SyncRoot);
return true;
}
private static void CompleteCondWaiter(
PthreadCondState state,
PthreadCondWaiter waiter,
bool timedOut)
{
var completed = false;
lock (state.SyncRoot)
{
completed = CompleteCondWaiterLocked(state, waiter, timedOut);
}
if (completed)
{
WakeCooperativeWaiter(waiter);
}
}
private static void RemoveCondWaiterLocked(PthreadCondState state, PthreadCondWaiter waiter)
{
if (waiter.Node is not null)
{
state.WaiterQueue.Remove(waiter.Node);
waiter.Node = null;
state.Waiters = Math.Max(0, state.Waiters - 1);
}
}
private static bool TryGrantCondWaiterMutex(PthreadCondWaiter waiter)
{
var mutexWaiter = waiter.MutexWaiter;
if (waiter.CompletionState == 0 || mutexWaiter is null)
{
return false;
}
lock (waiter.MutexState)
{
return TryGrantMutexWaiterLocked(waiter.MutexState, mutexWaiter);
}
}
private static int CompleteBlockedCondWait(
CpuContext ctx,
ulong condAddress,
ulong mutexAddress,
PthreadCondState state,
PthreadCondWaiter waiter)
{
waiter.TimeoutTimer?.Dispose();
waiter.TimeoutTimer = null;
var result = waiter.MutexWaiter is not null &&
Volatile.Read(ref waiter.MutexWaiter.Granted) == 1
? (waiter.CompletionState == 2
? CondTimedOutResult(waiter)
: (int)OrbisGen2Result.ORBIS_GEN2_OK)
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
TracePthreadCond(
waiter.CompletionState == 2 ? "wait-resume-timeout" : "wait-resume",
condAddress,
mutexAddress,
state,
waiter.CompletionState == 2,
result);
_ = ctx;
return result;
}
private static int CondTimedOutResult(PthreadCondWaiter waiter) =>
waiter.PosixErrors
private static int CondTimedOutResult(bool posixErrors) =>
posixErrors
? 60 // ETIMEDOUT on Orbis/FreeBSD; pthread APIs return errno directly.
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
private static void WakeCooperativeWaiter(PthreadCondWaiter waiter)
{
if (waiter.Cooperative)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(waiter.WakeKey, 1);
}
}
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
{
if (timeoutUsec == 0)
@@ -91,10 +91,6 @@ public static class KernelPthreadExtendedCompatExports
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
}
// On the outer class deliberately: a static on the nested state class gives it a type
// initializer that first runs on a guest thread and fail-fasts the CLR.
private static long _nextRwlockWakeId;
private sealed class PthreadRwlockState
{
public object SyncRoot { get; } = new();
@@ -105,8 +101,6 @@ public static class KernelPthreadExtendedCompatExports
public ulong WriterThreadId { get; set; }
public int WaitingWriters { get; set; }
// See PthreadMutexState.WakeKey.
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
public int GetReaderCount(ulong threadId)
{
@@ -168,17 +162,6 @@ public static class KernelPthreadExtendedCompatExports
}
}
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
{
public required PthreadRwlockState Rwlock { get; init; }
public required ulong ThreadId { get; init; }
public required bool Write { get; init; }
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
}
private readonly record struct TlsKeyState(ulong Destructor);
private readonly record struct PthreadAttrState(
@@ -1184,7 +1167,6 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1479,35 +1461,30 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// In-place block: Monitor.Wait releases SyncRoot and parks
// atomically, so an unlock's PulseAll cannot be lost. Sliced
// only so teardown can unwind parked threads.
rwlock.WaitingWriters++;
var transferredToScheduler = false;
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_wrlock");
try
{
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_wrlock",
rwlock.WakeKey,
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
{
transferredToScheduler = true;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
{
Monitor.Wait(rwlock.SyncRoot);
if (GuestThreadBlocking.ShutdownRequested)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
GuestThreadBlocking.Checkpoint(currentThreadId, rwlock.SyncRoot);
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
}
rwlock.WriterThreadId = currentThreadId;
}
finally
{
if (!transferredToScheduler)
{
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
}
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
}
else
@@ -1517,20 +1494,26 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_rdlock",
rwlock.WakeKey,
new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
GuestThreadBlocking.NoteBlocked(currentThreadId, "pthread_rwlock_rdlock");
try
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
if (GuestThreadBlocking.ShutdownRequested)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
Monitor.Wait(rwlock.SyncRoot);
GuestThreadBlocking.Checkpoint(currentThreadId, rwlock.SyncRoot);
_ = Monitor.Wait(rwlock.SyncRoot, GuestThreadBlocking.WaitSliceMilliseconds);
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(currentThreadId);
}
}
if (rwlock.WriterThreadId != 0 ||
@@ -1547,33 +1530,6 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
{
lock (rwlock.SyncRoot)
{
if (write)
{
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
{
return false;
}
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
rwlock.WriterThreadId = currentThreadId;
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
return true;
}
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
return false;
}
rwlock.AddReader(currentThreadId);
return true;
}
}
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
// writer would share the rwlock with another holder — a data race.
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
@@ -1604,8 +1560,6 @@ public static class KernelPthreadExtendedCompatExports
rwlock.GetReaderCount(currentThreadId) == 0;
}
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
{
PthreadRwlockState? rwlock;
@@ -97,8 +97,6 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelUsleep");
if (micros < 1000)
{
// Guest worker pools use usleep(1) as a polling backoff. Do not turn
@@ -1304,6 +1302,30 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Same (pc, flags, out-info) contract as sceKernelGetModuleInfoForUnwind,
// surfaced through libSceSysmodule on Gen5; the unwinder threads whichever
// one the module's libc was linked against.
[SysAbiExport(
Nid = "4fU5yvOkVG4",
ExportName = "sceSysmoduleGetModuleInfoForUnwind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSysmodule")]
public static int SysmoduleGetModuleInfoForUnwind(CpuContext ctx) => KernelGetModuleInfoForUnwind(ctx);
// libc unwinder predicate: is this PC the kernel signal-return trampoline?
// Guest signal returns do not run through a guest-visible trampoline here,
// so no PC is ever one — report false and let the frame unwind normally.
[SysAbiExport(
Nid = "crb5j7mkk1c",
ExportName = "_is_signal_return",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int IsSignalReturn(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nu4a0-arQis",
ExportName = "sceKernelAioInitializeParam",
@@ -2097,7 +2119,6 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
var totalTicks = tvSec * TimeSpan.TicksPerSecond + Math.Max(tvNsec / 100L, 1L);
try
{
@@ -17,8 +17,6 @@ public static class KernelSemaphoreCompatExports
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
// Formatted once at creation; signal/wait/cancel/delete all wake through this key.
public required string WakeKey { get; init; }
public required int InitialCount { get; init; }
public required int MaxCount { get; init; }
public int Count { get; set; }
@@ -65,7 +63,6 @@ public static class KernelSemaphoreCompatExports
_semaphores[handle] = new KernelSemaphoreState
{
Name = name,
WakeKey = GetSemaphoreWakeKey(handle),
InitialCount = initialCount,
MaxCount = maxCount,
Count = initialCount,
@@ -111,148 +108,79 @@ public static class KernelSemaphoreCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, timeoutUsec);
}
if (_traceSema)
{
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
semaphore.WaitingThreads++;
}
// Block cooperatively: the wake predicate atomically acquires the
// tokens (so a wake commits the acquisition), while the resume
// handler distinguishes a real acquisition from a deadline expiry.
var acquired = false;
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
bool WakePredicate()
{
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
acquired = true;
return true;
}
return false;
}
}
int ResumeWait()
{
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
if (acquired)
{
if (_traceSema)
{
TraceSemaphore($"wait-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
lock (semaphore.Gate)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
}
if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
ResumeWait,
WakePredicate,
deadline))
{
if (_traceSema)
{
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Not a guest thread (or no scheduler): fall back to a host-thread
// wait so the semantics still hold on non-cooperative callers.
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
}
private static int WaitSemaphoreOnHostThread(
CpuContext ctx,
KernelSemaphoreState semaphore,
uint handle,
int needCount,
ulong timeoutAddress,
uint timeoutUsec)
{
// In-place block on the semaphore gate. Monitor.Wait releases the gate
// and parks atomically, so a concurrent SignalSema's PulseAll cannot be
// lost between the count check and the park. Semantics mirror FreeBSD
// ksem / sem_wait: acquire when count>=need, else sleep until posted or
// the deadline lapses. Waits are sliced only so teardown can unwind.
var deadlineMs = timeoutAddress != 0
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
: long.MaxValue;
lock (semaphore.Gate)
{
if (_traceSema)
if (semaphore.Count < needCount)
{
TraceSemaphore(
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
}
while (semaphore.Count < needCount)
{
var remaining = deadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
semaphore.WaitingThreads++;
if (_traceSema)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
}
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelWaitSema");
try
{
while (semaphore.Count < needCount)
{
if (GuestThreadBlocking.ShutdownRequested)
{
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
var remaining = deadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
GuestThreadBlocking.Checkpoint(guestThreadHandle, semaphore.Gate);
_ = Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
}
}
finally
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
}
}
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore(
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
}
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
if (_traceSema)
{
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
private static string GetSemaphoreWakeKey(uint handle) => $"sceKernelWaitSema:{handle:X8}";
[SysAbiExport(
Nid = "12wOHk8ywb0",
ExportName = "sceKernelPollSema",
@@ -315,7 +243,7 @@ public static class KernelSemaphoreCompatExports
}
semaphore.Count += signalCount;
// Wake host-thread waiters parked in the fallback path.
// Wake threads parked in-place on the gate; each re-checks the count.
Monitor.PulseAll(semaphore.Gate);
if (_traceSema)
{
@@ -323,9 +251,6 @@ public static class KernelSemaphoreCompatExports
}
}
// Wake cooperatively-blocked guest threads; their wake predicate
// acquires the tokens atomically, so this respects the new count.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -354,7 +279,6 @@ public static class KernelSemaphoreCompatExports
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.WaitingThreads = 0;
Monitor.PulseAll(semaphore.Gate);
if (_traceSema)
{
@@ -362,7 +286,6 @@ public static class KernelSemaphoreCompatExports
}
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -410,7 +333,6 @@ public static class KernelSemaphoreCompatExports
_semaphores[handle] = new KernelSemaphoreState
{
Name = $"posix@0x{semaphoreAddress:X16}",
WakeKey = GetSemaphoreWakeKey(handle),
InitialCount = initialCount,
MaxCount = int.MaxValue,
Count = initialCount,
@@ -14,44 +14,35 @@ namespace SharpEmu.Libs.Kernel;
// loop; left unimplemented, every wait returns immediately and the runtime
// busy-spins forever (millions of calls, no forward progress).
//
// This implements wait/wake over the existing cooperative-block scheduler,
// keyed on the address. The real primitive takes a compare value so the wait
// only sleeps while the address still holds the expected value; that exact
// value is not recovered here, so each wait is given a bounded deadline and
// treated as a spurious-wakeup-tolerant park: a genuinely missed wake
// self-heals when the deadline expires and the guest re-checks its own
// condition, which futex callers already tolerate. A matching wake releases
// waiters immediately through the same key.
// Waits block in place on a per-address gate (see GuestThreadBlocking):
// Monitor.Wait releases the gate and parks atomically, so a wake's generation
// bump + PulseAll cannot be lost between the generation check and the park.
// The real primitive takes a compare value so the wait only sleeps while the
// address still holds the expected value; that exact value is not recovered
// here, so each wait is bounded by a self-heal deadline and treated as a
// spurious-wakeup-tolerant park: the guest re-checks its own condition after
// resuming, which futex callers already tolerate. Wake-one degrades to
// wake-all for the same reason (each resumed waiter re-evaluates).
public static class KernelSyncOnAddressCompatExports
{
// Safety-net poll interval. Real releases come from the wake side (generation
// bump + WakeBlockedThreads); this only bounds how long a wait that genuinely
// raced/missed its wake stays parked before the guest re-evaluates. Kept
// large: a short interval turns every parked waiter into a hot re-poll that
// steals scheduler bandwidth from the threads that actually make progress
// (including the ones that would issue the wake), so it must be a rare last
// resort, not a spin substitute.
// Safety-net bound. Real releases come from the wake side; this only limits
// how long a wait that genuinely raced/missed its wake stays parked before
// the guest re-evaluates. Kept large: a short bound turns every parked
// waiter into a hot re-poll that steals CPU from the threads that would
// issue the wake, so it must be a rare last resort, not a spin substitute.
private static readonly TimeSpan WaitSelfHealTimeout = TimeSpan.FromMilliseconds(100);
// Per-address host gate for the non-cooperative (host main thread) fallback,
// which cannot use the guest-thread scheduler's block mechanism.
private static readonly ConcurrentDictionary<ulong, object> _hostAddressGates = new();
private static readonly ConcurrentDictionary<ulong, object> _addressGates = new();
// Per-address wake generation. A wait captures the current generation and
// its wake predicate stays unsatisfied (keeps the thread parked) until a
// wake bumps it. This is what actually holds the thread blocked: a bare
// "always satisfied" predicate is treated as an immediate late-arrival by
// the dispatcher's race guard and never yields, leaving the guest to
// busy-spin. The generation also closes the register-vs-park race for free:
// a wake landing in that window bumps the generation, so the predicate is
// already satisfied and the guest correctly resumes at once.
// stays parked while it is unchanged; a wake bumps it first, then pulses
// the gate, so a wait between its generation check and its park still
// observes the bump (the check happens under the gate).
private static readonly ConcurrentDictionary<ulong, long> _wakeGenerations = new();
private static long CurrentGeneration(ulong address) =>
_wakeGenerations.TryGetValue(address, out var generation) ? generation : 0;
private static string WakeKey(ulong address) => $"sceKernelSyncOnAddress:{address:X16}";
[SysAbiExport(
Nid = "Hc4CaR6JBL0",
ExportName = "sceKernelSyncOnAddressWait",
@@ -66,32 +57,33 @@ public static class KernelSyncOnAddressCompatExports
}
var observedGeneration = CurrentGeneration(address);
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(WaitSelfHealTimeout);
// Cooperative path: stay parked until a wake bumps this address's
// generation (or the deadline expires as a self-heal). The guest
// re-evaluates its own condition after resuming.
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelSyncOnAddressWait",
WakeKey(address),
resumeHandler: () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
wakeHandler: () => CurrentGeneration(address) != observedGeneration,
deadline))
var gate = _addressGates.GetOrAdd(address, static _ => new object());
var deadlineMs = Environment.TickCount64 + (long)WaitSelfHealTimeout.TotalMilliseconds;
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
GuestThreadBlocking.NoteBlocked(guestThreadHandle, "sceKernelSyncOnAddressWait");
try
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Non-cooperative caller (host main thread): bounded host wait so a
// missed wake self-heals instead of hanging.
var gate = _hostAddressGates.GetOrAdd(address, static _ => new object());
lock (gate)
{
if (CurrentGeneration(address) == observedGeneration)
lock (gate)
{
Monitor.Wait(gate, WaitSelfHealTimeout);
while (CurrentGeneration(address) == observedGeneration &&
!GuestThreadBlocking.ShutdownRequested)
{
var remaining = deadlineMs - Environment.TickCount64;
if (remaining <= 0)
{
// Self-heal: resume and let the guest re-check its condition.
break;
}
GuestThreadBlocking.Checkpoint(guestThreadHandle, gate);
_ = Monitor.Wait(gate, (int)Math.Min(remaining, GuestThreadBlocking.WaitSliceMilliseconds));
}
}
}
finally
{
GuestThreadBlocking.NoteUnblocked(guestThreadHandle);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -109,18 +101,13 @@ public static class KernelSyncOnAddressCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// rsi carries the number of waiters to release (1 = wake-one, a large
// value = wake-all); default to all if it looks unset.
var requested = unchecked((long)ctx[CpuRegister.Rsi]);
var wakeCount = requested is > 0 and < int.MaxValue ? (int)requested : int.MaxValue;
// Bump the generation first so a wait that has registered but not yet
// parked sees the change and resumes instead of missing this wake.
// Bump the generation first so a wait that has checked but not yet
// parked (it holds the gate for both) observes the change; then pulse
// parked waiters. rsi's wake count degrades to wake-all — resumed
// waiters re-evaluate their own condition, which futex callers tolerate.
_wakeGenerations.AddOrUpdate(address, 1, static (_, current) => current + 1);
GuestThreadExecution.Scheduler?.WakeBlockedThreads(WakeKey(address), wakeCount);
if (_hostAddressGates.TryGetValue(address, out var gate))
if (_addressGates.TryGetValue(address, out var gate))
{
lock (gate)
{
@@ -1,112 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class GuestThreadBlockWaiterRepresentationTests
{
[Fact]
public void SchedulerStoresOnlyTheWaiterObjectRepresentation()
{
var stateType = typeof(DirectExecutionBackend).GetNestedType(
"GuestThreadState",
BindingFlags.NonPublic);
Assert.NotNull(stateType);
var waiterProperty = stateType.GetProperty(
"BlockWaiter",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Assert.NotNull(waiterProperty);
Assert.Equal(typeof(IGuestThreadBlockWaiter), waiterProperty.PropertyType);
Assert.Null(stateType.GetProperty(
"BlockResumeHandler",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
Assert.Null(stateType.GetProperty(
"BlockWakeHandler",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
var registerMethods = typeof(DirectExecutionBackend)
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
.Where(method => method.Name == "RegisterBlockedGuestThreadContinuation")
.ToArray();
var registerMethod = Assert.Single(registerMethods);
Assert.Contains(
registerMethod.GetParameters(),
parameter => parameter.ParameterType == typeof(IGuestThreadBlockWaiter));
Assert.DoesNotContain(
registerMethod.GetParameters(),
parameter => IsFuncParameter(parameter.ParameterType));
var consumeMethods = typeof(GuestThreadExecution)
.GetMethods(BindingFlags.Static | BindingFlags.Public)
.Where(method => method.Name == nameof(GuestThreadExecution.TryConsumeCurrentThreadBlock));
Assert.DoesNotContain(
consumeMethods.SelectMany(method => method.GetParameters()),
parameter => IsFuncParameter(parameter.ParameterType));
}
[Fact]
public void DelegateCompatibilityBridgeIsConsumedAsOneWaiterObject()
{
var previousThread = GuestThreadExecution.EnterGuestThread(0x1234);
try
{
var canWake = false;
var wakeCalls = 0;
var resumeCalls = 0;
Assert.True(GuestThreadExecution.RequestCurrentThreadBlock(
context: null,
reason: "test_wait",
wakeKey: "test_waiter:1",
resumeHandler: () =>
{
resumeCalls++;
return 42;
},
wakeHandler: () =>
{
wakeCalls++;
return canWake;
}));
Assert.True(GuestThreadExecution.TryConsumeCurrentThreadBlock(
out var reason,
out _,
out var hasContinuation,
out var wakeKey,
out IGuestThreadBlockWaiter? waiter,
out var deadline));
Assert.Equal("test_wait", reason);
Assert.Equal("test_waiter:1", wakeKey);
Assert.False(hasContinuation);
Assert.Equal(0, deadline);
Assert.NotNull(waiter);
Assert.False(waiter.TryWake());
canWake = true;
Assert.True(waiter.TryWake());
Assert.Equal(42, waiter.Resume());
Assert.Equal(2, wakeCalls);
Assert.Equal(1, resumeCalls);
}
finally
{
GuestThreadExecution.RestoreGuestThread(previousThread);
}
}
private static bool IsFuncParameter(Type parameterType)
{
var type = parameterType.IsByRef
? parameterType.GetElementType()
: parameterType;
return type is not null &&
type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Func<>);
}
}
@@ -0,0 +1,56 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Emulation;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
public sealed class Sha1InstructionEmulatorTests
{
private static readonly Sha1Vector Destination = new(
0x0123_4567u, 0x89AB_CDEFu, 0x0F1E_2D3Cu, 0x4B5A_6978u);
private static readonly Sha1Vector Source = new(
0xFEDC_BA98u, 0x7654_3210u, 0xF0E1_D2C3u, 0xB4A5_9687u);
[Fact]
public void MessageSchedule1_MatchesIntelLaneSemantics()
{
Assert.Equal(
new Sha1Vector(0xF1C2_97A4u, 0x3D0E_5B68u, 0x0E3D_685Bu, 0xC2F1_A497u),
Sha1InstructionEmulator.MessageSchedule1(Destination, Source));
}
[Fact]
public void MessageSchedule2_MatchesIntelLaneSemantics()
{
Assert.Equal(
new Sha1Vector(0xECA8_6420u, 0xEEEE_EEEEu, 0xF294_3E58u, 0x7777_7777u),
Sha1InstructionEmulator.MessageSchedule2(Destination, Source));
}
[Fact]
public void NextE_MatchesIntelLaneSemantics()
{
Assert.Equal(
new Sha1Vector(0xFEDC_BA98u, 0x7654_3210u, 0xF0E1_D2C3u, 0xC77C_30E5u),
Sha1InstructionEmulator.NextE(Destination, Source));
}
[Theory]
[InlineData(0, 0x20E8_2326u, 0x911F_2CA8u, 0xECE0_593Fu, 0x0C1C_11FBu)]
[InlineData(1, 0x4598_D5B9u, 0x7BA0_0411u, 0x464E_3C51u, 0xF514_1B52u)]
[InlineData(2, 0xEE0E_73F6u, 0x2509_A67Bu, 0x26C6_85CCu, 0x0097_5845u)]
[InlineData(3, 0x9C7B_0B46u, 0xAEC8_EB49u, 0x8FD5_A2B7u, 0xFD49_9AECu)]
public void FourRounds_MatchesAllFourSha1Functions(
byte function,
uint lane0,
uint lane1,
uint lane2,
uint lane3)
{
Assert.Equal(
new Sha1Vector(lane0, lane1, lane2, lane3),
Sha1InstructionEmulator.FourRounds(Destination, Source, function));
}
}