Compare commits

...

46 Commits

Author SHA1 Message Date
Kushida 884584da67 fix: restore WaitSema loop guard boundary (#133) 2026-07-14 15:07:25 +03:00
Spooks d43edc865a Agent/fix gen5 thread agc compat (#130)
* Fix Gen5 thread and AGC compatibility

* Trim compatibility comments

* Report selected Vulkan GPU

* Clean up CPU title label

* Improve emulator frame pacing and performance

* Regenerate package locks with pinned SDK

---------

Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
2026-07-14 14:41:42 +03:00
Berk cf6964710a [emulator] Improve emulator performance by optimizing memory access and reducing unnecessary overhead in kernel and CPU execution paths (#131) 2026-07-14 14:28:44 +03:00
Mike Saito 4f028d0483 Expand Aerolib from nids.csv and wire socket/net kernel NID handlers (#128)
* Expand Aerolib catalog from nids.csv and wire socket/net NID handlers

Load authoritative NID pairs from scripts/nids.csv with ps5_names fallback.
Replace mislabeled kernel zero stubs with socket/connect/bind/getsockname HLE
and sceNet byte-order exports backed by the CSV symbol names.

* Add inet_pton, htons, and bzero kernel compat with CSV NIDs

Wire libc network helpers using authoritative NID names from nids.csv
instead of synthetic Gst* exports used on the crt-loader branch.

* Fix REUSE annotation for scripts/nids.csv

* Drop bundled nids.csv; extend ps5_names and regenerate Aerolib

Remove scripts/nids.csv from the repository and fold csv-only symbol names
into scripts/ps5_names.txt so Aerolib keeps the full catalog via name2nid.
2026-07-14 12:59:59 +03:00
Alex Zorzi 4db98bd8fe [GUI] Add Italian language (#129) 2026-07-14 12:58:10 +03:00
realdody 4600a2ed1f Display game icon (icon0.png) in window title bar (#124) 2026-07-14 12:39:43 +03:00
realdody c5c5ee1f36 [logging] Fetch hardware info, add to video window title and log (#117) 2026-07-14 12:24:05 +03:00
Dmitriy b48b1a5e09 [GUI] Add Russian language (#123) 2026-07-14 12:22:02 +03:00
Berk 511a01e03a [GUI] now languages are embedded in a single assembly (#120) 2026-07-14 02:08:36 +03:00
Berk aaebfe017b [GUI] Added localization support, more clean design, and improved settings management (#119)
* [GUI] Added localization support, more clean design, and improved settings management

* reuse
2026-07-14 01:46:47 +03:00
StealUrKill 9cdc8550ec [pthread] Fix cond-var lock inversion and POSIX timedwait ABI (upstream #113) (#115)
Two follow-ups to upstream #102's condition-variable changes, as analyzed
in upstream issue #113:

- The pending-signal consume path reacquired the guest mutex while still
  holding the condition state lock, inverting lock order against
  cond-signal (mutex -> SyncRoot) and deadlocking both threads. Leave the
  condition lock before relocking, matching the normal wake path.
  This unfroze Dreaming Sarah (PPSA02929) at its title screen.

- pthread_cond_timedwait's third argument is a pointer to an absolute
  CLOCK_REALTIME timespec, not a relative microsecond count; the guest
  address was being truncated into a duration, yielding arbitrary
  timeouts. Read the timespec and convert to a relative wait.
  scePthreadCondTimedwait keeps its separate relative-time ABI.
2026-07-14 00:29:17 +03:00
kostyaff d6fccedab8 [AGC] Support scalar high 32-bit multiplies (#109)
Signed-off-by: kostyaff <filipchukks@gmail.com>
2026-07-14 00:29:05 +03:00
Deeptanshu Lal fed7a6d062 [AGC] Decode gfx10 SOPP hint instructions (#108)
* [AGC] Decode gfx10 SOPP hint instructions

s_clause (0x21), s_waitcnt_depctr (0x23), s_round_mode (0x24) and s_denorm_mode (0x25) were missing from the SOPP decode table, so any shader containing one of these scheduling/mode hints failed to decode entirely with unknown-sopp. No emitter changes are needed: non-branch SOPP instructions are already emitted as no-ops. Opcodes verified against LLVM SOPInstructions.td (SOPP_Real_32_gfx10); decode and end-to-end SPIR-V compilation verified with a synthetic program containing all four hints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* [AGC] Narrow SOPP additions to scheduling hints only

Per review: s_round_mode (0x24) and s_denorm_mode (0x25) write the
shader floating-point MODE state, and the emitter's blanket SOPP no-op
would have silently ignored their simm16 payloads, trading a loud
decode failure for a potential floating-point semantics mismatch. They
are removed and keep failing decode explicitly until their semantics
are modeled or conservatively validated.

s_clause (0x21) and s_waitcnt_depctr (0x23) remain: they are pure
scheduler/dependency hints with no value semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:28:47 +03:00
Spooks 50f78f3713 [VideoOut] Drive vblank from a timer so UE render threads advance (#114) 2026-07-13 21:34:39 +03:00
Spooks 63b440efcd [HLE] Fix guest-thread sync and boot for Unreal Engine titles (#102)
* [HLE] Fix guest-thread sync and boot for Unreal Engine titles

Silent Hill: The Short Message (and other UE titles) now boot the full
engine thread graph instead of hanging early. Four related fixes:

- pthread cond/mutex semantics: retain a signal raised with no waiter as
  pending, and key block/wake on the state's identity rather than a
  resolved address that could differ between lock and unlock. This ends
  the ~1.5M-call cond_wait busy-spin.

- Warm HLE type initializers and force-JIT their methods on a host thread
  at Freeze(). A .cctor or first-time JIT running on a guest thread's
  hijacked stack fail-fasts the CLR as "Invalid Program: attempted to
  call a UnmanagedCallersOnly method from managed code".

- Guest thread scheduling: pump after a wake so a readied thread actually
  runs, add a dispatcher thread for when every guest thread is parked,
  and make the pump-depth guard an atomic CAS.

- Route mutex/rwlock lock/unlock off the non-blocking leaf-import fast
  path so a contended lock can deschedule its guest thread.

Ported from the unreal-boot-fixes branch.

* [HLE] Keep mutex/rwlock unlock on the leaf-import fast path

The previous change routed all mutex/rwlock lock and unlock NIDs off the
leaf fast path so a contended lock could deschedule its guest thread. But
unlock never blocks, and taking it off the fast path made it slow enough
that Demon's Souls' job workers livelocked in a guest spinlock (millions
of mutex_unlock calls, no import progress, main thread stuck in
sceKernelWaitEventFlag).

Only *lock* needs to leave the leaf path. Restore the four unlock NIDs
(mutex + rwlock) so guest spinlocks stay cheap, while lock/rd/wrlock
remain off it for the blocking case Silent Hill needs.

* [HLE] Gate pthread_mutex_lock guest-thread blocking (fixes Demon's Souls)

Re-enabling cooperative deschedule on a contended pthread_mutex_lock
regressed Demon's Souls: its job workers run on libSceFiber, and blocking
a guest thread mid-fiber left sceFiberSwitch returning ESRCH followed by
a null fiber-context deref (0xC0000005). Bisect confirmed the pthread
change as the cause; the game reaches the same point as before it once
the block is skipped.

Gate the block behind SHARPEMU_MUTEX_LOCK_BLOCKING (off by default) so
contended locks fall through to the synchronous host-thread wait. The
rest of the pthread fixes (cond_wait pending signals, identity wake keys)
are unaffected.
2026-07-13 18:59:38 +03:00
Deeptanshu Lal 0565d01744 [AGC] Support VOP3 signed 32-bit multiplies (v_mul_lo_i32, v_mul_hi_i32) (#106)
Two gaps around the VOP3 signed multiplies caused whole-shader SPIR-V
compilation failures:

- v_mul_lo_i32 (0x16B) decoded correctly but had no emission case, so
  any shader containing it failed with "unsupported vector opcode
  VMulLoI32". Its low 32 result bits are identical to the unsigned
  multiply in two's complement, so it now shares the v_mul_lo_u32 IMul
  case.
- v_mul_hi_i32 (0x16C) was missing from the VOP3 decode table entirely
  and decoded as an opaque Vop3Raw16C, which also fails at emission.
  It is now decoded and emitted by sign-extending both operands to
  64 bits, multiplying, and taking the upper 32 bits of the product,
  mirroring the existing v_mul_hi_u32 pattern.

Opcode numbers verified against LLVM's AMDGPU backend
(VOP3Instructions.td): V_MUL_LO_U32 gfx10 = 0x169, V_MUL_HI_U32 =
0x16a, V_MUL_LO_I32 = 0x16b, V_MUL_HI_I32 = 0x16c. Behavior verified
by decoding and fully compiling a synthetic program containing all
four multiplies: previously the 0x16C word decoded as Vop3Raw16C and
compilation failed at the v_mul_lo_i32 instruction; now all four
decode by name and the program compiles to SPIR-V.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:53:19 +03:00
j92580498-max 56bf00e9a5 [RTC] Port core rtc helpers from PS5 3.20 libs (#105)
Co-authored-by: zocomputer <help@zocomputer.com>
2026-07-13 18:51:48 +03:00
Digote 46b729c5b4 [CPU] Preserve guest return value across TLS lookup (#104)
Co-authored-by: diego <diego@DIGOTE-PC>
2026-07-13 17:22:53 +03:00
Deeptanshu Lal 4b7df8623a [AGC] Fix v_fmac_f32 family decoding in Gen5 VOP2 table (#103)
VOP2 opcode 0x2B was mapped to v_ldexp_f32, which is its gfx6/gfx7
assignment. On gfx10-class hardware 0x2B is v_fmac_f32, so any shader
using it silently computed ldexp(a, b) instead of dst += a * b.
v_ldexp_f32 on gfx10 only exists as VOP3 0x362, which the VOP3 table
already maps correctly.

Also add the remaining members of the fmac family:
- v_fmamk_f32 (0x2C) and v_fmaak_f32 (0x2D), including their mandatory
  literal dword in instruction sizing and operand construction, reusing
  the existing v_madmk/v_madak handling.
- The VOP3-encoded form of v_fmac_f32 (0x12B), emitted when source
  modifiers are present.

SPIR-V emission reuses the existing v_mac_f32 body (fma with the
destination register as addend) and the v_mad/v_fma case group.

Opcode assignments verified against LLVM's AMDGPU backend
(VOP2Instructions.td): V_FMAC_F32 gfx10 = 0x02b, V_FMAMK_F32 = 0x02c,
V_FMAAK_F32 = 0x02d; V_LDEXP_F32 is 0x02b only on gfx6/gfx7 and is
VOP3-only 0x362 on gfx10. Decode verified by feeding hand-assembled
gfx1013 words through Gen5ShaderTranslator: 0x560A0501 previously
decoded as VLdexpF32 and a v_fmamk_f32 program failed with
unknown-vop2 op=0x2C; both now decode correctly, and VOP3 0x362 still
decodes as VLdexpF32.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 17:20:12 +03:00
Digote 3c2134474d [HLE] Avoid duplicate export registration (#100)
Co-authored-by: diego <diego@DIGOTE-PC>
2026-07-13 17:18:21 +03:00
Spooks 298ef01809 [VideoOut] Prefer NVIDIA/discrete GPUs over integrated (#97)
SelectPhysicalDevice took the first device exposing a graphics+present
queue. On hybrid-graphics laptops that is the integrated GPU, so the
discrete card went unused - and AMD's integrated driver access-violates
inside vkCreateGraphicsPipelines while compiling some translated guest
shaders, killing the process. The CLR surfaced that native AV as
"Invalid Program: attempted to call a UnmanagedCallersOnly method from
managed code", which made it look like a CPU/threading fault.

Score the candidates instead: NVIDIA parts win, other discrete GPUs come
next, and an integrated GPU is only chosen when nothing else can present.
SHARPEMU_VK_DEVICE=<substring> pins a specific adapter, and the selected
device is logged.
2026-07-13 15:05:59 +03:00
Berk 2060dacaf1 [emulator] Fix mitigated child process handling & log improvements (#96)
* [emulator] Fix mitigated child process handling

* reuse

* [log] log to file for CLI and fix some issues
2026-07-13 14:34:26 +03:00
ParantezTech f73c9e8c3f added contributing guidelines 2026-07-13 12:48:36 +03:00
Mike Saito 3d2c30b151 fix(core): lazy dlsym stub materialization, COW snapshots and deferred bootstrap logging (#94)
* fix(core): implement 4-tier lazy dlsym stub materialization and argument normalization

Enforce transactional and thread-safe resolution for standalone ELF bootstrapper pipelines. - Implement a 4-tier additive fallback cascade (T0: runtime symbols, T1: import entries scan, T2: Aerolib mapping, T3: runtime slack-pool lazy stub allocation at 0x7000_0000_0000). - Fix UnmanagedCallersOnly CLR runtime crashes on second bootstrap by adding NormalizeKernelDynlibDlsymArguments to detect and swap mirrored (symbol, handle) register inputs via rigorous pointer bounds verification. - Protect failure paths via CompleteKernelDynlibDlsymFailure, cleanly zero-filling target outputAddress buffers and returns Rax = -1 with zero managed logging execution in hot native paths.

* fix(core): harden lazy-stub diagnostics with COW snapshots and deferred bootstrap logging

Follow-up to lazy stub pool copy-on-write publishing in TryGetOrCreateLazyImportStub. - Snapshot _importEntries in ProbeReturnRip before near-call and PLT import lookup loops to prevent torn iteration during concurrent array replacement. - Defer SHARPEMU_LOG_BOOTSTRAP output: hot path records raw register slots in a ring buffer under lock; TryReadAsciiZ and Console.Error run only after import handler completion via DrainDeferredBootstrapTraces. - Normalize bootstrap dynlib register order at DispatchImport gateway entry before any logging or trace reads, so swapped RDI/RSI on Import#2 cannot fault the native gateway when bootstrap tracing is enabled. - Resolve lazy stub pool bounds from the full SelfLoader-mapped import region via VirtualQuery instead of a hardcoded 4 KiB cap. - Use ConcurrentDictionary for runtime symbol registration during concurrent dlsym. - Emit distinct [LOADER][WARN] reasons when import stub region resolution fails versus lazy stub pool exhaustion.
2026-07-13 12:25:37 +03:00
Berk 61d28e9e08 [CPU] optimize strcasecmp for hot path (#95) 2026-07-13 12:19:57 +03:00
Digote 4bd42795c7 [logging] Migrate HLE diagnostics to SharpEmuLog (#80)
Signed-off-by: Digote <45742711+Digote@users.noreply.github.com>
2026-07-13 12:11:27 +03:00
Mike Saito c03ca32a02 fix(vfs): harden getdirentries and APR filepath resolution (#77)
* fix(vfs): defer host cursor commit in getdirentries

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

Deferred Commit in KernelGetdirentriesCore:

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

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

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

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

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

Files: KernelMemoryCompatExports.cs

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

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

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

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

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

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

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

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

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

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

Co-authored-by: j92580498-max <252151737+j92580498-max@users.noreply.github.com>
2026-07-12 18:24:11 +03:00
Foued Attar cdee77521e Fix UnmanagedCallersOnly boot crash & Core Engine Improvements (CPU, HLE, AGC) (#81)
* [agc] WAIT_REG_MEM suspend/resume, draw packet fixes, new HLE exports, debug cleanup

Rebased onto upstream 79a7437 (par274/sharpemu, rewritten history).

- GpuWaitRegistry: DCBs suspended on unsatisfied WAIT_REG_MEM are re-polled
  against guest memory on every submit; fixed 64-bit and standard packet parse
  offsets, apply the mask, treat PM4 compare function 0 as "always".
- TryReadSubmittedDrawCount: accept the 5-dword ItDrawIndex2 form emitted by
  DcbDrawIndex (count at +4); menu draws were silently discarded before.
- sceAgcDriverSubmitMultiDcbs: reversed ABI (rdi=address array, rsi=dword
  sizes, rdx=count).
- VideoOut: vblank events, sceVideoOutGetFlipStatus, buffers registered via
  sceVideoOutRegisterBuffers are valid flip targets.
- New HLE: libc stdio (fopen/fread/fseek/ftell/fclose/fgets), Dinkumware
  _Getpctype ctype table, NpTrophy2 stubs, AMPR PAK sequential-read tracker,
  MsgDialog lifecycle, NGS2 alt NIDs + dummy vtable for handle objects,
  guarded memset intrinsic, abort()/strcasecmp null-arg recovery.
- Removed investigation-only code (INT3 breakpoints, qfont/mcpp dumps,
  error-candidate printf traces, unconditional debug logs).

First rendered frame: Quake (PPSA01880) presents a 1920x1080 guest frame.

* Implemented a guarded native intrinsic (rep movsb) in DirectExecutionBackend to bypass HLE dispatch overhead, while preserving memory safety checks.

* [hle] clock_gettime clock ids, AudioOut2 canary fix, NID rebinds, new offline stubs

- clock_gettime (lLMT9vJAck0): support CLOCK_SECOND and the *_PRECISE/*_FAST
  variants instead of returning EINVAL, which games treated as fatal and
  retried in a tight loop.
- AudioOut2: context param writes shrunk to the guest-observed layout (the
  old 0x80-byte reset smashed the stack canary at +0x60 and killed audio
  init); ContextQueryMemory writes the single u64 the caller expects.
- NGS2: dropped wrong alt-NID aliases (they hash to sceImeUpdate,
  sceMouseRead, sceSystemGestureUpdateAllTouchRecognizer - now bound in
  their real libraries); added sceNgs2PanInit; fixed VoiceGetState NIDs.
- New verified stubs: sceUltInitialize, sceNpUniversalDataSystemDestroyHandle,
  sceNpGetOnlineId, sceNpGetNpReachabilityState, sceImeKeyboardOpen,
  sceImeKeyboardGetResourceId, sceMouseOpen, sceKernelAprGetFileSize.
- Import gateway: unwind guest workers at dispatch during backend teardown;
  env-gated SHARPEMU_LOG_THREAD_MODE tracing.

* [cpu] Isolate guest execution on native worker threads

Guest entry stubs no longer run above CLR-managed frames: each run is handed
to a pooled raw OS thread whose loop is emitted native code. While guest code
executes there is not a single managed frame on the thread and it stays in
preemptive GC mode, so the GC never walks a frame chain interleaved with
guest stubs that carry no CLR unwind info (the ReversePInvokeBadTransition /
UnmanagedCallersOnly FailFast class of crashes on pumped guest threads).

- NativeGuestExecutor: CreateThread + emitted run loop (WaitForSingleObject,
  UnmanagedCallersOnly prologue/epilogue, entry stub call, SetEvent). The
  prologue rebinds guest TLS, the host-RSP slot, thread affinity and the
  Active* ambient per run, so workers carry no guest identity and pool
  freely; the orchestrating managed thread parks in a preemptive wait.
- All three entry sites route through RunGuestEntryStub: guest thread
  entries, blocked-continuation resumes, and the main ExecuteEntry.
- Teardown stops workers before any executable stub or TLS index they
  reference is freed; a worker that will not stop leaks its loop instead of
  freeing running code.
- Kill switch: SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS=1 restores the inline
  calli path.
2026-07-12 18:04:12 +03:00
RedBlackAka d2fca37716 [GUI] Basic hotkey and full-screen support (#75) 2026-07-12 12:54:37 +03:00
GamingChelsea 5bee81df70 [GUI] Console Search & Log Path Selector (#74)
* Add console search filter and log file path selector

* Add console search filter and log file path selector

* Increase max width of ConsoleSearchBox
2026-07-12 02:31:20 +03:00
Brando d4bb282c49 GUI: Discord Rich Presence (#73)
Publish launcher status to Discord over its local IPC named pipe with
no external dependency: framed-JSON handshake and SET_ACTIVITY, a
background worker with reconnection and latest-state dedup, and silent
no-op behavior when Discord is not running.

- "Browsing the library" (game count, session elapsed) while idle;
  "Playing <game>" with title id and elapsed time during emulation.
  Activities always carry timestamps: Discord accepts but does not
  render activities without them
- Presence flips back to browsing immediately on Stop instead of
  waiting for process exit: a game wedged in a GPU driver call can
  outlive termination for a while. Stop also terminates the job
  object so the whole child tree dies even in that state
- Toggle in the Options panel (persisted); client id configurable in
  gui-settings.json; SHARPEMU_LOG_DISCORD=1 traces queue/send/failure
2026-07-12 00:54:42 +03:00
Foued Attar e1cf5b13ef [AGC] Quake rendering progress: WAIT_REG_MEM, draw fixes, VideoOut, and HLE improvements (#68)
* [agc] WAIT_REG_MEM suspend/resume, draw packet fixes, new HLE exports, debug cleanup

Rebased onto upstream 79a7437 (par274/sharpemu, rewritten history).

- GpuWaitRegistry: DCBs suspended on unsatisfied WAIT_REG_MEM are re-polled
  against guest memory on every submit; fixed 64-bit and standard packet parse
  offsets, apply the mask, treat PM4 compare function 0 as "always".
- TryReadSubmittedDrawCount: accept the 5-dword ItDrawIndex2 form emitted by
  DcbDrawIndex (count at +4); menu draws were silently discarded before.
- sceAgcDriverSubmitMultiDcbs: reversed ABI (rdi=address array, rsi=dword
  sizes, rdx=count).
- VideoOut: vblank events, sceVideoOutGetFlipStatus, buffers registered via
  sceVideoOutRegisterBuffers are valid flip targets.
- New HLE: libc stdio (fopen/fread/fseek/ftell/fclose/fgets), Dinkumware
  _Getpctype ctype table, NpTrophy2 stubs, AMPR PAK sequential-read tracker,
  MsgDialog lifecycle, NGS2 alt NIDs + dummy vtable for handle objects,
  guarded memset intrinsic, abort()/strcasecmp null-arg recovery.
- Removed investigation-only code (INT3 breakpoints, qfont/mcpp dumps,
  error-candidate printf traces, unconditional debug logs).

First rendered frame: Quake (PPSA01880) presents a 1920x1080 guest frame.

* Implemented a guarded native intrinsic (rep movsb) in DirectExecutionBackend to bypass HLE dispatch overhead, while preserving memory safety checks.
2026-07-12 00:22:48 +03:00
j92580498-max de4fc1e1a8 Add sceKernelNanosleep to libKernel (#72)
Implements the sceKernelNanosleep export (NID QvsZxomvUHs) for both Gen4
and Gen5 targets. Reads the requested timespec from guest memory,
validates the pointer and tv_nsec range, sleeps for the requested
duration, and zeroes the optional remaining-time struct on completion.

Also fixes: reading rqtp as a guest pointer to a timespec (tv_sec/tv_nsec
int64 pair) instead of raw register values, and keeps the optimized
sceKernelUsleep short-sleep path untouched.

Co-authored-by: par274 <par274@users.noreply.github.com>
2026-07-11 23:42:26 +03:00
Mike Saito 5e76554514 core: unify clock dispatch logic, add precise clocks, and enforce coalesced time writes (#71)
Comprehensive refactoring of the system time subsystem to unify clock dispatching, support precise clock extensions, and secure memory boundaries against partial state corruption.

Centralized Clock Dispatch Engine:
- Extracted shared elapsed-tick calculation and clock-routing math into a unified internal static bool ResolveClockTime() dispatch engine under KernelRuntimeCompatExports.cs.
- Moved all clock identifiers from KernelMemoryCompatExports to KernelRuntimeCompatExports as internal const int constants to eliminate cross-file duplication while preserving raw compiler switch-case layout optimizations.
- Added native alias mapping support for CLOCK_REALTIME_PRECISE (9) and CLOCK_MONOTONIC_PRECISE (11).
- Hardened the Orbis sceKernelClockGettime path by routing it through the new dispatcher, resolving a pre-existing logic flaw where any non-zero clock_id incorrectly fell back to monotonic time. Invalid IDs now properly fail with ORBIS_GEN2_ERROR_INVALID_ARGUMENT.

Coalesced Single-Transaction Memory Writes:
- Replaced consecutive isolated 8-byte scalar writes across POSIX clock_gettime, gettimeofday, and Orbis sceKernelClockGettime/sceKernelGettimeofday with safe single-transaction 16-byte stackalloc byte buffer writes via BinaryPrimitives and ctx.Memory.TryWrite. This entirely prevents partial memory state corruption on virtual page boundaries.
- Implemented a single 8-byte coalesced zero-fill transaction for the deprecated/legacy timezone buffer (timezoneAddress != 0), aligning it with standard FreeBSD stub behavior.
- Standardized POSIX failure path routines. Write faults cleanly issue TrySetErrno(ctx, Efault) while safely omitting explicit manual Rax writes, letting the import dispatcher natively sign-extend the return -1 value to 0xFFFFFFFFFFFFFFFF.

Zero-Alloc Host RDTSC Execution Stub:
- Patched CreateRdtscReader() to stream native architecture opcodes out of stack-allocated spans directly into host executable memory zones (VirtualAlloc) via unsafe { Buffer.MemoryCopy(...) }, completely removing the high-frequency .ToArray() runtime allocation overhead on the hot path.

Files: KernelRuntimeCompatExports.cs, KernelMemoryCompatExports.cs
2026-07-11 23:39:44 +03:00
Mike Saito 3a24db567f core: implement coalesced writes for gettimeofday and set POSIX EFAULT (#70)
Follow-up task to enforce coalesced guest memory writes within the gettimeofday subsystem, removing remaining partial-write risks on virtual memory page boundaries.

* sceKernelGettimeofday Hardening: Replaced consecutive isolated 8-byte scalar writes with a single 16-byte coalesced transaction buffer using stackalloc byte[16] and BinaryPrimitives. It preserves native Orbis semantics by returning ORBIS_GEN2_ERROR_MEMORY_FAULT on failure states without side-effect partial-writes.
* POSIX gettimeofday Compliance:
  - Applied the identical single-transaction 16-byte write pattern for the timeval structure.
  - Implemented a single 8-byte coalesced zero-fill transaction for the deprecated/legacy timezone buffer (timezoneAddress != 0) using BinaryPrimitives.WriteInt32LittleEndian, aligning it with standard FreeBSD stub behavior.
  - Integrated proper TrySetErrno(ctx, Efault) tracking upon write failures. The method safely omits explicit manual Rax writes on error paths, allowing the import dispatcher to cleanly sign-extend the return -1 value to 0xFFFFFFFFFFFFFFFF.

Out of scope: Subsystem clock and timeval validation is now fully complete; no further temporal partial-write vulnerabilities remain within the core runtime memory compat layers.

Files: KernelRuntimeCompatExports.cs
2026-07-11 23:05:36 +03:00
kostyaff a9b06974be [docs] Improve game compatibility report form (#69)
Signed-off-by: kostyaff <filipchukks@gmail.com>
2026-07-11 22:51:10 +03:00
Mike Saito 19added142 core: implement sceKernelGetCompiledSdkVersion based on target generation (#66)
Replaced the no-op stub for sceKernelGetCompiledSdkVersion with a proper runtime compliance implementation.

Runtime Validation: Added explicit NULL pointer verification for the destination buffer address (versionAddress == 0). It returns ORBIS_GEN2_ERROR_INVALID_ARGUMENT and sign-extends the target Rax register to 0xFFFFFFFF80020003, strictly mirroring the PthreadJoin error-handling pattern of this subsystem.
Target-Based SDK Fallback: Implemented deterministic fallback version routing based on ctx.TargetGeneration (0x05000000 for Gen4 and 0x09000000 for Gen5 standard Orbis layout). This ensures guest applications pass early firmware checks until native metadata extraction is implemented.
Atomic Memory Write: Secured the state write sequence via the native ctx.TryWriteUInt32 layer, correctly catching virtual memory page faults, propagating ORBIS_GEN2_ERROR_MEMORY_FAULT to Rax, and safely bypassing partial-write state corruption.
Out of scope (follow-up): Native parsing of the compiled SDK version flags directly out of the guest ELF note/metadata sections.
2026-07-11 22:18:26 +03:00
kostyaff 8a40251a1c [logging] Migrate SelfLoader and DirectExecutionBackend.Diagnostics to SharpEmuLog (#65)
Phase 2 of Console.Error.WriteLine → structured logging migration.

SelfLoader.cs (30 sites):
- Category: "Loader"
- TLS load_start/load_done, Segment info, DTPMOD64 patching → Debug
- ELF alignment mismatch, invalid symbol value skips, CRITICAL invalid patch → Warning
- Runtime symbol index populated, Initializers discovered → Info
- [FOCUS][SCAN/SKIP] relocation trace, [RELOC] target trace → Debug
- TryLoadTableBytes diagnostics → Debug (FAILED → Warning)
- ResolveMappedAddressOrFallback trace → Debug

DirectExecutionBackend.Diagnostics.cs (17 sites):
- Category: "Native" (Log field in main DirectExecutionBackend.cs partial)
- DumpRecentImportTrace → Info
- Suspicious unresolved pointer hits/cap → Warning
- ProbeReturnRip return-rip bytes/slots/PLT trace → Debug

Level mapping: [TRACE]/[FOCUS]/[RELOC]/[TEST] → Debug; [INFO] → Info;
[WARNING]/WARNING/CRITICAL/Skipping/FAILED → Warning.

[LOADER] prefix dropped — category is in LogEntry.

Console.WriteLine (stdout, ~25 sites in SelfLoader.cs) intentionally
left untouched — only Console.Error.WriteLine was in scope.

Build: 0 errors, 0 warnings.

Co-authored-by: Hermes Atlas <hermesatlas@example.com>
2026-07-11 22:17:40 +03:00
PandaCatz edb4eb86a2 [kernel] Wake blocked waiters on semaphore signal, cancel, and delete (#67)
sceKernelWaitSema parks a guest thread on the scheduler when the count is not
yet available, but sceKernelSignalSema only incremented the count and returned:
there was no WakeBlockedThreads call anywhere in the file, so a thread blocked
in WaitSema was never woken and the game hung there. sceKernelCancelSema and
sceKernelDeleteSema left parked waiters stranded the same way.

Give each semaphore a per-handle wake key and each waiter a small record with
the count it needs and a result slot. Signal, cancel, and delete wake the
waiters through the scheduler after releasing the semaphore lock, matching the
lock order the event flag and event queue paths already use. The wake handler
runs under the scheduler gate and consumes the count under the semaphore lock,
so a waiter needing more than is available stays parked while a smaller waiter
can still proceed; the resume handler hands the recorded result back as the
guest's return value.

Cancel bumps an epoch and delete sets a flag so woken waiters return what the
kernel returns in those cases: ECANCELED (0x80020055) for a canceled wait and
the EACCES-class 0x8002000D for a deleted semaphore. Delete succeeds even with
waiters present. Only the woken waiter's own handler adjusts the waiting-thread
count, so a waiter that parks during a cancel is not double-counted, and the
create path now wakes a waiter that raced onto the handle if the handle
write-back fails instead of stranding it.

This does not change the immediate paths: an available count is still consumed
inline, and a wait with a timeout pointer still returns immediately (honoring
the timeout through the scheduler is a separate change).

Verified with a block/wake harness that drives real guest threads through the
real import trampolines: signal-after-block, signal racing the park,
multi-waiter signal, need-count gating with a smaller waiter slipping past, and
cancel and delete with parked waiters including the reported waiter count, plus
event flag and event queue regression checks. Builds clean on Windows and
Linux.
2026-07-11 22:17:24 +03:00
89 changed files with 17317 additions and 1208 deletions
+18 -3
View File
@@ -16,6 +16,11 @@ body:
Example title:
Demons Souls (PPSA01341)
**Before reporting:**
- Test the game on a **fresh build from `main`**, not an old release.
- Copy the **exact commit SHA** shown at the top of the log.
- `DEBUG` and `TRACE` lines are diagnostic context, not errors by themselves; report the last milestone or the first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop.
- type: input
id: game_name
attributes:
@@ -73,11 +78,20 @@ body:
id: logs
attributes:
label: Log File
description: Drag and drop your log file here, or click to browse
description: Attach the **full** log file (`.log`, `.txt`, or `.zip`). Do not paste only a fragment — the maintainer needs the complete log from launch to crash.
validations:
required: false
required: true
accept: ".log,.txt,.zip"
- type: input
id: last_milestone_first_error
attributes:
label: Last Milestone / First Error
description: Paste the **last useful milestone line** or the **first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop** from the log. `DEBUG`/`TRACE` lines do not count unless a real error follows them.
placeholder: "e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ..."
validations:
required: true
- type: dropdown
id: operating_system
attributes:
@@ -119,7 +133,8 @@ body:
id: emulator_version
attributes:
label: Emulator Version / Commit
placeholder: e.g. v0.0.1 / a1b2c3d
description: Copy the **exact short SHA** from the top of the log. Do not enter just `0.0.1` without a commit hash.
placeholder: e.g. a1b2c3d
validations:
required: true
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+1
View File
@@ -39,3 +39,4 @@ Desktop.ini
ehthumbs.db
.vs/
.idea/
+70
View File
@@ -0,0 +1,70 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Contributing
Contributions are always welcome!
Before opening a pull request, please keep the following in mind:
- Keep PRs small and focused on a single topic.
- Discuss large architectural changes before implementing them.
- Follow the project's existing coding style.
- Do not submit generated code unless you fully understand and can maintain it.
- Do not introduce Sony proprietary code, firmware, keys, decrypted assets, or other copyrighted PlayStation materials.
- All reverse engineering should be based on publicly available information, clean-room techniques, or your own original research.
- Game-specific hacks should be avoided whenever possible. Prefer generic implementations that improve overall compatibility.
- New features should not break existing behavior.
- Ensure the project builds successfully before submitting a PR.
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
## AI-Assisted Contributions
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.
However, contributors are expected to fully understand every line of code they submit. By opening a pull request, you confirm that you are able to explain, modify, debug, and maintain the submitted code without relying on the AI that generated it.
When submitting an AI-assisted PR:
- Clearly explain **what the change does**, **why it is needed**, and **what problem it solves**, using your own words.
- Describe **how you verified the change**, including the games, applications, or test cases used.
- Avoid excessive product-level logging. Use logging only when it provides meaningful diagnostic value.
- Comments should document design decisions or implementation details in your own words. Avoid generic AI-generated comments that merely restate what the code already does.
- Be prepared to answer review questions about the implementation. "The AI generated it" is not considered a sufficient explanation.
- Large AI-generated changes without a clear understanding of the implementation are unlikely to be accepted.
- If the implementation cannot be reasonably explained during code review, the pull request may be rejected regardless of whether it works.
The quality, correctness, maintainability, and long-term ownership of the submitted code remain the responsibility of the contributor.
## Coding Style
SharpEmu follows a consistent coding style across the project. Please ensure your contributions match the existing style.
- Use **4 spaces** for indentation (no tabs).
- Use **2 spaces** for XML-based files (e.g. `.csproj`, `.props`, `.targets`, `.xml`, `yml`, GitHub workflow files where applicable).
- Respect the project's `.editorconfig`.
- Ensure every text file ends with a **single trailing newline**
- Avoid formatting-only commits unless they are the purpose of the PR.
- Keep naming, formatting, and file organization consistent with the surrounding code.
- Prefer small, focused changes over large refactors.
### REUSE Compliance
This repository follows the REUSE Specification.
Every new file must contain the appropriate SPDX license header. Pull requests that do not comply with the project's REUSE requirements will fail CI and will not be merged.
### Recommended Development Environment
The repository includes an `.editorconfig` and Visual Studio solution files.
For the best experience, we recommend using:
- Visual Studio Code with;
- C#
- C# Dev Kit
Most editors that support `.editorconfig` will automatically apply the project's formatting rules.
+13
View File
@@ -122,3 +122,16 @@ Provided valuable references for filesystem handling and low-level C# implementa
# License
- [**GPL-2.0 license**](https://github.com/par274/sharpemu/blob/main/LICENSE)
## Contributing
Before opening an issue or pull request, please read our contribution guidelines:
**[CONTRIBUTING.md](./CONTRIBUTING.md)**
The guide covers:
- Coding style and formatting
- AI-assisted contributions
- Pull request expectations
- Testing guidelines
- Legal and reverse engineering policy
+1
View File
@@ -8,6 +8,7 @@ path = [
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
"src/SharpEmu.GUI/Languages/**",
"_logs/**",
".github/images/**",
"assets/images/**"
+7 -6
View File
@@ -3,8 +3,8 @@
#!/usr/bin/env python3
import struct
import hashlib
import struct
from base64 import b64encode as base64enc
from binascii import unhexlify as uhx
from pathlib import Path
@@ -21,7 +21,7 @@ def name2nid(name):
def generate():
names_path = Path(NAMES)
output_path = Path(OUTPUT)
entries = []
with open(names_path, 'r', encoding='utf-8') as f:
for line in f:
@@ -29,12 +29,12 @@ def generate():
if name:
nid = name2nid(name)
entries.append((nid, name))
print(f"Found {len(entries)} entries")
data = bytearray()
data.extend(struct.pack('<I', len(entries)))
for nid, name in entries:
nid_bytes = nid.encode('utf-8')
name_bytes = name.encode('utf-8')
@@ -42,10 +42,11 @@ def generate():
data.extend(nid_bytes)
data.extend(struct.pack('<H', len(name_bytes)))
data.extend(name_bytes)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(data)
print(f"Generated: {output_path} ({len(data):,} bytes)")
print(f"Total entries: {len(entries)}")
File diff suppressed because it is too large Load Diff
+373 -13
View File
@@ -8,12 +8,15 @@ using SharpEmu.HLE;
using SharpEmu.Logging;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace SharpEmu.CLI;
internal static partial class Program
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
private static readonly object ConsoleMirrorSync = new();
private static StreamWriter? _consoleMirrorFile;
private const int DefaultImportTraceLimit = 32;
private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
@@ -21,11 +24,15 @@ internal static partial class Program
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
private const int JobObjectExtendedLimitInformation = 9;
private const int STARTF_USESTDHANDLES = 0x00000100;
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
private const string MitigatedChildEnvironment = "SHARPEMU_MITIGATED_CHILD";
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const int ATTACH_PARENT_PROCESS = -1;
private const int STD_INPUT_HANDLE = -10;
private const int STD_OUTPUT_HANDLE = -11;
private const int STD_ERROR_HANDLE = -12;
private const uint GENERIC_READ = 0x80000000;
@@ -43,6 +50,7 @@ internal static partial class Program
}
finally
{
DropConsoleFileMirror();
SharpEmuLog.Shutdown();
}
}
@@ -61,6 +69,10 @@ internal static partial class Program
// itself to a console before the first write.
EnsureCliConsole();
UseUtf8ConsoleOutput();
if (isMitigatedChild && TryGetLogFileArgument(args, out var earlyLogFilePath))
{
TryEnableConsoleFileMirror(earlyLogFilePath);
}
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
@@ -69,15 +81,21 @@ internal static partial class Program
return childExitCode;
}
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel))
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
{
PrintUsage();
return 1;
}
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
{
TryEnableConsoleFileMirror(logFilePath);
}
SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner);
Log.Info(HostSystemInfo.Summary);
ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
@@ -234,6 +252,10 @@ internal static partial class Program
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
{
isMitigatedChild = false;
var trustedMitigatedChild = string.Equals(
Environment.GetEnvironmentVariable(MitigatedChildEnvironment),
"1",
StringComparison.Ordinal);
if (args.Length == 0)
{
return args;
@@ -244,7 +266,7 @@ internal static partial class Program
{
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
{
isMitigatedChild = true;
isMitigatedChild = trustedMitigatedChild;
continue;
}
@@ -283,9 +305,11 @@ internal static partial class Program
var commandLine = BuildCommandLine(processPath, childArgs);
var startupInfoEx = new STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
nint attributeList = 0;
nint mitigationPolicies = 0;
var previousChildEnvironment = Environment.GetEnvironmentVariable(MitigatedChildEnvironment);
try
{
nuint attributeListSize = 0;
@@ -293,7 +317,9 @@ internal static partial class Program
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
{
return false;
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to initialize mitigation attributes: {Marshal.GetLastWin32Error()}");
return true;
}
startupInfoEx.lpAttributeList = attributeList;
@@ -301,8 +327,7 @@ internal static partial class Program
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
var policy2 =
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -317,24 +342,31 @@ internal static partial class Program
0,
0))
{
return false;
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}");
return true;
}
var cmdLineBuilder = new StringBuilder(commandLine);
nint jobHandle = 0;
if (!CreateProcessW(
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
var created = CreateProcessW(
processPath,
cmdLineBuilder,
0,
0,
false,
true,
EXTENDED_STARTUPINFO_PRESENT,
0,
Environment.CurrentDirectory,
ref startupInfoEx,
out var processInfo))
out var processInfo);
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (!created)
{
return false;
childExitCode = 5;
Console.Error.WriteLine($"[ERROR] Failed to launch mitigated child process: {Marshal.GetLastWin32Error()}");
return true;
}
try
@@ -388,6 +420,8 @@ internal static partial class Program
}
finally
{
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (attributeList != 0)
{
DeleteProcThreadAttributeList(attributeList);
@@ -401,6 +435,238 @@ internal static partial class Program
}
}
private static bool TryGetLogFileArgument(IReadOnlyList<string> args, out string path)
{
for (var i = 0; i < args.Count; i++)
{
var argument = args[i];
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 < args.Count &&
!string.IsNullOrWhiteSpace(args[i + 1]) &&
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
ShouldConsumeLogFilePath(args, i + 1))
{
path = args[i + 1];
return true;
}
path = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
return true;
}
const string logFilePrefix = "--log-file=";
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrWhiteSpace(argument[logFilePrefix.Length..]))
{
path = argument[logFilePrefix.Length..];
return true;
}
}
path = string.Empty;
return false;
}
private static string BuildDefaultLogFilePath(string? ebootPath)
{
var baseDirectory = AppContext.BaseDirectory;
var logsDirectory = Path.Combine(baseDirectory, "user", "logs");
var name = TryReadTitleId(ebootPath) ?? "UNKNOWN";
foreach (var invalid in Path.GetInvalidFileNameChars())
{
name = name.Replace(invalid, '_');
}
return Path.Combine(logsDirectory, $"{name}-{DateTime.Now:yyyyMMdd-HHmmss}.log");
}
private static string? TryReadTitleId(string? ebootPath)
{
if (string.IsNullOrWhiteSpace(ebootPath))
{
return null;
}
try
{
var directory = Path.GetDirectoryName(Path.GetFullPath(ebootPath));
if (string.IsNullOrEmpty(directory))
{
return null;
}
foreach (var paramPath in new[]
{
Path.Combine(directory, "sce_sys", "param.json"),
Path.Combine(directory, "param.json"),
})
{
if (!File.Exists(paramPath))
{
continue;
}
using var stream = File.OpenRead(paramPath);
using var document = JsonDocument.Parse(stream);
if (document.RootElement.TryGetProperty("titleId", out var titleIdElement) &&
titleIdElement.ValueKind == JsonValueKind.String)
{
var titleId = titleIdElement.GetString();
if (!string.IsNullOrWhiteSpace(titleId))
{
return titleId.Trim();
}
}
}
}
catch
{
// Logging should never block launch; unknown title ids use a stable fallback.
}
return null;
}
private static string? TryFindEbootPathToken(IReadOnlyList<string> args)
{
for (var i = args.Count - 1; i >= 0; i--)
{
var argument = args[i];
if (string.IsNullOrWhiteSpace(argument) ||
argument.StartsWith("--", StringComparison.Ordinal))
{
continue;
}
return argument;
}
return null;
}
private static bool ShouldConsumeLogFilePath(IReadOnlyList<string> args, int candidateIndex)
{
var candidate = args[candidateIndex];
if (LooksLikeLogFilePath(candidate))
{
return true;
}
for (var i = candidateIndex + 1; i < args.Count; i++)
{
var argument = args[i];
if (!string.IsNullOrWhiteSpace(argument) &&
!argument.StartsWith("--", StringComparison.Ordinal))
{
return true;
}
}
return false;
}
private static bool LooksLikeLogFilePath(string path)
{
var extension = Path.GetExtension(path);
return string.Equals(extension, ".log", StringComparison.OrdinalIgnoreCase) ||
string.Equals(extension, ".txt", StringComparison.OrdinalIgnoreCase);
}
private static void TryEnableConsoleFileMirror(string path)
{
lock (ConsoleMirrorSync)
{
if (_consoleMirrorFile is not null)
{
return;
}
try
{
var directory = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var stream = new FileStream(
path,
FileMode.Create,
FileAccess.Write,
FileShare.ReadWrite,
bufferSize: 4096,
FileOptions.SequentialScan);
_consoleMirrorFile = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = true,
};
Console.SetOut(new TeeTextWriter(Console.Out, _consoleMirrorFile));
Console.SetError(new TeeTextWriter(Console.Error, _consoleMirrorFile));
Console.Error.WriteLine($"[DEBUG] Log file: {Path.GetFullPath(path)}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[WARN] Could not open log file '{path}': {ex.Message}");
}
}
}
private static void DropConsoleFileMirror()
{
lock (ConsoleMirrorSync)
{
try
{
_consoleMirrorFile?.Flush();
_consoleMirrorFile?.Dispose();
}
catch
{
}
_consoleMirrorFile = null;
}
}
private static void ConfigureInheritedStdHandles(ref STARTUPINFO startupInfo)
{
if (!OperatingSystem.IsWindows())
{
return;
}
var input = GetStdHandle(STD_INPUT_HANDLE);
var output = GetStdHandle(STD_OUTPUT_HANDLE);
var error = GetStdHandle(STD_ERROR_HANDLE);
if (!IsHandleValid(output) && !IsHandleValid(error))
{
return;
}
if (IsHandleValid(input))
{
_ = SetHandleInformation(input, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdInput = input;
}
if (IsHandleValid(output))
{
_ = SetHandleInformation(output, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdOutput = output;
}
if (IsHandleValid(error))
{
_ = SetHandleInformation(error, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
startupInfo.hStdError = error;
}
startupInfo.dwFlags |= STARTF_USESTDHANDLES;
}
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
{
var builder = new StringBuilder();
@@ -503,27 +769,30 @@ internal static partial class Program
private static void PrintUsage()
{
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\eboot.bin""");
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
}
private static bool TryParseArguments(
string[] args,
out string ebootPath,
out SharpEmuRuntimeOptions runtimeOptions,
out LogLevel logLevel)
out LogLevel logLevel,
out string? logFilePath)
{
if (args.Length == 0)
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
var strictDynlibResolution = false;
var importTraceLimit = 0;
var cpuEngine = CpuExecutionEngine.NativeOnly;
logFilePath = null;
logLevel = SharpEmuLog.MinimumLevel;
var pathTokens = new List<string>(args.Length);
for (var i = 0; i < args.Length; i++)
@@ -553,6 +822,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -566,6 +836,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -573,6 +844,23 @@ internal static partial class Program
continue;
}
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 < args.Length &&
!string.IsNullOrWhiteSpace(args[i + 1]) &&
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
ShouldConsumeLogFilePath(args, i + 1))
{
logFilePath = args[++i];
}
else
{
logFilePath = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
}
continue;
}
const string logLevelPrefix = "--log-level=";
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
{
@@ -596,6 +884,7 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -618,11 +907,27 @@ internal static partial class Program
continue;
}
const string logFilePrefix = "--log-file=";
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase))
{
logFilePath = argument[logFilePrefix.Length..];
if (string.IsNullOrWhiteSpace(logFilePath))
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
return false;
}
continue;
}
if (argument.StartsWith("--", StringComparison.Ordinal))
{
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -634,6 +939,7 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -735,6 +1041,56 @@ internal static partial class Program
public nuint PeakJobMemoryUsed;
}
private sealed class TeeTextWriter : TextWriter
{
private readonly TextWriter _primary;
private readonly TextWriter _mirror;
public TeeTextWriter(TextWriter primary, TextWriter mirror)
{
_primary = primary;
_mirror = mirror;
}
public override Encoding Encoding => _primary.Encoding;
public override void Write(char value)
{
lock (ConsoleMirrorSync)
{
_primary.Write(value);
_mirror.Write(value);
}
}
public override void Write(string? value)
{
lock (ConsoleMirrorSync)
{
_primary.Write(value);
_mirror.Write(value);
}
}
public override void WriteLine(string? value)
{
lock (ConsoleMirrorSync)
{
_primary.WriteLine(value);
_mirror.WriteLine(value);
}
}
public override void Flush()
{
lock (ConsoleMirrorSync)
{
_primary.Flush();
_mirror.Flush();
}
}
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool InitializeProcThreadAttributeList(
@@ -819,6 +1175,10 @@ internal static partial class Program
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetStdHandle(int stdHandle, nint handle);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetHandleInformation(nint handle, uint mask, uint flags);
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern nint CreateFileW(
string fileName,
+8
View File
@@ -24,6 +24,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>0.0.1</Version>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
@@ -48,6 +51,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TargetPath>LICENSE.txt</TargetPath>
<Visible>False</Visible>
</Content>
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
<Visible>False</Visible>
</Content>
</ItemGroup>
<!-- Keep glfw as a loose file next to the executable; every other native
+4 -1
View File
@@ -216,7 +216,10 @@
}
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
@@ -8,6 +8,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu.Native;
@@ -37,13 +38,78 @@ public sealed partial class DirectExecutionBackend
}
}
private void RecordDeferredBootstrapTrace(
long dispatchIndex,
ulong op,
ulong symbolPointer,
ulong outputPointer,
ulong returnRip)
{
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
dispatchIndex,
op,
symbolPointer,
outputPointer,
returnRip);
_deferredBootstrapTraceWriteIndex =
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
{
_deferredBootstrapTraceCount++;
}
}
}
private void DrainDeferredBootstrapTraces()
{
if (!_logBootstrap)
{
return;
}
DeferredBootstrapTraceEntry[] pending;
lock (_deferredBootstrapTraceGate)
{
if (_deferredBootstrapTraceCount == 0)
{
return;
}
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
{
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
}
_deferredBootstrapTraceCount = 0;
}
foreach (var entry in pending)
{
var symbolText = "<unreadable>";
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
}
}
private void DumpRecentImportTrace()
{
if (_recentImportTraceCount == 0)
{
return;
}
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
for (int i = 0; i < _recentImportTraceCount; i++)
{
@@ -51,8 +117,8 @@ public sealed partial class DirectExecutionBackend
var entry = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(entry.Nid))
{
Console.Error.WriteLine(
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
Log.Info(
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
}
}
@@ -91,12 +157,12 @@ public sealed partial class DirectExecutionBackend
list.Add(num7);
if (num2 < 32)
{
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
Log.Info($"Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
num2++;
}
if (num >= 16384)
{
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
Log.Warning($"Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
return list;
}
}
@@ -106,7 +172,7 @@ public sealed partial class DirectExecutionBackend
}
if (num != 0)
{
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
Log.Warning($"Suspicious unresolved pointer hits: {num}");
}
return list;
}
@@ -121,18 +187,18 @@ public sealed partial class DirectExecutionBackend
Span<byte> destination = stackalloc byte[128];
if (!cpuContext.Memory.TryRead(returnRip, destination))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
Log.Debug($"Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
return;
}
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
Log.Debug($"Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
{
int num = BitConverter.ToInt32(destination.Slice(2, 4));
ulong num2 = returnRip + 6 + (ulong)num;
if (cpuContext.TryReadUInt64(num2, out var value2))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
Log.Debug($"Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
}
}
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
@@ -141,7 +207,7 @@ public sealed partial class DirectExecutionBackend
ulong num4 = returnRip + 7 + (ulong)num3;
if (cpuContext.TryReadUInt64(num4, out var value3))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
Log.Debug($"Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
}
}
for (int i = 0; i + 6 <= destination.Length; i++)
@@ -153,7 +219,7 @@ public sealed partial class DirectExecutionBackend
ulong num7 = num6 + 6 + (ulong)num5;
if (cpuContext.TryReadUInt64(num7, out var value4))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
Log.Debug($"Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
}
}
}
@@ -168,32 +234,33 @@ public sealed partial class DirectExecutionBackend
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
ulong callRip = returnRip + (ulong)i;
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
var importEntries = _importEntries;
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != target)
if (importEntries[importIndex].Address != target)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
Log.Debug(
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
Log.Debug(
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
}
break;
}
if (cpuContext.Memory.TryRead(target, targetBytes))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
Log.Debug(
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
{
@@ -201,25 +268,25 @@ public sealed partial class DirectExecutionBackend
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
Log.Debug(
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != slotTarget)
if (importEntries[importIndex].Address != slotTarget)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
Log.Debug(
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
Log.Debug(
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
}
break;
}
@@ -103,6 +103,15 @@ public sealed partial class DirectExecutionBackend
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
// Thread-mode probe: a hardware exception raised while this thread is inside
// the managed import gateway means the VEH->managed reentry happened from
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
if (LogThreadMode && _threadModeGatewayDepth > 0)
{
TraceThreadMode(
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
}
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
@@ -8,6 +8,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
@@ -19,6 +20,16 @@ public sealed partial class DirectExecutionBackend
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
{
if (LogThreadMode)
{
_threadModeGatewayCalls++;
_threadModeGatewayDepth++;
if (!_threadModeGatewayFirstLogged)
{
_threadModeGatewayFirstLogged = true;
TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}");
}
}
try
{
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
@@ -36,6 +47,13 @@ public sealed partial class DirectExecutionBackend
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
return 18446744071562199298uL;
}
finally
{
if (LogThreadMode)
{
_threadModeGatewayDepth--;
}
}
}
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
@@ -69,6 +87,12 @@ public sealed partial class DirectExecutionBackend
WriteCtxU64(contextRecord, 152, nextRsp);
WriteCtxU64(contextRecord, 248, returnRip);
Interlocked.Increment(ref _rawSentinelRecoveries);
if (LogThreadMode)
{
TraceThreadMode(
$"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " +
$"gateway_depth={_threadModeGatewayDepth}");
}
return -1;
}
return 0;
@@ -87,12 +111,13 @@ public sealed partial class DirectExecutionBackend
LastError = "Import dispatch called without active CPU context";
return 18446744071562199298uL;
}
if ((uint)importIndex >= (uint)_importEntries.Length)
var importEntries = _importEntries;
if ((uint)importIndex >= (uint)importEntries.Length)
{
LastError = $"Import dispatch index out of range: {importIndex}";
return 18446744071562199042uL;
}
ImportStubEntry importStubEntry = _importEntries[importIndex];
ImportStubEntry importStubEntry = importEntries[importIndex];
int num2 = Volatile.Read(in _rawSentinelRecoveries);
if (num2 != _lastReportedRawSentinelRecoveries)
{
@@ -136,6 +161,14 @@ public sealed partial class DirectExecutionBackend
*(ulong*)(xmmSlot + 8));
}
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
{
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
*(ulong*)(argPackPtr + 8) = cpuContext[CpuRegister.Rsi];
*(ulong*)(argPackPtr + 16) = cpuContext[CpuRegister.Rdx];
}
ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi];
ulong num3 = cpuContext[CpuRegister.Rdx];
@@ -186,16 +219,6 @@ public sealed partial class DirectExecutionBackend
TraceGuestContext(
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
}
if (_logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
string symbolText = "<unreadable>";
if (TryReadAsciiZ(value2, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
}
if (!isGuestWorker &&
!ActiveForcedGuestExit &&
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
@@ -204,6 +227,17 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
// Backend teardown in progress: unwind this worker to the host instead of
// dispatching. RunGuestThread parks it as Blocked with no wake handler, its
// host thread exits, and RequestGuestThreadTeardown can finish before any
// executable memory is freed.
if (isGuestWorker &&
_guestTeardownRequested &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "backend teardown"))
{
cpuContext[CpuRegister.Rax] = 0uL;
return 0uL;
}
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
@@ -211,12 +245,13 @@ public sealed partial class DirectExecutionBackend
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
bool flag5 = false;
ExportedFunction? matchedExport = importStubEntry.Export;
var traceFlags = importStubEntry.TraceFlags;
bool periodicTrace = num <= 128 ||
(num >= 240 && num <= 400) ||
(num >= 900 && num <= 1300) ||
num % 100000 == 0L ||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
flag ||
flag2 ||
flag3;
@@ -277,15 +312,15 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rsi],
cpuContext[CpuRegister.Rdx]);
}
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256)
{
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
if ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64)
{
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
if ((traceFlags & ImportStubTraceFlags.RawArgs) != 0)
{
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
@@ -315,7 +350,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush();
}
}
if (importStubEntry.Nid == "Ou3iL1abvng")
if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
{
if (_logStackCheck)
{
@@ -347,16 +382,20 @@ public sealed partial class DirectExecutionBackend
ActiveGuestReturnSlotAddress);
try
{
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
{
if (_logBootstrap)
{
RecordDeferredBootstrapTrace(num, value, value2, num3, num7);
}
orbisGen2Result = DispatchBootstrapBridge();
}
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
{
orbisGen2Result = DispatchKernelDynlibDlsym();
}
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
{
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
}
@@ -511,6 +550,7 @@ public sealed partial class DirectExecutionBackend
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
DrainDeferredBootstrapTraces();
return cpuContext[CpuRegister.Rax];
}
catch (Exception ex)
@@ -519,6 +559,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
DrainDeferredBootstrapTraces();
return 18446744071562199298uL;
}
}
@@ -671,6 +712,7 @@ public sealed partial class DirectExecutionBackend
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Q2V+iqvjgC0" or // vsnprintf
"AV6ipCNa4Rw" or // strcasecmp
"q1cHNfGycLI" or // scePadRead
"xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent
@@ -768,11 +810,15 @@ public sealed partial class DirectExecutionBackend
return !_logUsleep;
}
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
// slows guest spinlocks enough to livelock (Demon's Souls).
return nid is
"9UK1vLZQft4" or // scePthreadMutexLock
"tn3VlD0hG60" or // scePthreadMutexUnlock
"7H0iTOciTLo" or // pthread_mutex_lock
"2Z+PpY6CaJg" or // pthread_mutex_unlock
"EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock
"q1cHNfGycLI" or // scePadRead
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
@@ -835,6 +881,7 @@ public sealed partial class DirectExecutionBackend
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
"AV6ipCNa4Rw" or // strcasecmp
"pNtJdE3x49E" or // wcscmp
"fV2xHER+bKE" or // wcscoll
"E8wCoUEbfzk" or // wcsncmp
@@ -845,12 +892,7 @@ public sealed partial class DirectExecutionBackend
"6ULAa0fq4jA" or // scePthreadRwlockInit
"1471ajPzxh0" or // pthread_rwlock_destroy
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
"mqdNorrB+gI" or // scePthreadRwlockWrlock
"EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
"aI+OeCz8xrQ" or // scePthreadSelf
"EotR8a3ASf4" or // pthread_self
"eoht7mQOCmo" or // scePthreadGetspecific
@@ -860,7 +902,12 @@ public sealed partial class DirectExecutionBackend
"vz+pg2zdopI" or // sceKernelGetEventUserData
"mJ7aghmgvfc" or // sceKernelGetEventId
"23CPPI1tyBY" or // sceKernelGetEventFilter
"kwGyyjohI50"; // sceKernelGetEventData
"kwGyyjohI50" or // sceKernelGetEventData
// _Getpctype is a per-character ctype table lookup; the embedded mcpp preprocessor
// re-fetches the table pointer on every isdigit()/isalpha() check, so classifying a
// large input legitimately calls it hundreds of thousands of times back-to-back -
// real, finite work that would otherwise trip the loop guard.
"sUP1hBaouOw"; // _Getpctype
}
private long NextImportDispatchIndex()
@@ -1030,7 +1077,15 @@ public sealed partial class DirectExecutionBackend
}
private static bool IsImportLoopGuardBoundary(string nid) =>
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
nid switch
{
"1jfXLRVzisc" => true, // sceKernelUsleep
"QcteRwbsnV0" => true, // usleep
"n88vx3C5nW8" => true, // gettimeofday
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
"T72hz6ffq08" => true, // scePthreadYield
_ => false
};
private void ResetImportLoopPattern()
{
@@ -1270,30 +1325,485 @@ public sealed partial class DirectExecutionBackend
{
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
ulong outputAddress = cpuContext[CpuRegister.Rdx];
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress);
try
{
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
!TryResolveDlsymGuestAddress(symbolName, out var resolvedAddress) ||
outputAddress == 0 ||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
{
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
}
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
catch
{
Console.Error.WriteLine(
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
{
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
cpuContext[CpuRegister.Rax] = 0uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void NormalizeKernelDynlibDlsymArguments(
CpuContext cpuContext,
out ulong symbolNameAddress,
out ulong outputAddress)
{
var handle = cpuContext[CpuRegister.Rdi];
symbolNameAddress = cpuContext[CpuRegister.Rsi];
outputAddress = cpuContext[CpuRegister.Rdx];
// Standalone bootstrap loaders sometimes call through the bridge with
// (symbol_ptr, handle, out) while sceKernelDlsym is (handle, symbol_ptr, out).
// Heuristic only: valid when RSI looks like a small handle and RDI is a guest pointer.
if (symbolNameAddress < 0x10000 &&
IsPlausibleDynlibSymbolPointer(handle))
{
symbolNameAddress = handle;
handle = cpuContext[CpuRegister.Rsi];
cpuContext[CpuRegister.Rdi] = handle;
cpuContext[CpuRegister.Rsi] = symbolNameAddress;
}
}
private static bool IsPlausibleDynlibSymbolPointer(ulong address)
{
return address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
}
private OrbisGen2Result CompleteKernelDynlibDlsymFailure(CpuContext cpuContext, ulong outputAddress)
{
if (outputAddress != 0)
{
_ = TryWriteUInt64Compat(outputAddress, 0);
}
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
private void ResetLazyDlsymStubState()
{
lock (_lazyDlsymStubGate)
{
_lazyDlsymStubCache.Clear();
_lazyImportStubPoolMapped = false;
_lazyImportStubPoolBase = 0;
_lazyImportStubNextSlot = 0;
_lazyImportStubPoolLimit = 0;
}
}
private bool TryResolveDlsymGuestAddress(string symbolName, out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(symbolName))
{
return false;
}
// Tier 0: ELF runtime symbols and aliases.
if (TryResolveRuntimeSymbolAddress(symbolName, out guestAddress) ||
TryResolveRuntimeSymbolAlias(symbolName, out guestAddress))
{
return true;
}
// Tier 1: existing import stub entries (duplicate prevention).
if (TryFindImportStubGuestAddress(symbolName, out guestAddress))
{
return true;
}
var hasAerolibSymbol = Aerolib.Instance.TryGetByExportName(symbolName, out var hleSymbol);
if (hasAerolibSymbol)
{
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (TryResolveRuntimeSymbolAddress(hleSymbol.Nid, out guestAddress) ||
TryResolveRuntimeSymbolAddress(hleSymbol.ExportName, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
{
return true;
}
}
else if (Aerolib.Instance.TryGetByNid(symbolName, out hleSymbol))
{
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
{
return true;
}
hasAerolibSymbol = true;
}
// Tier 3: lazy materialization for callable HLE symbols missing from ELF imports.
if (!TryResolveLazyDispatchTarget(symbolName, hasAerolibSymbol, in hleSymbol, out var dispatchNid, out var export))
{
return false;
}
return TryGetOrCreateLazyImportStub(
dispatchNid,
symbolName,
hasAerolibSymbol ? hleSymbol : null,
export,
out guestAddress);
}
private bool TryFindImportStubGuestAddress(string identifier, out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(identifier))
{
return false;
}
var importEntries = _importEntries;
for (var i = 0; i < importEntries.Length; i++)
{
var entry = importEntries[i];
if (!ImportStubEntryMatchesIdentifier(entry, identifier))
{
continue;
}
if (entry.Address >= 0x10000)
{
guestAddress = entry.Address;
return true;
}
}
return false;
}
private static bool ImportStubEntryMatchesIdentifier(in ImportStubEntry entry, string identifier)
{
if (string.Equals(entry.Nid, identifier, StringComparison.Ordinal))
{
return true;
}
if (entry.Export is not { } export)
{
return false;
}
return string.Equals(export.Name, identifier, StringComparison.Ordinal) ||
string.Equals(export.Nid, identifier, StringComparison.Ordinal);
}
private static bool IsKernelDynlibDlsymIdentifier(string identifier)
{
return string.Equals(identifier, "sceKernelDlsym", StringComparison.Ordinal) ||
string.Equals(identifier, KernelDynlibDlsymAerolibNid, StringComparison.Ordinal) ||
string.Equals(identifier, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal);
}
private bool TryResolveLazyDispatchTarget(
string symbolName,
bool hasAerolibSymbol,
in SysAbiSymbol hleSymbol,
out string dispatchNid,
out ExportedFunction? export)
{
export = null;
dispatchNid = string.Empty;
if (IsKernelDynlibDlsymIdentifier(symbolName))
{
dispatchNid = KernelDynlibDlsymAerolibNid;
_ = _moduleManager.TryGetExport(dispatchNid, out export);
return true;
}
if (!hasAerolibSymbol)
{
return false;
}
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
{
return false;
}
if (_moduleManager.TryGetExport(hleSymbol.Nid, out export))
{
dispatchNid = hleSymbol.Nid;
return true;
}
return false;
}
private bool TryGetOrCreateLazyImportStub(
string dispatchNid,
string symbolName,
SysAbiSymbol? hleSymbol,
ExportedFunction? export,
out ulong guestAddress)
{
guestAddress = 0;
if (string.IsNullOrWhiteSpace(dispatchNid))
{
return false;
}
lock (_lazyDlsymStubGate)
{
if (TryGetCachedLazyDlsymAddress(dispatchNid, symbolName, out guestAddress))
{
return true;
}
if (!EnsureLazyImportStubPoolMapped())
{
LogLazyImportStubMaterializationFailure(
"import stub region unresolved",
dispatchNid,
symbolName);
return false;
}
if (!TryAllocateLazyImportStubSlot(out guestAddress))
{
LogLazyImportStubMaterializationFailure(
"lazy import stub pool exhausted",
dispatchNid,
symbolName);
return false;
}
var previousEntries = _importEntries;
var importIndex = previousEntries.Length;
var newEntries = new ImportStubEntry[importIndex + 1];
if (importIndex > 0)
{
Array.Copy(previousEntries, newEntries, importIndex);
}
newEntries[importIndex] = new ImportStubEntry(guestAddress, dispatchNid, export);
var hostTrampoline = CreateImportHandlerTrampoline(importIndex);
if (hostTrampoline == 0 ||
!PatchImportStub((nint)(long)guestAddress, hostTrampoline))
{
guestAddress = 0;
LogLazyImportStubMaterializationFailure(
"failed to patch lazy import stub trampoline",
dispatchNid,
symbolName);
return false;
}
_importEntries = newEntries;
RegisterDlsymRuntimeSymbolKeys(symbolName, dispatchNid, hleSymbol, guestAddress);
return true;
}
}
private bool TryGetCachedLazyDlsymAddress(string dispatchNid, string symbolName, out ulong guestAddress)
{
if (_lazyDlsymStubCache.TryGetValue(dispatchNid, out guestAddress) ||
_lazyDlsymStubCache.TryGetValue(symbolName, out guestAddress))
{
return guestAddress >= 0x10000;
}
return false;
}
private void RegisterDlsymRuntimeSymbolKeys(
string symbolName,
string dispatchNid,
SysAbiSymbol? hleSymbol,
ulong guestAddress)
{
RegisterDlsymRuntimeSymbolKey(symbolName, guestAddress);
RegisterDlsymRuntimeSymbolKey(dispatchNid, guestAddress);
if (IsKernelDynlibDlsymIdentifier(symbolName) || IsKernelDynlibDlsymIdentifier(dispatchNid))
{
RegisterDlsymRuntimeSymbolKey(RuntimeStubNids.KernelDynlibDlsym, guestAddress);
RegisterDlsymRuntimeSymbolKey(KernelDynlibDlsymAerolibNid, guestAddress);
RegisterDlsymRuntimeSymbolKey("sceKernelDlsym", guestAddress);
}
if (hleSymbol.HasValue)
{
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.Nid, guestAddress);
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.ExportName, guestAddress);
}
}
private void RegisterDlsymRuntimeSymbolKey(string key, ulong guestAddress)
{
if (string.IsNullOrWhiteSpace(key) || !IsRuntimeSymbolAddressUsable(guestAddress))
{
return;
}
_runtimeSymbolsByName[key] = guestAddress;
_lazyDlsymStubCache[key] = guestAddress;
}
private void LogLazyImportStubMaterializationFailure(string reason, string dispatchNid, string symbolName)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Lazy import stub materialization failed ({reason}): " +
$"nid={dispatchNid} symbol='{symbolName}'");
}
private unsafe bool TryResolveImportStubRegionBounds(
ImportStubEntry[] importEntries,
out ulong regionBase,
out ulong regionLimit)
{
regionBase = 0;
regionLimit = 0;
for (var candidateIndex = 0; candidateIndex < 64; candidateIndex++)
{
var candidateBase = ImportStubRegionCanonicalBase -
(ulong)candidateIndex * ImportStubRegionAddressStride;
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
memoryInfo.RegionSize == 0 ||
memoryInfo.State != 4096)
{
continue;
}
var candidateLimit = candidateBase + memoryInfo.RegionSize;
var hasStub = false;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < candidateBase || entryAddress >= candidateLimit)
{
continue;
}
if ((entryAddress - candidateBase) % LazyImportStubSlotSize != 0)
{
continue;
}
hasStub = true;
break;
}
if (!hasStub)
{
continue;
}
regionBase = candidateBase;
regionLimit = candidateLimit;
return true;
}
ulong maxStubEnd = 0;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < ImportStubRegionCanonicalBase)
{
continue;
}
if ((entryAddress - ImportStubRegionCanonicalBase) % LazyImportStubSlotSize != 0)
{
continue;
}
var entryEnd = entryAddress + LazyImportStubSlotSize;
if (entryEnd > maxStubEnd)
{
maxStubEnd = entryEnd;
regionBase = ImportStubRegionCanonicalBase;
}
}
if (regionBase == 0 || maxStubEnd <= regionBase)
{
return false;
}
var spanBytes = maxStubEnd - regionBase;
regionLimit = regionBase + AlignUp(spanBytes, ImportStubRegionPageSize);
return regionLimit > regionBase;
}
private bool EnsureLazyImportStubPoolMapped()
{
if (_lazyImportStubPoolMapped && _lazyImportStubPoolBase != 0)
{
return true;
}
var importEntries = _importEntries;
if (!TryResolveImportStubRegionBounds(importEntries, out var importStubRegionBase, out var importStubRegionLimit))
{
return false;
}
ulong nextSlot = importStubRegionBase;
for (var i = 0; i < importEntries.Length; i++)
{
var entryAddress = importEntries[i].Address;
if (entryAddress < importStubRegionBase || entryAddress >= importStubRegionLimit)
{
continue;
}
var entryEnd = entryAddress + LazyImportStubSlotSize;
if (entryEnd > nextSlot)
{
nextSlot = entryEnd;
}
}
if (nextSlot >= importStubRegionLimit)
{
return false;
}
_lazyImportStubPoolBase = importStubRegionBase;
_lazyImportStubNextSlot = nextSlot;
_lazyImportStubPoolLimit = importStubRegionLimit;
_lazyImportStubPoolMapped = true;
return true;
}
private bool TryAllocateLazyImportStubSlot(out ulong guestAddress)
{
guestAddress = 0;
if (!_lazyImportStubPoolMapped ||
_lazyImportStubNextSlot < _lazyImportStubPoolBase ||
_lazyImportStubNextSlot + LazyImportStubSlotSize > _lazyImportStubPoolLimit)
{
return false;
}
guestAddress = _lazyImportStubNextSlot;
_lazyImportStubNextSlot += LazyImportStubSlotSize;
return true;
}
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
{
address = 0;
@@ -1358,29 +1868,20 @@ public sealed partial class DirectExecutionBackend
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong bridgeHandle = cpuContext[CpuRegister.Rdi];
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
ulong outputAddress = cpuContext[CpuRegister.Rdx];
_ = TryReadAsciiZ(symbolNameAddress, 512, out var symbolName);
OrbisGen2Result result = DispatchKernelDynlibDlsym();
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
try
{
return result;
OrbisGen2Result result = DispatchKernelDynlibDlsym();
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
}
if (_logBootstrap)
catch
{
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
}
if (cpuContext[CpuRegister.Rax] == 0uL)
{
return OrbisGen2Result.ORBIS_GEN2_OK;
}
Console.Error.WriteLine(
$"[LOADER][WARN] bootstrap_bridge unresolved: handle=0x{bridgeHandle:X} symbol='{symbolName}' out=0x{outputAddress:X16}");
return OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1421,6 +1922,7 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
Span<byte> destination = stackalloc byte[1];
for (int i = 0; i < maxLength; i++)
@@ -0,0 +1,582 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
// Guest entry stubs must not run above CLR-managed frames on a CLR-created thread:
// the suspension/stack-walk machinery then has to traverse a frame chain that is
// interleaved with guest stubs carrying no CLR unwind info, and any window in which
// the thread-mode bookkeeping disagrees with the actual stack (import dispatch, VEH
// redirection) fail-fasts the runtime — the audio_output_thread ~7th-cycle
// "attempted to call a UnmanagedCallersOnly method from managed code" crash.
//
// Guest execution therefore runs on dedicated raw OS threads whose run loop is
// emitted native code. While guest code executes there is not a single managed
// frame on the thread and it sits in preemptive mode, so the GC ignores it
// entirely. The only managed activity on the thread is the reverse-P/Invoke import
// dispatch and the per-run prologue/epilogue, which the CLR supports on foreign
// threads (lazy attach, preemptive between calls). The managed orchestrator
// (RunGuestThread / the main execution thread) parks in a preemptive wait for the
// duration of the run, so its own frame chain is never walked across guest frames.
private static readonly bool NativeGuestWorkersDisabled =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
private readonly object _nativeWorkerGate = new();
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
private bool _nativeWorkersDisposed;
private int _nativeWorkerCreationFailedLogged;
private const uint StackSizeParamIsAReservation = 0x00010000u;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint CreateThread(
nint lpThreadAttributes,
nuint dwStackSize,
nint lpStartAddress,
nint lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
// thread; falls back to the historical inline calli (guest frames above this
// thread's managed frames) when workers are disabled or unavailable.
//
// Callers set the Active* thread-statics before emitting the stub and read the
// yield/forced-exit flags right after this returns, so the worker outcome is
// copied back into this thread's statics before returning.
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
{
var worker = RentNativeGuestExecutor();
if (worker is null)
{
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
return CallNativeEntry(entryStub);
}
try
{
var state = _activeGuestThreadState;
var nativeReturn = worker.Run(
_activeCpuContext!,
state,
GuestThreadExecution.CurrentGuestThreadHandle,
_activeEntryReturnSentinelRip,
_activeGuestReturnSlotAddress,
(nint)hostRspSlot,
(nint)entryStub,
state?.AffinityMask ?? 0,
out var yieldRequested,
out var yieldReason,
out var forcedExit);
_activeGuestThreadYieldRequested = yieldRequested;
_activeGuestThreadYieldReason = yieldReason;
_activeForcedGuestExit = forcedExit;
return nativeReturn;
}
finally
{
ReturnNativeGuestExecutor(worker);
}
}
private NativeGuestExecutor? RentNativeGuestExecutor()
{
if (NativeGuestWorkersDisabled)
{
return null;
}
lock (_nativeWorkerGate)
{
if (_nativeWorkersDisposed)
{
return null;
}
if (_idleNativeWorkers.Count > 0)
{
return _idleNativeWorkers.Pop();
}
}
var worker = NativeGuestExecutor.TryCreate(this);
if (worker is null)
{
if (Interlocked.Exchange(ref _nativeWorkerCreationFailedLogged, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][WARN] Failed to create a native guest worker thread; falling back to inline guest execution.");
}
return null;
}
lock (_nativeWorkerGate)
{
if (_nativeWorkersDisposed)
{
worker.Dispose();
return null;
}
_allNativeWorkers.Add(worker);
}
return worker;
}
private void ReturnNativeGuestExecutor(NativeGuestExecutor worker)
{
lock (_nativeWorkerGate)
{
if (!_nativeWorkersDisposed)
{
_idleNativeWorkers.Push(worker);
return;
}
}
worker.Dispose();
}
private void DisposeNativeGuestExecutors()
{
NativeGuestExecutor[] workers;
lock (_nativeWorkerGate)
{
if (_nativeWorkersDisposed)
{
return;
}
_nativeWorkersDisposed = true;
workers = _allNativeWorkers.ToArray();
_allNativeWorkers.Clear();
_idleNativeWorkers.Clear();
}
foreach (var worker in workers)
{
worker.Dispose();
}
}
// A pooled raw OS thread that executes guest entry stubs. The run loop is emitted
// native code (no CLR unwind info required — nothing ever unwinds through it):
//
// loop: WaitForSingleObject(work);
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
// SetEvent(done); goto loop;
//
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
// the host-RSP slot, the Active* thread-statics and the GuestThreadExecution ambient
// on every run, so a worker can be reused for any guest thread.
private sealed unsafe class NativeGuestExecutor : IDisposable
{
private const uint LoopStubSize = 512u;
private const uint WorkerStackReservation = 4u * 1024u * 1024u;
private static nint _waitForSingleObjectAddress;
private static nint _setEventAddress;
private static nint _exitThreadAddress;
private readonly DirectExecutionBackend _backend;
private readonly AutoResetEvent _workAvailable = new(false);
private readonly AutoResetEvent _workCompleted = new(false);
private GCHandle _selfHandle;
private void* _controlBlock;
private void* _loopStub;
private nint _threadHandle;
private uint _nativeThreadId;
// Single in-flight run; publication is ordered by the work/done event pair.
private CpuContext? _runContext;
private GuestThreadState? _runState;
private ulong _runGuestThreadHandle;
private ulong _runSentinelRip;
private ulong _runReturnSlotAddress;
private nint _runHostRspSlot;
private nint _runEntryStub;
private ulong _runAffinityMask;
private int _runNativeResult;
private bool _runYieldRequested;
private string? _runYieldReason;
private bool _runForcedExit;
private bool _runPrologueFailed;
// Prologue -> epilogue carry, only touched on the worker thread.
private DirectExecutionBackend? _prevBackend;
private CpuContext? _prevContext;
private ulong _prevSentinel;
private ulong _prevReturnSlot;
private bool _prevForcedExit;
private bool _prevYieldRequested;
private string? _prevYieldReason;
private GuestThreadState? _prevState;
private ulong _prevGuestThreadHandle;
private nint _prevHostRspSlot;
private int _prevHostThreadId;
private bool _entered;
private NativeGuestExecutor(DirectExecutionBackend backend)
{
_backend = backend;
}
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
{
if (!EnsureKernel32Exports())
{
return null;
}
var executor = new NativeGuestExecutor(backend);
if (!executor.Initialize())
{
executor.Dispose();
return null;
}
return executor;
}
private static bool EnsureKernel32Exports()
{
if (_exitThreadAddress != 0)
{
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
}
nint kernel32 = GetModuleHandle("kernel32.dll");
if (kernel32 == 0)
{
return false;
}
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
}
private bool Initialize()
{
_selfHandle = GCHandle.Alloc(this);
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
if (_controlBlock == null)
{
return false;
}
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
if (_loopStub == null)
{
return false;
}
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
byte* code = (byte*)_loopStub;
int offset = 0;
void Emit(byte value) => code[offset++] = value;
void EmitMovRcxImm64(ulong value)
{
Emit(0x48); Emit(0xB9);
*(ulong*)(code + offset) = value;
offset += sizeof(ulong);
}
void EmitMovRaxImm64(ulong value)
{
Emit(0x48); Emit(0xB8);
*(ulong*)(code + offset) = value;
offset += sizeof(ulong);
}
void EmitCallRax()
{
Emit(0xFF); Emit(0xD0);
}
void EmitSetDoneEvent()
{
EmitMovRcxImm64((ulong)doneHandle);
EmitMovRaxImm64((ulong)_setEventAddress);
EmitCallRax();
}
// Thread entry leaves RSP ≡ 8 (mod 16); after this every call below happens
// at RSP ≡ 0 with shadow space available.
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x28); // sub rsp, 0x28
int loopStart = offset;
EmitMovRcxImm64((ulong)workHandle);
Emit(0xBA); // mov edx, INFINITE
*(uint*)(code + offset) = 0xFFFFFFFFu;
offset += sizeof(uint);
EmitMovRaxImm64((ulong)_waitForSingleObjectAddress);
EmitCallRax();
EmitMovRaxImm64((ulong)_controlBlock);
Emit(0x83); Emit(0x38); Emit(0x00); // cmp dword [rax], 0
Emit(0x0F); Emit(0x85); // jne stop
int stopJump = offset;
offset += sizeof(int);
EmitMovRcxImm64((ulong)executorHandle);
EmitMovRaxImm64((ulong)prologuePtr);
EmitCallRax(); // rax = entry stub, or 0 on failure
Emit(0x48); Emit(0x85); Emit(0xC0); // test rax, rax
Emit(0x0F); Emit(0x84); // je skipEntry
int skipJump = offset;
offset += sizeof(int);
EmitCallRax(); // guest entry stub -> eax
int skipEntryOffset = offset;
Emit(0x89); Emit(0xC2); // mov edx, eax
EmitMovRcxImm64((ulong)executorHandle);
EmitMovRaxImm64((ulong)epiloguePtr);
EmitCallRax();
EmitSetDoneEvent();
Emit(0xE9); // jmp loop
*(int*)(code + offset) = loopStart - (offset + sizeof(int));
offset += sizeof(int);
int stopOffset = offset;
// Signal done so a Run() racing teardown cannot park forever; a stopped
// worker is never re-rented, so the stale signal is unobservable.
EmitSetDoneEvent();
Emit(0x31); Emit(0xC9); // xor ecx, ecx
EmitMovRaxImm64((ulong)_exitThreadAddress);
EmitCallRax();
Emit(0xCC); // int3 (ExitThread never returns)
*(int*)(code + stopJump) = stopOffset - (stopJump + sizeof(int));
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
uint oldProtect = 0;
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
{
return false;
}
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
_threadHandle = CreateThread(
0,
WorkerStackReservation,
(nint)_loopStub,
0,
StackSizeParamIsAReservation,
out _nativeThreadId);
if (_threadHandle == 0)
{
return false;
}
if (LogThreadMode)
{
TraceThreadMode($"worker_created native_tid={_nativeThreadId} loop=0x{(ulong)_loopStub:X16}");
}
return true;
}
public int Run(
CpuContext context,
GuestThreadState? state,
ulong guestThreadHandle,
ulong sentinelRip,
ulong returnSlotAddress,
nint hostRspSlot,
nint entryStub,
ulong affinityMask,
out bool yieldRequested,
out string? yieldReason,
out bool forcedExit)
{
_runContext = context;
_runState = state;
_runGuestThreadHandle = guestThreadHandle;
_runSentinelRip = sentinelRip;
_runReturnSlotAddress = returnSlotAddress;
_runHostRspSlot = hostRspSlot;
_runEntryStub = entryStub;
_runAffinityMask = affinityMask;
_runPrologueFailed = true;
_runYieldRequested = false;
_runYieldReason = null;
_runForcedExit = false;
_workAvailable.Set();
_workCompleted.WaitOne();
_runContext = null;
_runState = null;
yieldRequested = _runYieldRequested;
yieldReason = _runYieldReason;
forcedExit = _runForcedExit;
if (_runPrologueFailed)
{
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
}
return _runNativeResult;
}
[UnmanagedCallersOnly]
private static nint RunPrologue(nint executorHandle)
{
try
{
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
return executor.EnterRun();
}
catch (Exception ex)
{
try
{
Console.Error.WriteLine(
$"[LOADER][ERROR] Native guest worker prologue failed: {ex.GetType().Name}: {ex.Message}");
}
catch
{
}
return 0;
}
}
[UnmanagedCallersOnly]
private static void RunEpilogue(nint executorHandle, int nativeResult)
{
try
{
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
executor.ExitRun(nativeResult);
}
catch (Exception ex)
{
try
{
Console.Error.WriteLine(
$"[LOADER][ERROR] Native guest worker epilogue failed: {ex.GetType().Name}: {ex.Message}");
}
catch
{
}
}
}
private nint EnterRun()
{
var backend = _backend;
_prevBackend = _activeExecutionBackend;
_prevContext = _activeCpuContext;
_prevSentinel = _activeEntryReturnSentinelRip;
_prevReturnSlot = _activeGuestReturnSlotAddress;
_prevForcedExit = _activeForcedGuestExit;
_prevYieldRequested = _activeGuestThreadYieldRequested;
_prevYieldReason = _activeGuestThreadYieldReason;
_prevState = _activeGuestThreadState;
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
_entered = true;
_activeExecutionBackend = backend;
_activeCpuContext = _runContext;
_activeEntryReturnSentinelRip = _runSentinelRip;
_activeGuestReturnSlotAddress = _runReturnSlotAddress;
_activeForcedGuestExit = false;
_activeGuestThreadYieldRequested = false;
_activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!);
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (_runState is { } state)
{
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
}
if (_runAffinityMask != 0)
{
backend.ApplyGuestThreadAffinity(_runAffinityMask);
}
_runPrologueFailed = false;
if (LogThreadMode)
{
TraceThreadMode(
$"worker_enter guest=0x{_runGuestThreadHandle:X16} stub=0x{(ulong)_runEntryStub:X16}");
}
return _runEntryStub;
}
private void ExitRun(int nativeResult)
{
_runNativeResult = nativeResult;
_runYieldRequested = _activeGuestThreadYieldRequested;
_runYieldReason = _activeGuestThreadYieldReason;
_runForcedExit = _activeForcedGuestExit;
if (!_entered)
{
return;
}
_entered = false;
if (_runState is { } state)
{
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
}
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
_activeExecutionBackend = _prevBackend;
_activeCpuContext = _prevContext;
_activeEntryReturnSentinelRip = _prevSentinel;
_activeGuestReturnSlotAddress = _prevReturnSlot;
_activeForcedGuestExit = _prevForcedExit;
_activeGuestThreadYieldRequested = _prevYieldRequested;
_activeGuestThreadYieldReason = _prevYieldReason;
_activeGuestThreadState = _prevState;
_prevBackend = null;
_prevContext = null;
_prevState = null;
_prevYieldReason = null;
if (LogThreadMode)
{
TraceThreadMode(
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
}
}
public void Dispose()
{
if (_controlBlock != null)
{
*(int*)_controlBlock = 1;
}
try
{
_workAvailable.Set();
}
catch (ObjectDisposedException)
{
}
var exited = _threadHandle == 0;
if (_threadHandle != 0)
{
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
CloseHandle(_threadHandle);
_threadHandle = 0;
}
if (!exited)
{
// The worker is still parked in guest code (teardown should have unwound
// it first). Leak the stub, control block, events and GC handle rather
// than have the thread execute freed memory.
Console.Error.WriteLine(
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} did not stop; leaking its run loop.");
return;
}
if (_loopStub != null)
{
VirtualFree(_loopStub, 0u, 32768u);
_loopStub = null;
}
if (_controlBlock != null)
{
VirtualFree(_controlBlock, 0u, 32768u);
_controlBlock = null;
}
if (_selfHandle.IsAllocated)
{
_selfHandle.Free();
}
_workAvailable.Dispose();
_workCompleted.Dispose();
}
}
}
File diff suppressed because it is too large Load Diff
+41 -41
View File
@@ -10,11 +10,13 @@ using SharpEmu.Core;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Loader;
public sealed class SelfLoader : ISelfLoader
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
private const uint SelfMagic = 0x4F153D1D;
private const ulong SelfSegmentFlag = 0x800;
private const int PageSize = 0x1000;
@@ -155,7 +157,7 @@ public sealed class SelfLoader : ISelfLoader
}
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
Console.Error.WriteLine($"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
var loadContext = ParseLayout(imageData);
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
@@ -255,7 +257,7 @@ public sealed class SelfLoader : ISelfLoader
{
_nextTlsModuleId++;
}
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
return new SelfImage(
loadContext.IsSelf,
@@ -403,15 +405,15 @@ public sealed class SelfLoader : ISelfLoader
var virtualAddress = header.VirtualAddress + imageBase;
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
if (header.Alignment > 1)
{
var vaddrMod = virtualAddress % header.Alignment;
var offsetMod = header.Offset % header.Alignment;
if (vaddrMod != offsetMod)
{
Console.Error.WriteLine(
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
Log.Warning(
$"Segment {index} ELF alignment mismatch! " +
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
}
@@ -671,13 +673,13 @@ public sealed class SelfLoader : ISelfLoader
{
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
{
Console.Error.WriteLine(
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
Log.Debug(
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
}
else
{
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
}
}
@@ -689,8 +691,8 @@ public sealed class SelfLoader : ISelfLoader
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
{
Console.Error.WriteLine(
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
Log.Debug(
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
}
}
@@ -783,15 +785,15 @@ public sealed class SelfLoader : ISelfLoader
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Console.Error.WriteLine(
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
Log.Debug(
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
}
if (!IsSupportedRelocationType(relocation.Type))
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
}
continue;
}
@@ -800,7 +802,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
Log.Debug("[FOCUS][SKIP] target address not mapped");
}
continue;
}
@@ -840,7 +842,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
}
continue;
}
@@ -852,10 +854,9 @@ public sealed class SelfLoader : ISelfLoader
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Console.Error.WriteLine(
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
Log.Warning(
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
continue;
}
descriptors.Add(new RelocationDescriptor(
@@ -873,10 +874,9 @@ public sealed class SelfLoader : ISelfLoader
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Console.Error.WriteLine(
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
Log.Warning(
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
continue;
}
descriptors.Add(new RelocationDescriptor(
@@ -893,7 +893,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
}
continue;
}
@@ -902,7 +902,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
}
continue;
}
@@ -950,8 +950,8 @@ public sealed class SelfLoader : ISelfLoader
if (sectionSymbols > 0 || dynamicSymbols > 0)
{
Console.Error.WriteLine(
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
Log.Info(
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
}
}
@@ -1025,8 +1025,8 @@ public sealed class SelfLoader : ISelfLoader
if (preInitializers.Count != 0 || initializers.Count != 0)
{
Console.Error.WriteLine(
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
Log.Info(
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
}
}
@@ -1409,8 +1409,8 @@ public sealed class SelfLoader : ISelfLoader
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
Console.Error.WriteLine(
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
Log.Debug(
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
if (mapSize > int.MaxValue)
{
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
@@ -1842,16 +1842,16 @@ public sealed class SelfLoader : ISelfLoader
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
var guestAddr = location + imageBase;
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
if (virtualMemory.TryRead(guestAddr, tableBytes))
{
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
return true;
}
if (virtualMemory.TryRead(location, tableBytes))
{
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
return true;
}
@@ -1859,11 +1859,11 @@ public sealed class SelfLoader : ISelfLoader
{
var slice = elfData.Slice((int)location, (int)size);
tableBytes = slice.ToArray();
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
return true;
}
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
tableBytes = Array.Empty<byte>();
return false;
}
@@ -1920,17 +1920,17 @@ public sealed class SelfLoader : ISelfLoader
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
{
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
if (address == 0)
{
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
Log.Debug("-> return 0 (null)");
return 0;
}
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
{
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
Log.Debug($"-> resolved raw 0x{resolved:X16}");
return resolved;
}
@@ -1939,18 +1939,18 @@ public sealed class SelfLoader : ISelfLoader
var rebased = address + imageBase;
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
{
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
return resolvedRebased;
}
}
if (address < 0x10000)
{
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
Log.Debug($"-> reject small 0x{address:X}");
return 0;
}
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
Log.Debug($"-> fallback raw 0x{address:X}");
return address;
}
+1 -2
View File
@@ -83,7 +83,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
};
var moduleManager = new ModuleManager();
moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.Freeze();
@@ -140,7 +139,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
+4 -1
View File
@@ -65,7 +65,10 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
+28
View File
@@ -127,6 +127,34 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Top-level page switcher (Library / Options): plain transparent
buttons, not TabItem, so there is no Fluent selected-tab underline.
The active page is conveyed by brightness alone; LB/RB gamepad
hints flank the pair. -->
<Style Selector="Button.segment">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
<Setter Property="FontSize" Value="22" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Padding" Value="6,4" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.segment:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="Button.segment.active">
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Gamepad shoulder-button hint chip (LB/RB, L1/R1). -->
<Style Selector="Border.padHint">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="8,3" />
</Style>
<Style Selector="ContextMenu">
<Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
+162
View File
@@ -0,0 +1,162 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using System.Collections.Specialized;
namespace SharpEmu.GUI;
public sealed class ConsoleWindow : Window
{
private readonly AvaloniaList<LogLine> _sourceLines;
private readonly AvaloniaList<LogLine> _visibleLines = new();
private readonly ListBox _list;
private readonly TextBox _searchBox;
private readonly CheckBox _autoScrollCheck;
public ConsoleWindow(
AvaloniaList<LogLine> lines,
Action clear,
bool autoScroll)
{
var loc = Localization.Instance;
_sourceLines = lines;
Title = loc.Get("Console.WindowTitle");
Width = 980;
Height = 620;
MinWidth = 520;
MinHeight = 320;
Background = new SolidColorBrush(Color.Parse("#0D1017"));
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
_searchBox = new TextBox
{
Watermark = loc.Get("Console.SearchWatermark"),
Width = 320,
Margin = new Thickness(0, 0, 12, 0),
};
_autoScrollCheck = new CheckBox
{
Content = loc.Get("Console.AutoScroll"),
IsChecked = autoScroll,
FontSize = 12,
Margin = new Thickness(0, 0, 12, 0),
VerticalAlignment = VerticalAlignment.Center,
};
var copyButton = new Button
{
Classes = { "ghost" },
Content = loc.Get("Console.Copy"),
Padding = new Thickness(10, 4),
Margin = new Thickness(0, 0, 8, 0),
};
var clearButton = new Button
{
Classes = { "ghost" },
Content = loc.Get("Console.Clear"),
Padding = new Thickness(10, 4),
};
copyButton.Click += async (_, _) => await CopyAsync();
clearButton.Click += (_, _) => clear();
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
_list = new ListBox
{
Classes = { "console" },
ItemsSource = _visibleLines,
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
{
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
return text;
}),
};
Content = new Grid
{
Margin = new Thickness(12),
RowDefinitions = new RowDefinitions("Auto,*"),
Children =
{
new Grid
{
Margin = new Thickness(0, 0, 0, 8),
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
Children =
{
new TextBlock
{
Classes = { "sectionTitle" },
Text = loc.Get("Console.Title"),
VerticalAlignment = VerticalAlignment.Center,
},
_searchBox.WithGridColumn(1),
_autoScrollCheck.WithGridColumn(2),
copyButton.WithGridColumn(3),
clearButton.WithGridColumn(4),
},
},
_list.WithGridRow(1),
},
};
lines.CollectionChanged += OnLinesChanged;
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
RefreshVisibleLines();
}
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshVisibleLines();
if (_autoScrollCheck.IsChecked == true)
{
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
}
}
private void RefreshVisibleLines()
{
var query = _searchBox.Text ?? string.Empty;
_visibleLines.Clear();
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
? _sourceLines
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
}
private async Task CopyAsync()
{
if (_visibleLines.Count == 0 || Clipboard is null)
{
return;
}
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
}
}
file static class GridExtensions
{
public static T WithGridColumn<T>(this T control, int column) where T : Control
{
Grid.SetColumn(control, column);
return control;
}
public static T WithGridRow<T>(this T control, int row) where T : Control
{
Grid.SetRow(control, row);
return control;
}
}
+252
View File
@@ -0,0 +1,252 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
namespace SharpEmu.GUI;
/// <summary>
/// Minimal Discord Rich Presence client. Speaks Discord's local IPC
/// protocol (framed JSON over the discord-ipc-N named pipe) directly, so no
/// external dependency is needed. All failures are silent: no Discord, no
/// presence, no impact on the launcher.
/// </summary>
public sealed class DiscordRichPresence : IDisposable
{
private const int OpHandshake = 0;
private const int OpFrame = 1;
private const int MaxFrameBytes = 64 * 1024;
private static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
};
private readonly object _gate = new();
private readonly string _clientId;
private readonly AutoResetEvent _signal = new(false);
private readonly Thread _worker;
private NamedPipeClientStream? _pipe;
private PresenceState? _desired;
private PresenceState? _sent;
private volatile bool _disposed;
private sealed record PresenceState(string Details, string? State, long? StartUnixSeconds);
public DiscordRichPresence(string clientId)
{
_clientId = clientId;
_worker = new Thread(WorkerLoop)
{
IsBackground = true,
Name = "DiscordRichPresence",
};
_worker.Start();
}
/// <summary>
/// Requests the given presence; the worker applies it asynchronously and
/// retries (with reconnection) until it is delivered or replaced.
/// </summary>
public void SetPresence(string details, string? state = null, long? startUnixSeconds = null)
{
lock (_gate)
{
_desired = new PresenceState(details, state, startUnixSeconds);
}
Trace($"queued details=\"{details}\" state=\"{state}\"");
_signal.Set();
}
public void Dispose()
{
_disposed = true;
_signal.Set();
ClosePipe();
}
private void WorkerLoop()
{
while (!_disposed)
{
// Woken immediately on presence changes; the periodic tick
// retries after connection failures (e.g. Discord starts later).
_signal.WaitOne(TimeSpan.FromSeconds(15));
if (_disposed || _clientId.Length == 0)
{
continue;
}
PresenceState? desired;
lock (_gate)
{
desired = _desired;
}
if (desired is null || desired == _sent)
{
continue;
}
try
{
EnsureConnected();
SendActivity(desired);
_sent = desired;
Trace($"sent details=\"{desired.Details}\" state=\"{desired.State}\"");
}
catch (Exception exception)
{
// Connection died (or never existed); reconnect on a later tick.
Trace($"send failed: {exception.Message}");
ClosePipe();
_sent = null;
}
}
}
private void EnsureConnected()
{
if (_pipe is { IsConnected: true })
{
return;
}
ClosePipe();
Exception? lastError = null;
for (var index = 0; index < 10; index++)
{
var pipe = new NamedPipeClientStream(
".",
$"discord-ipc-{index}",
PipeDirection.InOut,
PipeOptions.Asynchronous);
try
{
pipe.Connect(500);
_pipe = pipe;
WriteFrame(OpHandshake, JsonSerializer.Serialize(new
{
v = 1,
client_id = _clientId,
}));
// Discord replies with a READY (or error) frame; consume it
// so the pipe buffer stays drained.
_ = ReadFrame();
return;
}
catch (Exception exception)
{
lastError = exception;
pipe.Dispose();
_pipe = null;
}
}
throw lastError ?? new IOException("Discord IPC pipe not found.");
}
private void SendActivity(PresenceState presence)
{
var payload = new Dictionary<string, object?>
{
["cmd"] = "SET_ACTIVITY",
["nonce"] = Guid.NewGuid().ToString(),
["args"] = new Dictionary<string, object?>
{
["pid"] = Environment.ProcessId,
["activity"] = new Dictionary<string, object?>
{
["details"] = presence.Details,
["state"] = presence.State,
["timestamps"] = presence.StartUnixSeconds is { } start
? new Dictionary<string, object?> { ["start"] = start }
: null,
["assets"] = new Dictionary<string, object?>
{
["large_image"] = "logo",
["large_text"] = "SharpEmu",
},
},
},
};
WriteFrame(OpFrame, JsonSerializer.Serialize(payload, JsonOptions));
// Consume Discord's acknowledgement to keep the pipe drained.
_ = ReadFrame();
}
private void WriteFrame(int opcode, string json)
{
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
var payload = Encoding.UTF8.GetBytes(json);
var frame = new byte[8 + payload.Length];
BitConverter.TryWriteBytes(frame.AsSpan(0, 4), opcode);
BitConverter.TryWriteBytes(frame.AsSpan(4, 4), payload.Length);
payload.CopyTo(frame, 8);
pipe.Write(frame, 0, frame.Length);
pipe.Flush();
}
private string ReadFrame()
{
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
var header = ReadExactly(pipe, 8);
var length = BitConverter.ToInt32(header, 4);
if (length is < 0 or > MaxFrameBytes)
{
throw new IOException($"Discord IPC frame length {length} is out of range.");
}
return Encoding.UTF8.GetString(ReadExactly(pipe, length));
}
private static byte[] ReadExactly(NamedPipeClientStream pipe, int count)
{
var buffer = new byte[count];
var read = 0;
while (read < count)
{
var chunk = pipe.Read(buffer, read, count - read);
if (chunk <= 0)
{
throw new IOException("Discord IPC pipe closed.");
}
read += chunk;
}
return buffer;
}
private static void Trace(string message)
{
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCORD"),
"1",
StringComparison.Ordinal))
{
Console.Error.WriteLine($"[DISCORD] {message}");
}
}
private void ClosePipe()
{
var pipe = _pipe;
_pipe = null;
try
{
pipe?.Dispose();
}
catch (Exception)
{
// Best effort; the pipe may already be broken.
}
}
}
+14 -22
View File
@@ -85,6 +85,14 @@ internal sealed class EmulatorProcess : IDisposable
return;
}
// Prefer terminating the job: it kills the whole tree, including
// any children the emulator spawned, even when the main process
// is wedged in a GPU driver call.
if (_jobHandle != 0)
{
_ = TerminateJobObject(_jobHandle, 1);
}
if (_processHandle != 0)
{
_ = TerminateProcess(_processHandle, 1);
@@ -160,8 +168,7 @@ internal sealed class EmulatorProcess : IDisposable
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
var policy2 =
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -192,29 +199,10 @@ internal sealed class EmulatorProcess : IDisposable
ref startupInfoEx,
out var processInfo);
if (!created)
{
// Some mitigation policy bits (e.g. XFG) are not supported on
// older Windows builds. Mirror the CLI's behavior and fall back
// to launching without the mitigation attribute list.
startupInfoEx.lpAttributeList = 0;
created = CreateProcessW(
exePath,
new StringBuilder(BuildCommandLine(exePath, arguments)),
0,
0,
true,
CREATE_NO_WINDOW,
0,
currentDirectory,
ref startupInfoEx,
out processInfo);
}
if (!created)
{
var error = Marshal.GetLastWin32Error();
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message}).");
throw new Win32Exception(error, $"Failed to start '{exePath}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
}
CloseHandle(processInfo.hThread);
@@ -639,6 +627,10 @@ internal sealed class EmulatorProcess : IDisposable
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool TerminateProcess(nint process, uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool TerminateJobObject(nint job, uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(nint handle);
+16 -5
View File
@@ -29,10 +29,12 @@ public sealed class GameEntry : INotifyPropertyChanged
private long _sizeBytes;
public GameEntry(
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
string name, string? titleId, string? version, string path, long sizeBytes,
string? coverPath, string? backgroundPath)
{
Name = name;
TitleId = titleId;
Version = version;
Path = path;
_sizeBytes = sizeBytes;
CoverPath = coverPath;
@@ -46,6 +48,9 @@ public sealed class GameEntry : INotifyPropertyChanged
public string? TitleId { get; }
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
public string? Version { get; }
public string Path { get; }
/// <summary>
@@ -65,7 +70,7 @@ public sealed class GameEntry : INotifyPropertyChanged
_sizeBytes = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeText)));
}
}
@@ -106,9 +111,15 @@ public sealed class GameEntry : INotifyPropertyChanged
public bool HasCover => _cover is not null;
public string Detail => TitleId is not null
? $"{TitleId} • {FormatSize(SizeBytes)}"
: FormatSize(SizeBytes);
public bool HasTitleId => TitleId is not null;
/// <summary>Badge text shown in the launch bar, e.g. "v01.000.000".</summary>
public string? VersionText => Version is null ? null : $"v{Version}";
public bool HasVersion => Version is not null;
/// <summary>Formatted install size badge shown in the launch bar.</summary>
public string SizeText => FormatSize(SizeBytes);
private static string ComputeInitials(string name)
{
+25 -1
View File
@@ -23,14 +23,38 @@ public sealed class GuiSettings
public bool StrictDynlibResolution { get; set; }
/// <summary>Mirror emulator output to user/logs/&lt;titleId&gt;-&lt;timestamp&gt;.log.</summary>
/// <summary>
/// Mirror emulator output to user/logs/&lt;titleId&gt;-&lt;timestamp&gt;.log, if <see cref="LogFilePath"/> is null.
/// </summary>
public bool LogToFile { get; set; }
/// <summary>If <see cref="LogToFile"/> is true it logs to this file path.</summary>
public string? LogFilePath { get; set; }
/// <summary>
/// If <see cref="OverrideLogFile"/> is false it appends &lt;titleId&gt;-&lt;timestamp&gt; to the filename specified by
/// <see cref="LogFilePath"/>. Otherwise it uses the exact filename from <see cref="LogFilePath"/>
/// </summary>
public bool OverrideLogFile { get; set; }
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
public bool PlayTitleMusic { get; set; } = true;
public string? EmulatorPath { get; set; }
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
public string Language { get; set; } = "en";
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
public bool DiscordRichPresence { get; set; } = true;
/// <summary>
/// Discord application ID used for Rich Presence; the default is the
/// SharpEmu application. Override to rebrand what Discord shows as
/// "Playing …" (register at discord.com/developers/applications).
/// </summary>
public string DiscordClientId { get; set; } = "1525606762248540221";
// The emulator is portable and keeps its data next to the executable;
// the GUI follows the same convention.
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "English",
"Page.Library": "Library",
"Page.Options": "Options",
"Page.GameCount.One": "1 game",
"Page.GameCount.Other": "{0} games",
"Library.SearchWatermark": "Search library…",
"Library.AddFolder": " Add folder",
"Library.Rescan": "⟳ Rescan",
"Library.OpenFile": "Open file…",
"Library.Context.Launch": "Launch",
"Library.Context.OpenFolder": "Open game folder",
"Library.Context.CopyPath": "Copy path",
"Library.Context.CopyTitleId": "Copy title ID",
"Library.Context.Remove": "Remove from library",
"Library.Empty.Title": "Your library is empty",
"Library.Empty.Hint": "Add a folder containing your games to get started.",
"Library.Empty.SearchTitle": "No games match your search",
"Library.Empty.SearchHint": "Nothing in the library matches “{0}”.",
"Library.Empty.AddFolder": " Add game folder",
"Library.Loading": "Loading library…",
"Options.General": "General",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Strict dynlib resolution",
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
"Options.LogLevel.Label": "Log level",
"Options.LogLevel.Desc": "Verbosity of the emulator console output.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Trace the first N imports per module (0 = off).",
"Options.LogToFile.Label": "Log to file",
"Options.LogToFile.Desc": "Mirror emulator output to a log file.",
"Options.LogFilePath.Label": "Log file path",
"Options.LogFilePath.Default": "No custom path — logs go to user/logs next to the emulator.",
"Options.LogFilePath.Select": "Select…",
"Options.OverrideLogFile.Label": "Override log file",
"Options.OverrideLogFile.Desc": "Use the exact file path instead of appending title ID and timestamp.",
"Options.TitleMusic.Label": "Title music",
"Options.TitleMusic.Desc": "Loop the selected game's preview music in the library.",
"Options.Discord.Label": "Discord presence",
"Options.Discord.Desc": "Show the running game on your Discord profile.",
"Options.Language.Label": "Emulator language",
"Options.Language.Desc": "Language used throughout the launcher. Applies immediately.",
"Common.On": "On",
"Common.Off": "Off",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Search...",
"Console.AutoScroll": "Auto-scroll",
"Console.Split": "Split",
"Console.Copy": "Copy",
"Console.Clear": "Clear",
"Console.WindowTitle": "SharpEmu Console",
"Launch.NoGameSelected": "No game selected",
"Launch.NoGameHint": "Pick a game from the library, or open an eboot.bin directly.",
"Launch.Idle": "Idle",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Launch",
"Launch.Stop": "■ Stop",
"Launch.Running": "Running — {0}",
"Launch.Stopping": "Stopping…",
"Launch.Exited": "Exited with code {0} ({1})",
"Launch.ExeNotFound": "SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).",
"Launch.LogFile": "Log file: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Failed to start the emulator: {0}",
"Launch.ProcessExited": "Process exited with code {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "invalid arguments",
"Exit.EbootNotFound": "eboot not found",
"Exit.RuntimeException": "runtime exception",
"Exit.EmulationError": "emulation error",
"Exit.Unknown": "unknown",
"Status.EmulatorLocating": "Emulator: locating…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.",
"Status.ScanningLibrary": "Scanning library…",
"Status.AddFolderPrompt": "Add a game folder to populate the library.",
"Status.LibraryScanned": "Library scanned: {0} game(s) in {1} folder(s).",
"Status.CouldNotOpenFolder": "Could not open folder: {0}",
"Status.CopiedToClipboard": "{0} copied to clipboard.",
"Status.RemovedFromLibrary": "Removed “{0}” from the library. Re-add its folder to restore it.",
"Status.Running": "Running {0}",
"Status.Stopping": "Stopping…",
"Status.Idle": "Idle",
"Clipboard.Path": "Path",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Playing {0}",
"Discord.Browsing": "Browsing the library",
"Dialog.ChooseGameFolder": "Choose a folder containing games",
"Dialog.OpenExecutable": "Open an executable to launch",
"Dialog.PsExecutables": "PS executables",
"Dialog.SaveLogFile": "Select where to save the Log file",
"Dialog.PlainTextFiles": "Plain Text Files",
"Dialog.LogFiles": "Log Files"
}
+134
View File
@@ -0,0 +1,134 @@
{
"_languageName": "Italiano",
"Page.Library": "Libreria",
"Page.Options": "Opzioni",
"Page.GameCount.One": "1 gioco",
"Page.GameCount.Other": "{0} giochi",
"Library.SearchWatermark": "Cerca nella libreria…",
"Library.AddFolder": " Aggiungi cartella",
"Library.Rescan": "⟳ Riscansiona",
"Library.OpenFile": "Apri file…",
"Library.Context.Launch": "Avvia",
"Library.Context.OpenFolder": "Apri cartella gioco",
"Library.Context.CopyPath": "Copia percorso",
"Library.Context.CopyTitleId": "Copia ID titolo",
"Library.Context.Remove": "Rimuovi dalla libreria",
"Library.Empty.Title": "La tua libreria è vuota",
"Library.Empty.Hint": "Aggiungi la cartella che contiene i tuoi giochi per partire.",
"Library.Empty.SearchTitle": "Nessun gioco corrisponde alla ricerca",
"Library.Empty.SearchHint": "Nulla nella libreria corrisponde a “{0}”.",
"Library.Empty.AddFolder": " Aggiungi cartella giochi",
"Library.Loading": "Caricamento libreria…",
"Options.General": "Generale",
"Options.Section.Emulation": "EMULAZIONE",
"Options.Section.Logging": "LOG",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Motore di esecuzione utilizzato per eseguire il codice del gioco.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Risoluzione rigorosa dynlib",
"Options.Strict.Desc": "Interrompi l'avvio quando un simbolo importato non può essere trovato.",
"Options.LogLevel.Label": "Livello Log",
"Options.LogLevel.Desc": "Livello di dettaglio dell'output console dell'emulatore.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Limite tracciamento import",
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
"Options.LogToFile.Label": "Salva log su file",
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
"Options.LogFilePath.Label": "Percorso file di log",
"Options.LogFilePath.Default": "Nessun percorso personalizzato — i log vengono salvati in user/logs accanto all'emulatore.",
"Options.LogFilePath.Select": "Seleziona…",
"Options.OverrideLogFile.Label": "Sovrascrivi file di log",
"Options.OverrideLogFile.Desc": "Usa esattamente il percorso specificato invece di aggiungere Title ID e timestamp.",
"Options.TitleMusic.Label": "Musica del titolo",
"Options.TitleMusic.Desc": "Riproduci in loop la musica di anteprima del gioco selezionato nella libreria.",
"Options.Discord.Label": "Presenza Discord",
"Options.Discord.Desc": "Mostra il gioco in esecuzione sul tuo profilo Discord.",
"Options.Language.Label": "Lingua dell'emulatore",
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
"Common.On": "On",
"Common.Off": "Off",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Cerca...",
"Console.AutoScroll": "Scorrimento automatico",
"Console.Split": "Dividi",
"Console.Copy": "Copia",
"Console.Clear": "Cancella",
"Console.WindowTitle": "Console SharpEmu",
"Launch.NoGameSelected": "Nessun gioco selezionato",
"Launch.NoGameHint": "Scegli un gioco dalla libreria, oppure apri direttamente un eboot.bin.",
"Launch.Idle": "Inattivo",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Avvia",
"Launch.Stop": "■ Ferma",
"Launch.Running": "In esecuzione — {0}",
"Launch.Stopping": "Arresto in corso…",
"Launch.Exited": "Terminato con codice {0} ({1})",
"Launch.ExeNotFound": "Eseguibile SharpEmu non trovato. Compila prima il progetto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "File di log: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Impossibile avviare l'emulatore: {0}",
"Launch.ProcessExited": "Processo terminato con codice {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argomenti non validi",
"Exit.EbootNotFound": "eboot non trovato",
"Exit.RuntimeException": "errore di runtime",
"Exit.EmulationError": "errore di emulazione",
"Exit.Unknown": "sconosciuto",
"Status.EmulatorLocating": "Emulatore: ricerca in corso…",
"Status.EmulatorPath": "Emulatore: {0}",
"Status.EmulatorNotFound": "Emulatore: eseguibile SharpEmu non trovato — compila prima SharpEmu.CLI.",
"Status.ScanningLibrary": "Scansione della libreria…",
"Status.AddFolderPrompt": "Aggiungi una cartella di giochi per popolare la libreria.",
"Status.LibraryScanned": "Libreria scansionata: {0} gioco/giochi in {1} cartella/e.",
"Status.CouldNotOpenFolder": "Impossibile aprire la cartella: {0}",
"Status.CopiedToClipboard": "{0} copiato negli appunti.",
"Status.RemovedFromLibrary": "“{0}” rimosso dalla libreria. Riaggiungi la sua cartella per ripristinarlo.",
"Status.Running": "In esecuzione: {0}",
"Status.Stopping": "Arresto in corso…",
"Status.Idle": "Inattivo",
"Clipboard.Path": "Percorso",
"Clipboard.TitleId": "ID Titolo",
"Discord.Playing": "Sta giocando a {0}",
"Discord.Browsing": "Sta esplorando la libreria",
"Dialog.ChooseGameFolder": "Scegli una cartella contenente giochi",
"Dialog.OpenExecutable": "Apri un eseguibile da avviare",
"Dialog.PsExecutables": "Eseguibili PS",
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
"Dialog.PlainTextFiles": "File di testo semplice",
"Dialog.LogFiles": "File di log"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "Русский",
"Page.Library": "Библиотека",
"Page.Options": "Настройки",
"Page.GameCount.One": "1 игра",
"Page.GameCount.Other": "Игр: {0}",
"Library.SearchWatermark": "Поиск…",
"Library.AddFolder": "+ Добавить папку",
"Library.Rescan": "⟳ Сканировать",
"Library.OpenFile": "Открыть файл…",
"Library.Context.Launch": "Запустить",
"Library.Context.OpenFolder": "Открыть папку с игрой",
"Library.Context.CopyPath": "Скопировать путь",
"Library.Context.CopyTitleId": "Скопировать ID игры",
"Library.Context.Remove": "Удалить из библиотеки",
"Library.Empty.Title": "Ваша библиотека пуста",
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
"Library.Empty.AddFolder": "+ Добавить папку с играми",
"Library.Loading": "Загрузка библиотеки…",
"Options.General": "Основные",
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
"Options.Section.Launcher": "ЛАУНЧЕР",
"Options.CpuEngine.Label": "Движок ЦП",
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
"Options.CpuEngine.Native": "Нативный",
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
"Options.LogLevel.Label": "Уровень логгирования",
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Лимит трассировки импортов",
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
"Options.LogToFile.Label": "Запись лога в файл",
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
"Options.LogFilePath.Label": "Путь к файлу лога",
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
"Options.LogFilePath.Select": "Выбрать…",
"Options.OverrideLogFile.Label": "Переопределить файл лога",
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
"Options.TitleMusic.Label": "Музыка игры",
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
"Options.Discord.Label": "Статус Discord",
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
"Options.Language.Label": "Язык эмулятора",
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
"Common.On": "Включено",
"Common.Off": "Выключено",
"Console.Title": "КОНСОЛЬ",
"Console.SearchWatermark": "Поиск...",
"Console.AutoScroll": "Авто-прокрутка",
"Console.Split": "Разделить",
"Console.Copy": "Скопировать",
"Console.Clear": "Очистить",
"Console.WindowTitle": "Консоль SharpEmu",
"Launch.NoGameSelected": "Ничего не выбрано",
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
"Launch.Idle": "Ожидание",
"Launch.Console": "≡ Консоль",
"Launch.Launch": "▶ Запустить",
"Launch.Stop": "■ Остановить",
"Launch.Running": "Запущено - {0}",
"Launch.Stopping": "Остановка…",
"Launch.Exited": "Завершено с кодом {0} ({1})",
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Лог: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "некорректные аргументы",
"Exit.EbootNotFound": "eboot не найден",
"Exit.RuntimeException": "ошибка выполнения",
"Exit.EmulationError": "ошибка эмуляции",
"Exit.Unknown": "неизвестная ошибка",
"Status.EmulatorLocating": "Эмулятор: поиск…",
"Status.EmulatorPath": "Эмулятор: {0}",
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
"Status.ScanningLibrary": "Сканирование библиотеки…",
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
"Status.Running": "Запущено: {0}",
"Status.Stopping": "Остановка…",
"Status.Idle": "Ожидание",
"Clipboard.Path": "Путь",
"Clipboard.TitleId": "ID игры",
"Discord.Playing": "Играет в {0}",
"Discord.Browsing": "Просматривает библиотеку",
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
"Dialog.PsExecutables": "Исполняемые файлы PS",
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
"Dialog.PlainTextFiles": "Текстовые файлы",
"Dialog.LogFiles": "Логи"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "Türkçe",
"Page.Library": "Kütüphane",
"Page.Options": "Ayarlar",
"Page.GameCount.One": "1 oyun",
"Page.GameCount.Other": "{0} oyun",
"Library.SearchWatermark": "Kütüphanede ara…",
"Library.AddFolder": " Klasör ekle",
"Library.Rescan": "⟳ Yeniden tara",
"Library.OpenFile": "Dosya aç…",
"Library.Context.Launch": "Başlat",
"Library.Context.OpenFolder": "Oyun klasörünü aç",
"Library.Context.CopyPath": "Yolu kopyala",
"Library.Context.CopyTitleId": "Title ID'yi kopyala",
"Library.Context.Remove": "Kütüphaneden kaldır",
"Library.Empty.Title": "Kütüphaneniz boş",
"Library.Empty.Hint": "Başlamak için oyunlarınızı içeren bir klasör ekleyin.",
"Library.Empty.SearchTitle": "Aramanızla eşleşen oyun yok",
"Library.Empty.SearchHint": "Kütüphanede “{0}” ile eşleşen bir şey yok.",
"Library.Empty.AddFolder": " Oyun klasörü ekle",
"Library.Loading": "Kütüphane yükleniyor…",
"Options.General": "Genel",
"Options.Section.Emulation": "EMÜLASYON",
"Options.Section.Logging": "GÜNLÜKLEME",
"Options.Section.Launcher": "BAŞLATICI",
"Options.CpuEngine.Label": "CPU motoru",
"Options.CpuEngine.Desc": "Oyun kodunu çalıştırmak için kullanılan yürütme motoru.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Katı dynlib çözümü",
"Options.Strict.Desc": "İçe aktarılan bir sembol çözümlenemediğinde başlatmayı başarısız kılar.",
"Options.LogLevel.Label": "Günlük seviyesi",
"Options.LogLevel.Desc": "Emülatör konsol çıktısının ayrıntı düzeyi.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Import izleme sınırı",
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
"Options.LogToFile.Label": "Dosyaya günlükle",
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
"Options.LogFilePath.Label": "Günlük dosyası yolu",
"Options.LogFilePath.Default": "Özel yol yok — günlükler emülatörün yanındaki user/logs klasörüne yazılır.",
"Options.LogFilePath.Select": "Seç…",
"Options.OverrideLogFile.Label": "Günlük dosyasının üzerine yaz",
"Options.OverrideLogFile.Desc": "Title ID ve zaman damgası eklemek yerine tam dosya yolunu kullan.",
"Options.TitleMusic.Label": "Oyun müziği",
"Options.TitleMusic.Desc": "Kütüphanede seçili oyunun önizleme müziğini döngüye al.",
"Options.Discord.Label": "Discord durumu",
"Options.Discord.Desc": "Discord profilinde çalışan oyunu göster.",
"Options.Language.Label": "Emülatör dili",
"Options.Language.Desc": "Başlatıcı genelinde kullanılan dil. Hemen uygulanır.",
"Common.On": "Açık",
"Common.Off": "Kapalı",
"Console.Title": "KONSOL",
"Console.SearchWatermark": "Ara...",
"Console.AutoScroll": "Otomatik kaydır",
"Console.Split": "Ayır",
"Console.Copy": "Kopyala",
"Console.Clear": "Temizle",
"Console.WindowTitle": "SharpEmu Konsol",
"Launch.NoGameSelected": "Oyun seçilmedi",
"Launch.NoGameHint": "Kütüphaneden bir oyun seçin veya doğrudan bir eboot.bin açın.",
"Launch.Idle": "Boşta",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Başlat",
"Launch.Stop": "■ Durdur",
"Launch.Running": "Çalışıyor — {0}",
"Launch.Stopping": "Durduruluyor…",
"Launch.Exited": "Çıkış kodu {0} ({1})",
"Launch.ExeNotFound": "SharpEmu çalıştırılabilir dosyası bulunamadı. Önce SharpEmu.CLI projesini derleyin (dotnet build).",
"Launch.LogFile": "Günlük dosyası: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emülatör başlatılamadı: {0}",
"Launch.ProcessExited": "İşlem {0} kodla sonlandı ({1}).",
"Exit.Ok": "Tamam",
"Exit.InvalidArguments": "geçersiz argümanlar",
"Exit.EbootNotFound": "eboot bulunamadı",
"Exit.RuntimeException": "çalışma zamanı hatası",
"Exit.EmulationError": "emülasyon hatası",
"Exit.Unknown": "bilinmiyor",
"Status.EmulatorLocating": "Emülatör: bulunuyor…",
"Status.EmulatorPath": "Emülatör: {0}",
"Status.EmulatorNotFound": "Emülatör: SharpEmu çalıştırılabilir dosyası bulunamadı — önce SharpEmu.CLI'yi derleyin.",
"Status.ScanningLibrary": "Kütüphane taranıyor…",
"Status.AddFolderPrompt": "Kütüphaneyi doldurmak için bir oyun klasörü ekleyin.",
"Status.LibraryScanned": "Kütüphane tarandı: {1} klasörde {0} oyun.",
"Status.CouldNotOpenFolder": "Klasör açılamadı: {0}",
"Status.CopiedToClipboard": "{0} panoya kopyalandı.",
"Status.RemovedFromLibrary": "“{0}” kütüphaneden kaldırıldı. Geri getirmek için klasörünü yeniden ekleyin.",
"Status.Running": "{0} çalışıyor",
"Status.Stopping": "Durduruluyor…",
"Status.Idle": "Boşta",
"Clipboard.Path": "Yol",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "{0} oynuyor",
"Discord.Browsing": "Kütüphaneye göz atıyor",
"Dialog.ChooseGameFolder": "Oyunlarınızı içeren bir klasör seçin",
"Dialog.OpenExecutable": "Başlatılacak bir çalıştırılabilir dosya aç",
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
"Dialog.LogFiles": "Günlük Dosyaları"
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.GUI;
/// <summary>
/// Loads UI strings for the launcher. Every language ships embedded in the
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
/// self-contained; an optional Languages/&lt;code&gt;.json file next to the
/// executable overrides the embedded copy for that code, so a translation
/// fix or a brand-new language never needs a rebuild.
/// </summary>
public sealed class Localization
{
public static Localization Instance { get; } = new();
public sealed record LanguageInfo(string Code, string NativeName);
private const string EmbeddedResourcePrefix = "Languages.";
private const string EmbeddedResourceSuffix = ".json";
private Dictionary<string, string> _strings = new();
private Localization()
{
}
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
public string CurrentCode { get; private set; } = "en";
public string Get(string key) => _strings.TryGetValue(key, out var value) ? value : key;
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
/// <summary>
/// Languages available either embedded in the binary or as a loose
/// override file, sorted by code. A loose file's declared name wins when
/// the same code exists in both places.
/// </summary>
public List<LanguageInfo> DiscoverLanguages()
{
var languages = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var code in EmbeddedLanguageCodes())
{
using var stream = OpenEmbeddedLanguageStream(code);
if (stream is not null)
{
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
}
}
try
{
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
{
var code = Path.GetFileNameWithoutExtension(file);
using var stream = File.OpenRead(file);
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
}
}
catch (Exception)
{
// No loose Languages directory: the embedded languages still stand.
}
var result = languages.Values.ToList();
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
return result;
}
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
public void Load(string code)
{
if (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
{
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
}
}
private static IEnumerable<string> EmbeddedLanguageCodes()
{
foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames())
{
if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) &&
name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal))
{
yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length];
}
}
}
private static Stream? OpenEmbeddedLanguageStream(string code) =>
typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}");
private static string? ReadLanguageName(Stream stream)
{
try
{
using var document = JsonDocument.Parse(stream);
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
name.ValueKind == JsonValueKind.String)
{
return name.GetString();
}
}
catch (Exception)
{
// Malformed file: fall back to the code as its own display name.
}
return null;
}
private bool TryLoadLooseFile(string code)
{
try
{
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
}
catch (Exception)
{
return false;
}
}
private bool TryLoadEmbedded(string code)
{
try
{
using var stream = OpenEmbeddedLanguageStream(code);
if (stream is null)
{
return false;
}
using var reader = new StreamReader(stream);
return TryLoad(code, reader.ReadToEnd());
}
catch (Exception)
{
return false;
}
}
private bool TryLoad(string code, string json)
{
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
if (loaded is null)
{
return false;
}
_strings = loaded;
CurrentCode = code;
return true;
}
}
+309 -150
View File
@@ -9,14 +9,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
Title="SharpEmu"
Width="1280" Height="820"
MinWidth="980" MinHeight="640"
WindowState="Maximized"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgBrush}"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="PreferSystemChrome"
ExtendClientAreaTitleBarHeightHint="44"
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
KeyDown="OnKeyDown">
<Grid RowDefinitions="44,*,32">
<Grid RowDefinitions="Auto,*,Auto">
<!-- Selected-game backdrop: key art behind the main content, dimmed by
a scrim so tiles and text stay readable. Fades on selection. -->
@@ -40,8 +42,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Border>
</Panel>
<!-- Title bar -->
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
the game gets the whole screen. -->
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
RenderOptions.BitmapInterpolationMode="HighQuality" />
@@ -53,119 +56,290 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
<!-- Main content -->
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
<Grid Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto,Auto">
<!-- Library toolbar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
</Border>
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
VerticalAlignment="Center" Margin="0,0,12,0" />
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content=" Add folder"
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
VerticalAlignment="Center" />
<!-- Library / Options page switcher, with the library toolbar sharing
the same row on the right. Plain buttons (not TabItem) so there is
no underline; LB/RB hint chips flank the pair and the gamepad's
shoulder buttons actually switch pages from anywhere. -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
<Border Classes="padHint" VerticalAlignment="Center">
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
</Border>
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
<Border Classes="padHint" VerticalAlignment="Center">
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
VerticalAlignment="Center">
<TextBox x:Name="SearchBox" Watermark="Search library…" Width="240" VerticalAlignment="Center" />
<Button x:Name="AddFolderButton" Classes="ghost" Content=" Add folder" VerticalAlignment="Center" />
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
</StackPanel>
</Grid>
<!-- Cover-art grid -->
<Panel Grid.Row="1">
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
SelectionMode="Single" Padding="0">
<ListBox.ContextMenu>
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
<MenuItem.Icon>
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
<MenuItem.Icon>
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxRemove" Header="Remove from library"
Foreground="{StaticResource DangerHoverBrush}">
<MenuItem.Icon>
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="128" Height="186" Spacing="7">
<Border Classes="coverShadow" Width="128" Height="128">
<Border Classes="coverClip">
<Panel>
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
</Panel>
</Border>
</Border>
<StackPanel Spacing="2">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Empty state -->
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
HorizontalAlignment="Center" />
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content=" Add game folder"
HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
<!-- Library page. The tile row gets extra top margin so it sits
closer to eye level (PS5 home-screen style) instead of hugging
the Library/Options switcher above it. -->
<Panel x:Name="LibraryPage" Margin="0,64,0,0">
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
SelectionMode="Single" Padding="0">
<ListBox.ContextMenu>
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
<MenuItem.Icon>
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
<MenuItem.Icon>
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxRemove" Header="Remove from library"
Foreground="{StaticResource DangerHoverBrush}">
<MenuItem.Icon>
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="128" Height="172" Spacing="7">
<Border Classes="coverShadow" Width="128" Height="128">
<Border Classes="coverClip">
<Panel>
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
</Panel>
</Border>
</Border>
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
TextAlignment="Center" HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Empty state -->
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
HorizontalAlignment="Center" />
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content=" Add game folder"
HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
<!-- Loading state: covers the grid while a scan (initial load,
rescan, or a just-added folder) is in flight, so the screen
never looks blank mid-scan. -->
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
</StackPanel>
</Panel>
<!-- Options page: sub-tabs so future categories (Graphics, …) slot
in next to General. Card-grouped sections match the visual
language used by the console panel and launch bar below. -->
<Grid x:Name="OptionsPage" IsVisible="False">
<TabControl>
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="CpuEngineLabel" Text="CPU engine" FontSize="13" />
<TextBlock x:Name="CpuEngineDesc" Text="Execution engine used to run game code."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
<TextBlock x:Name="StrictDesc" Text="Fail the launch when an imported symbol cannot be resolved."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="StrictToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogLevelLabel" Text="Log level" FontSize="13" />
<TextBlock x:Name="LogLevelDesc" Text="Verbosity of the emulator console output."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="LogLevelBox" Width="160" SelectedIndex="2"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="TraceImportsLabel" Text="Import trace limit" FontSize="13" />
<TextBlock x:Name="TraceImportsDesc" Text="Trace the first N imports per module (0 = off)."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<NumericUpDown Grid.Column="1" x:Name="TraceImportsBox" Width="160" Minimum="0"
Maximum="4096" Increment="16" Value="0" FormatString="0"
VerticalAlignment="Center" CornerRadius="8" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogToFileLabel" Text="Log to file" FontSize="13" />
<TextBlock x:Name="LogToFileDesc" Text="Mirror emulator output to a log file."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogFilePathLabel" Text="Log file path" FontSize="13" />
<TextBlock x:Name="LogFilePathText" Text="No custom path"
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<Button Grid.Column="1" x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="OverrideLogFileLabel" Text="Override log file" FontSize="13" />
<TextBlock x:Name="OverrideLogFileDesc"
Text="Use the exact file path instead of appending title ID and timestamp."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LanguageLabel" Text="Emulator language" FontSize="13" />
<TextBlock x:Name="LanguageDesc"
Text="Language used throughout the launcher. Applies immediately."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="LanguageBox" Width="160"
VerticalAlignment="Center" CornerRadius="8"
DisplayMemberBinding="{Binding NativeName}" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="TitleMusicLabel" Text="Title music" FontSize="13" />
<TextBlock x:Name="TitleMusicDesc" Text="Loop the selected game's preview music in the library."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
IsChecked="True" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="DiscordLabel" Text="Discord presence" FontSize="13" />
<TextBlock x:Name="DiscordDesc" Text="Show the running game on your Discord profile."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</Panel>
<!-- Console (collapsible) -->
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." Width="320" />
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="5" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
Padding="10,4" />
</Grid>
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
@@ -179,8 +353,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
</Border>
<!-- Launch bar -->
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
<!-- Launch bar; capped so it does not sprawl on a maximized window. -->
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14" MaxWidth="1280">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="Auto,*,Auto">
@@ -199,9 +373,32 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Panel>
</Border>
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
TextTrimming="CharacterEllipsis" />
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center" Margin="14,0,14,0">
<Grid ColumnDefinitions="Auto,Auto,*">
<TextBlock Grid.Column="0" x:Name="SelectedGameTitle" Text="No game selected" FontSize="16"
FontWeight="Bold" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
MaxWidth="340" Margin="0,0,10,0" />
<!-- Title id / version / size badges, right next to the
title. The title's own MaxWidth (not a "*" column) is
what keeps them from drifting to the far right. -->
<StackPanel Grid.Column="1" x:Name="SelectedBadgesRow" Orientation="Horizontal" Spacing="6"
IsVisible="False" VerticalAlignment="Center">
<Border Classes="pill" IsVisible="{Binding HasTitleId, FallbackValue=False}">
<TextBlock Text="{Binding TitleId}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill" IsVisible="{Binding HasVersion, FallbackValue=False}">
<TextBlock Text="{Binding VersionText}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill">
<TextBlock Text="{Binding SizeText, FallbackValue=''}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
</Grid>
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" Spacing="7">
@@ -213,56 +410,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
</StackPanel>
</Grid>
<!-- Launch options (collapsible) -->
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="CPU engine" />
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
<ComboBoxItem Content="Native" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log level" />
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
<ComboBoxItem Content="Trace" />
<ComboBoxItem Content="Debug" />
<ComboBoxItem Content="Info" />
<ComboBoxItem Content="Warning" />
<ComboBoxItem Content="Error" />
<ComboBoxItem Content="Critical" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
Value="0" FormatString="0" CornerRadius="8" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log to file" />
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
</StackPanel>
<StackPanel>
<TextBlock Classes="fieldLabel" Text="Title music" />
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
</Grid>
<!-- Status bar -->
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
<Grid x:Name="StatusBar" Grid.Row="2" Height="32" Background="{StaticResource ChromeBrush}"
ColumnDefinitions="*,Auto">
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
TextTrimming="CharacterEllipsis" />
File diff suppressed because it is too large Load Diff
+8
View File
@@ -30,6 +30,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Languages\*.json">
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
with the emulator's pad HLE. They are dependency-free, so they are
compiled in directly rather than pulling a reference to all of
+1 -1
View File
@@ -47,7 +47,7 @@ internal sealed class SndPreviewPlayer
{
// Debounce so skimming through the library does not decode (or
// start) a preview per tile.
await Task.Delay(300).ConfigureAwait(false);
await Task.Delay(120).ConfigureAwait(false);
byte[]? wav;
lock (_sync)
+6 -4
View File
@@ -3,11 +3,13 @@
using System.Buffers.Binary;
using System.Linq;
using SharpEmu.Logging;
namespace SharpEmu.HLE;
public sealed class Aerolib : ISymbolCatalog
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
@@ -108,14 +110,14 @@ public sealed class Aerolib : ISymbolCatalog
if (resourceName == null)
{
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
Log.Error("Embedded resource 'aerolib.bin' not found");
return;
}
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
Log.Error("Failed to open embedded resource stream");
return;
}
@@ -145,11 +147,11 @@ public sealed class Aerolib : ISymbolCatalog
_byExportName[name] = symbol;
}
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
}
}
Binary file not shown.
+195
View File
@@ -3,6 +3,7 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace SharpEmu.HLE;
@@ -12,6 +13,7 @@ public sealed class ModuleManager : IModuleManager
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
private readonly object _registrationGate = new();
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
private bool _isFrozen;
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
@@ -25,6 +27,12 @@ public sealed class ModuleManager : IModuleManager
throw new InvalidOperationException("Module registration is frozen.");
}
// Deduplicated: one assembly is reached through many types.
if (!_scannedAssemblies.Add((assembly, generation)))
{
return 0;
}
var registeredCount = 0;
var instances = new Dictionary<Type, object>();
@@ -73,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
{
_isFrozen = true;
}
WarmHleTypeInitializers();
}
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
private void WarmHleTypeInitializers()
{
Assembly[] assemblies;
lock (_registrationGate)
{
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
}
assemblies = WithGuestReachableDependencies(assemblies);
var bclWarmed = WarmFrameworkTypeInitializers();
var warmed = 0;
var failed = 0;
var jitted = 0;
var jitFailed = 0;
foreach (var assembly in assemblies)
{
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.Where(t => t is not null).ToArray()!;
}
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
foreach (var type in types)
{
if (type is null || type.ContainsGenericParameters)
{
continue;
}
try
{
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
warmed++;
}
catch
{
// A throw here beats a guest-thread fail-fast later; swallow and continue.
failed++;
}
// Force-JIT (not execute) every method so no guest thread compiles one first.
MethodBase[] members;
try
{
members = type.GetConstructors(allMembers)
.Concat<MethodBase>(type.GetMethods(allMembers))
.ToArray();
}
catch
{
continue;
}
foreach (var member in members)
{
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
{
continue;
}
try
{
RuntimeHelpers.PrepareMethod(member.MethodHandle);
jitted++;
}
catch
{
jitFailed++;
}
}
}
}
Console.Error.WriteLine(
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
}
// Framework .cctors too (but not JIT — the BCL is too large).
private static int WarmFrameworkTypeInitializers()
{
var warmed = 0;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
var name = assembly.GetName().Name;
if (name is null || !IsFrameworkAssembly(name))
{
continue;
}
Type[] types;
try
{
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex)
{
types = ex.Types.Where(t => t is not null).ToArray()!;
}
catch
{
continue;
}
foreach (var type in types)
{
if (type is null || type.ContainsGenericParameters)
{
continue;
}
try
{
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
warmed++;
}
catch
{
}
}
}
return warmed;
}
private static bool IsFrameworkAssembly(string assemblyName) =>
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
{
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
foreach (var assembly in scanned)
{
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
}
foreach (var assembly in scanned)
{
AssemblyName[] references;
try
{
references = assembly.GetReferencedAssemblies();
}
catch
{
continue;
}
foreach (var reference in references)
{
var name = reference.Name;
if (name is null || !IsGuestReachableInterop(name))
{
continue;
}
try
{
var loaded = Assembly.Load(reference);
result[loaded.FullName ?? name] = loaded;
}
catch
{
}
}
}
return result.Values.ToArray();
}
private static bool IsGuestReachableInterop(string assemblyName) =>
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
public bool TryGetFunction(string nid, out Delegate function)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
+14
View File
@@ -40,6 +40,13 @@ public enum OrbisGen2Result : int
/// </summary>
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
/// <summary>
/// Indicates that the waited-on object was deleted while the caller was
/// blocked on it. Matches the SCE kernel EACCES code that waiters of a
/// deleted semaphore observe.
/// </summary>
ORBIS_GEN2_ERROR_DELETED = unchecked((int)0x8002000D),
/// <summary>
/// Indicates that the target resource is busy.
/// </summary>
@@ -50,6 +57,13 @@ public enum OrbisGen2Result : int
/// </summary>
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
/// <summary>
/// Indicates that a blocked wait was canceled (e.g. sceKernelCancelSema).
/// Matches the SCE kernel ECANCELED code that canceled semaphore waiters
/// observe.
/// </summary>
ORBIS_GEN2_ERROR_CANCELED = unchecked((int)0x80020055),
/// <summary>
/// Indicates that behavior is recognized but not implemented yet.
/// </summary>
+4
View File
@@ -12,6 +12,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Aerolib\aerolib.bin" />
</ItemGroup>
+5 -1
View File
@@ -1,6 +1,10 @@
{
"version": 2,
"dependencies": {
"net10.0": {}
"net10.0": {
"sharpemu.logging": {
"type": "Project"
}
}
}
}
+595 -143
View File
@@ -52,6 +52,8 @@ public static class AgcExports
private const uint RFlip = 0x17;
private const uint RReleaseMem = 0x18;
private const uint RDmaData = 0x19;
private const uint RPredication = 0x1A;
private const uint RJump = 0x1B;
private const uint SpiShaderPgmLoPs = 0x8;
private const uint SpiShaderPgmHiPs = 0x9;
private const uint SpiShaderPgmLoEs = 0xC8;
@@ -179,7 +181,6 @@ public static class AgcExports
private static long _createShaderTraceCount;
private static long _packetPayloadTraceCount;
private static bool _tracedMissingPixelShaderBindings;
private static long _unsatisfiedWaitTraceCount;
private static long _shaderTranslationMissTraceCount;
private static long _translatedDrawTraceCount;
private static long _standardDmaTraceCount;
@@ -390,6 +391,7 @@ public static class AgcExports
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
public Dictionary<ulong, ComputeImageWriter> ComputeImageWriters { get; } = new();
public ulong WorkSequence { get; set; }
public ulong FlipSequence { get; set; }
}
private readonly record struct RegisterDefaultValue(uint Offset, uint Value);
@@ -1924,6 +1926,90 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress);
}
[SysAbiExport(
Nid = "bbFueFP+J4k",
ExportName = "sceAgcDcbSetPredication",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbSetPredication(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
var enable = (uint)ctx[CpuRegister.Rsi];
var predicateType = (uint)ctx[CpuRegister.Rdx];
var continuePredicate = (uint)ctx[CpuRegister.Rcx];
var predicateAddress = ctx[CpuRegister.R8];
if (commandBufferAddress == 0 || enable > 1 || continuePredicate > 1)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var commandAddress) ||
!ctx.TryWriteUInt32(commandAddress, Pm4(5, ItNop, RPredication)) ||
!ctx.TryWriteUInt32(commandAddress + 4, enable) ||
!ctx.TryWriteUInt32(commandAddress + 8, predicateType) ||
!ctx.TryWriteUInt32(commandAddress + 12, continuePredicate) ||
!ctx.TryWriteUInt32(commandAddress + 16, unchecked((uint)predicateAddress)))
{
return ReturnPointer(ctx, 0);
}
TraceAgc(
$"agc.dcb_set_predication buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " +
$"enable={enable} type={predicateType} continue={continuePredicate} addr=0x{predicateAddress:X16}");
return ReturnPointer(ctx, commandAddress);
}
[SysAbiExport(
Nid = "xSAR0LTcRKM",
ExportName = "sceAgcDcbJump",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbJump(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
var targetAddress = ctx[CpuRegister.Rsi];
if (commandBufferAddress == 0)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 3, out var commandAddress) ||
!ctx.TryWriteUInt32(commandAddress, Pm4(3, ItNop, RJump)) ||
!ctx.TryWriteUInt32(commandAddress + 4, unchecked((uint)targetAddress)) ||
!ctx.TryWriteUInt32(commandAddress + 8, unchecked((uint)(targetAddress >> 32))))
{
return ReturnPointer(ctx, 0);
}
TraceAgc(
$"agc.dcb_jump buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " +
$"target=0x{targetAddress:X16}");
return ReturnPointer(ctx, commandAddress);
}
[SysAbiExport(
Nid = "w6Dj1VJt5qY",
ExportName = "sceAgcSetPacketPredication",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int SetPacketPredication(CpuContext ctx)
{
var packetAddress = ctx[CpuRegister.Rdi];
var enabled = ctx[CpuRegister.Rsi] != 0;
if (packetAddress == 0 || !ctx.TryReadUInt32(packetAddress, out var packetHeader))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
packetHeader = enabled ? packetHeader | 1u : packetHeader & ~1u;
if (!ctx.TryWriteUInt32(packetAddress, packetHeader))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "w2rJhmD+dsE",
ExportName = "sceAgcDriverAddEqEvent",
@@ -1976,9 +2062,22 @@ public static class AgcExports
public static int DriverSubmitDcb(CpuContext ctx)
{
var packetAddress = ctx[CpuRegister.Rdi];
if (packetAddress == 0 ||
!ctx.TryReadUInt64(packetAddress, out var commandAddress) ||
!ctx.TryReadUInt32(packetAddress + 8, out var dwordCount))
// Gen5 supports direct RSI/RDX and packed RDI submissions.
var commandAddress = ctx[CpuRegister.Rsi];
var directDwordCount = ctx[CpuRegister.Rdx];
uint dwordCount;
var directArguments = commandAddress != 0 &&
directDwordCount is > 0 and <= 1_000_000UL;
if (directArguments)
{
dwordCount = (uint)directDwordCount;
}
else if (packetAddress == 0 ||
!ctx.TryReadUInt64(packetAddress, out commandAddress) ||
!ctx.TryReadUInt32(packetAddress + 8, out dwordCount) ||
commandAddress == 0 ||
dwordCount == 0 ||
dwordCount > 1_000_000)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
@@ -1994,13 +2093,87 @@ public static class AgcExports
if (tracePackets)
{
TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}");
TraceAgc(
$"agc.driver_submit_dcb abi={(directArguments ? "direct" : "packed")} " +
$"packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}");
}
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
ulong flipSequenceBefore;
lock (gpuState.Gate)
{
flipSequenceBefore = gpuState.FlipSequence;
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
// Flip submissions are completed through VideoOut pacing.
if (gpuState.FlipSequence == flipSequenceBefore)
{
KernelEventQueueCompatExports.TriggerRegisteredEvents(
0,
KernelEventQueueCompatExports.KernelEventFilterGraphics,
0);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// ABI (reversed from Quake): rdi = array of DCB base addresses (u64 each),
// rsi = array of DCB sizes in dwords (u32 each), rdx = buffer count.
[SysAbiExport(
Nid = "6UzEidRZwkg",
ExportName = "sceAgcDriverSubmitMultiDcbs",
Target = Generation.Gen5,
LibraryName = "libSceAgcDriver")]
public static int DriverSubmitMultiDcbs(CpuContext ctx)
{
var addressArray = ctx[CpuRegister.Rdi];
var sizeArray = ctx[CpuRegister.Rsi];
var bufferCount = (uint)ctx[CpuRegister.Rdx];
if (addressArray == 0 || sizeArray == 0 || bufferCount == 0 || bufferCount > 4096)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var tracePackets = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal);
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
ulong flipSequenceBefore;
lock (gpuState.Gate)
{
flipSequenceBefore = gpuState.FlipSequence;
for (uint i = 0; i < bufferCount; i++)
{
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
commandAddress == 0 ||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
dwordCount == 0)
{
continue;
}
if (tracePackets)
{
TraceAgc(
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
}
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
}
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
if (gpuState.FlipSequence == flipSequenceBefore)
{
KernelEventQueueCompatExports.TriggerRegisteredEvents(
0,
KernelEventQueueCompatExports.KernelEventFilterGraphics,
0);
}
ctx[CpuRegister.Rax] = 0;
@@ -2049,6 +2222,7 @@ public static class AgcExports
}
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
@@ -2170,6 +2344,63 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress);
}
// WAIT_REG_MEM packets whose condition is not met suspend their DCB into
// GpuWaitRegistry. Each submit re-checks every suspended DCB against current guest
// memory (labels are advanced by ReleaseMem/WriteData/DmaData packets or by direct
// CPU writes) and resumes the ones whose condition is now satisfied. A resumed DCB
// can itself write labels that unblock others, so loop until a fixed point.
private static void DrainResumableDcbs(
CpuContext ctx,
SubmittedGpuState gpuState,
bool tracePackets)
{
for (var pass = 0; pass < 256; pass++)
{
var woken = GpuWaitRegistry.CollectSatisfied((address, is64Bit) =>
{
if (is64Bit)
{
return ctx.TryReadUInt64(address, out var value64)
? value64
: (ulong?)null;
}
return ctx.TryReadUInt32(address, out var value32)
? value32
: (ulong?)null;
});
if (woken is null)
{
return;
}
foreach (var waiter in woken)
{
var remainingDwords = waiter.TotalDwords - waiter.ResumeOffset;
if (remainingDwords == 0)
{
continue;
}
if (tracePackets)
{
TraceAgc(
$"agc.dcb.resumed addr=0x{waiter.WaitAddress:X16} " +
$"resume=0x{waiter.ResumeAddress:X16} dwords={remainingDwords}");
}
var state = waiter.State as SubmittedDcbState ?? gpuState.Graphics;
ParseSubmittedDcb(
ctx,
gpuState,
state,
waiter.ResumeAddress,
remainingDwords,
tracePackets);
}
}
}
private static void ParseSubmittedDcb(
CpuContext ctx,
SubmittedGpuState gpuState,
@@ -2193,6 +2424,40 @@ public static class AgcExports
}
var packetType = header >> 30;
if (packetType == 0)
{
// Consume alignment padding one dword at a time.
if (header == 0)
{
offset++;
continue;
}
// Preserve stream alignment across type-0 packets.
var type0Length = ((header >> 16) & 0x3FFFu) + 2u;
if (offset + type0Length > dwordCount)
{
offset++;
continue;
}
offset += type0Length;
continue;
}
if (packetType == 1)
{
const uint type1Length = 3;
if (offset + type1Length > dwordCount)
{
offset++;
continue;
}
offset += type1Length;
continue;
}
if (packetType == 2)
{
if (tracePackets)
@@ -2201,20 +2466,21 @@ public static class AgcExports
$"agc.dcb.packet dw={offset} addr=0x{currentAddress:X16} " +
$"header=0x{header:X8} len=1 type=2");
}
offset++;
continue;
}
if (packetType != 3)
{
return;
offset++;
continue;
}
var length = Pm4Length(header);
if (length == 0 || offset + length > dwordCount)
{
return;
offset++;
continue;
}
var op = (header >> 8) & 0xFFu;
@@ -2311,16 +2577,91 @@ public static class AgcExports
state.InstanceCount = Math.Max(instanceCount, 1);
}
if (op == ItNop &&
register is RWaitMem32 or RWaitMem64 &&
// WAIT_REG_MEM (AGC NOP-encapsulated 32/64-bit variants): when the condition is
// not yet satisfied, suspend this DCB; DrainResumableDcbs resumes it once the
// watched memory advances.
if (op == ItNop && register is RWaitMem32 or RWaitMem64 &&
length >= (register == RWaitMem32 ? 6u : 9u))
{
ObserveSubmittedWaitRegMem(ctx, currentAddress, register == RWaitMem64, tracePackets);
if (TryParseWaitRegMem(ctx, currentAddress, register == RWaitMem64,
out var waitAddr, out var refVal, out var waitMask, out var cmpFunc))
{
ulong curVal = 0;
bool hasCurVal;
if (register == RWaitMem64)
{
hasCurVal = ctx.TryReadUInt64(waitAddr, out curVal);
}
else if (ctx.TryReadUInt32(waitAddr, out var curVal32))
{
curVal = curVal32;
hasCurVal = true;
}
else
{
hasCurVal = false;
}
var waiter = new GpuWaitRegistry.WaitingDcb
{
CommandBufferAddress = commandAddress,
ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)),
TotalDwords = dwordCount,
ResumeOffset = offset + length,
ReferenceValue = refVal,
Mask = waitMask,
CompareFunction = cmpFunc,
Is64Bit = register == RWaitMem64,
State = state,
};
if (hasCurVal && !GpuWaitRegistry.Compare(waiter, curVal))
{
GpuWaitRegistry.Register(waitAddr, waiter);
if (tracePackets)
{
TraceAgc(
$"agc.dcb.suspended addr=0x{waitAddr:X16} ref=0x{refVal:X16} " +
$"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}");
}
return; // suspend parsing of this DCB
}
}
}
if (op == ItWaitRegMem && length >= 7)
{
ObserveSubmittedStandardWaitRegMem(ctx, currentAddress, tracePackets);
if (TryParseStandardWaitRegMem(ctx, currentAddress,
out var waitAddr, out var refVal, out var waitMask, out var cmpFunc) &&
ctx.TryReadUInt32(waitAddr, out var curVal))
{
var waiter = new GpuWaitRegistry.WaitingDcb
{
CommandBufferAddress = commandAddress,
ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)),
TotalDwords = dwordCount,
ResumeOffset = offset + length,
ReferenceValue = refVal,
Mask = waitMask,
CompareFunction = cmpFunc,
Is64Bit = false,
State = state,
};
if (!GpuWaitRegistry.Compare(waiter, curVal))
{
GpuWaitRegistry.Register(waitAddr, waiter);
if (tracePackets)
{
TraceAgc(
$"agc.dcb.suspended_std addr=0x{waitAddr:X16} ref=0x{refVal:X16} " +
$"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}");
}
return;
}
}
}
if (TryReadSubmittedDrawCount(
@@ -2377,6 +2718,7 @@ public static class AgcExports
if (op == ItNop && register == RFlip && length >= 6)
{
gpuState.FlipSequence++;
if (!ctx.TryReadUInt32(currentAddress + 4, out var videoOutHandle) ||
!ctx.TryReadUInt32(currentAddress + 8, out var displayBufferIndexRaw) ||
!ctx.TryReadUInt32(currentAddress + 12, out var flipMode) ||
@@ -2389,23 +2731,8 @@ public static class AgcExports
var flipArg = unchecked((long)(((ulong)flipArgHi << 32) | flipArgLo));
var displayBufferIndex = unchecked((int)displayBufferIndexRaw);
var handle = unchecked((int)videoOutHandle);
if (VideoOutExports.TryGetDisplayBufferInfo(
handle,
displayBufferIndex,
out var cachedDisplayBuffer) &&
VulkanVideoPresenter.TrySubmitGuestImage(
cachedDisplayBuffer.Address,
cachedDisplayBuffer.Width,
cachedDisplayBuffer.Height,
cachedDisplayBuffer.PitchInPixel))
{
TraceDisplayBuffer(
handle,
displayBufferIndex,
cachedDisplayBuffer,
"gpu-cache");
}
else if (state.SawIndexedDraw &&
// Prefer output decoded from the current DCB.
if (state.SawIndexedDraw &&
state.TranslatedDraw is { } translatedDraw &&
VideoOutExports.TryGetDisplayBufferInfo(
handle,
@@ -2469,6 +2796,22 @@ public static class AgcExports
displayBuffer.Width,
displayBuffer.Height);
}
else if (VideoOutExports.TryGetDisplayBufferInfo(
handle,
displayBufferIndex,
out var cachedDisplayBuffer) &&
VulkanVideoPresenter.TrySubmitGuestImage(
cachedDisplayBuffer.Address,
cachedDisplayBuffer.Width,
cachedDisplayBuffer.Height,
cachedDisplayBuffer.PitchInPixel))
{
TraceDisplayBuffer(
handle,
displayBufferIndex,
cachedDisplayBuffer,
"gpu-cache");
}
_ = VideoOutExports.SubmitFlipFromAgc(ctx, handle, displayBufferIndex, unchecked((int)flipMode), flipArg);
state.SawIndexedDraw = false;
@@ -2641,104 +2984,6 @@ public static class AgcExports
}
}
private static void ObserveSubmittedWaitRegMem(
CpuContext ctx,
ulong packetAddress,
bool is64Bit,
bool tracePacket)
{
if (!ctx.TryReadUInt64(packetAddress + 4, out var address) ||
!ctx.TryReadUInt32(packetAddress + (is64Bit ? 28u : 16u), out var control))
{
return;
}
ulong mask;
ulong reference;
ulong value;
if (is64Bit)
{
if (!ctx.TryReadUInt64(packetAddress + 12, out mask) ||
!ctx.TryReadUInt64(packetAddress + 20, out reference) ||
!ctx.TryReadUInt64(address, out value))
{
return;
}
}
else
{
if (!ctx.TryReadUInt32(packetAddress + 12, out var mask32) ||
!ctx.TryReadUInt32(packetAddress + 20, out var reference32) ||
!ctx.TryReadUInt32(address, out var value32))
{
return;
}
mask = mask32;
reference = reference32;
value = value32;
}
var compareFunction = control & 0xFFu;
TraceSubmittedWait(
address,
value,
mask,
reference,
compareFunction,
is64Bit ? 64 : 32,
tracePacket);
}
private static void ObserveSubmittedStandardWaitRegMem(
CpuContext ctx,
ulong packetAddress,
bool tracePacket)
{
if (!ctx.TryReadUInt32(packetAddress + 4, out var control) ||
!ctx.TryReadUInt64(packetAddress + 8, out var address) ||
!ctx.TryReadUInt32(packetAddress + 16, out var reference) ||
!ctx.TryReadUInt32(packetAddress + 20, out var mask) ||
!ctx.TryReadUInt32(address, out var value))
{
return;
}
TraceSubmittedWait(address, value, mask, reference, control & 0x7u, 32, tracePacket);
}
private static void TraceSubmittedWait(
ulong address,
ulong value,
ulong mask,
ulong reference,
uint compareFunction,
int bits,
bool tracePacket)
{
var maskedValue = value & mask;
var satisfied = compareFunction switch
{
0 => false,
1 => maskedValue < reference,
2 => maskedValue <= reference,
3 => maskedValue == reference,
4 => maskedValue != reference,
5 => maskedValue >= reference,
6 => maskedValue > reference,
_ => true,
};
if (!tracePacket && (satisfied || !ShouldTraceHotPath(ref _unsatisfiedWaitTraceCount)))
{
return;
}
TraceAgc(
$"agc.dcb.wait_reg_mem bits={bits} addr=0x{address:X16} " +
$"value=0x{value:X16} mask=0x{mask:X16} ref=0x{reference:X16} " +
$"compare={compareFunction} satisfied={satisfied}");
}
private static void ApplySubmittedReleaseMem(
CpuContext ctx,
ulong packetAddress,
@@ -2756,10 +3001,11 @@ public static class AgcExports
var dataSelection = (control >> 16) & 0xFFu;
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
var data = ((ulong)dataHi << 32) | dataLo;
var wroteData = dataSelection switch
{
1 or 2 => ctx.TryWriteUInt32(destinationAddress, dataLo),
3 => ctx.TryWriteUInt64(destinationAddress, data),
1 => ctx.TryWriteUInt32(destinationAddress, dataLo),
2 or 3 => ctx.TryWriteUInt64(destinationAddress, data),
_ => false,
};
@@ -2855,6 +3101,11 @@ public static class AgcExports
case ItDrawIndex2 when packetLength >= 6:
state.DrawIndexOffset = 0;
return ctx.TryReadUInt32(packetAddress + 16, out drawCount);
// 5-dword form emitted by DcbDrawIndex (count at +4, ItIndexBase/
// ItIndexBufferSize carried by separate preceding packets).
case ItDrawIndex2 when packetLength >= 5:
state.DrawIndexOffset = 0;
return ctx.TryReadUInt32(packetAddress + 4, out drawCount);
case ItDrawIndexOffset2 when packetLength >= 5:
if (!ctx.TryReadUInt32(packetAddress + 8, out var indexOffset))
{
@@ -3206,10 +3457,60 @@ public static class AgcExports
globalMemoryBindings,
vertexInputs,
renderTargets,
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()));
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
textures,
vertexInputs,
pixelEvaluation.InitialScalarRegisters));
return true;
}
private static readonly bool _transparentFillClearEnabled = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRANSPARENT_FILL_CLEAR"),
"1",
StringComparison.Ordinal);
/// <summary>
/// Chowdren resets its effect layers with an untextured transparent-black
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
/// Treat precisely that draw shape as an overwrite.
/// </summary>
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
VulkanGuestRenderState renderState,
IReadOnlyList<TranslatedImageBinding> textures,
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
IReadOnlyList<uint> pixelUserData)
{
if (!_transparentFillClearEnabled ||
textures.Count != 0 ||
vertexInputs.Count != 0 ||
pixelUserData.Count < 4 ||
renderState.Blend is not
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
})
{
return renderState;
}
for (var index = 0; index < 4; index++)
{
if ((pixelUserData[index] & 0x7FFF_FFFFu) != 0)
{
return renderState;
}
}
return renderState with
{
Blend = renderState.Blend with { Enable = false },
};
}
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
@@ -3832,25 +4133,28 @@ public static class AgcExports
TraceTextureHash(descriptor, source);
var nonZero = 0;
for (var i = 0; i < source.Length; i++)
if (_traceAgcShader)
{
if (source[i] != 0)
var nonZero = 0;
for (var i = 0; i < source.Length; i++)
{
nonZero++;
if (nonZero >= 64)
if (source[i] != 0)
{
break;
nonZero++;
if (nonZero >= 64)
{
break;
}
}
}
}
TraceAgcShader(
$"agc.texture_source addr=0x{descriptor.Address:X16} " +
$"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " +
$"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " +
$"dst=0x{descriptor.DstSelect:X3} " +
$"bytes={source.Length} nonzero64={nonZero}");
TraceAgcShader(
$"agc.texture_source addr=0x{descriptor.Address:X16} " +
$"fmt={descriptor.Format} num={descriptor.NumberType} tile={descriptor.TileMode} " +
$"size={descriptor.Width}x{descriptor.Height} pitch={descriptor.Pitch} " +
$"dst=0x{descriptor.DstSelect:X3} " +
$"bytes={source.Length} nonzero64={nonZero}");
}
var rgba = source;
texture = new VulkanGuestDrawTexture(
@@ -5561,6 +5865,16 @@ public static class AgcExports
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
}
private static void TraceAgc(ref AgcTraceHandler message)
{
if (!_traceAgc)
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] {message.ToStringAndClear()}");
}
private static void TraceAgcShader(string message)
{
if (!_traceAgcShader)
@@ -5571,6 +5885,62 @@ public static class AgcExports
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
}
private static void TraceAgcShader(ref AgcShaderTraceHandler message)
{
if (!_traceAgcShader)
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] {message.ToStringAndClear()}");
}
[InterpolatedStringHandler]
internal ref struct AgcTraceHandler
{
private DefaultInterpolatedStringHandler _inner;
public AgcTraceHandler(int literalLength, int formattedCount, out bool isEnabled)
{
isEnabled = _traceAgc;
_inner = isEnabled
? new DefaultInterpolatedStringHandler(literalLength, formattedCount)
: default;
}
public void AppendLiteral(string value) => _inner.AppendLiteral(value);
public void AppendFormatted<T>(T value) => _inner.AppendFormatted(value);
public void AppendFormatted<T>(T value, string? format) =>
_inner.AppendFormatted(value, format);
public string ToStringAndClear() => _inner.ToStringAndClear();
}
[InterpolatedStringHandler]
internal ref struct AgcShaderTraceHandler
{
private DefaultInterpolatedStringHandler _inner;
public AgcShaderTraceHandler(int literalLength, int formattedCount, out bool isEnabled)
{
isEnabled = _traceAgcShader;
_inner = isEnabled
? new DefaultInterpolatedStringHandler(literalLength, formattedCount)
: default;
}
public void AppendLiteral(string value) => _inner.AppendLiteral(value);
public void AppendFormatted<T>(T value) => _inner.AppendFormatted(value);
public void AppendFormatted<T>(T value, string? format) =>
_inner.AppendFormatted(value, format);
public string ToStringAndClear() => _inner.ToStringAndClear();
}
private static string FormatShaderDwords(IReadOnlyList<uint> values) =>
values.Count == 0
? "none"
@@ -5635,4 +6005,86 @@ public static class AgcExports
Console.Error.WriteLine(
$"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}");
}
// Packet layouts mirror what DcbWaitRegMem emits.
// 32-bit (ItNop/RWaitMem32): +4 addrLo, +8 addrHi, +12 mask, +16 cmp|op<<8, +20 ref.
// 64-bit (ItNop/RWaitMem64): +4 addrLo, +8 addrHi, +12 maskLo, +16 maskHi,
// +20 refLo, +24 refHi, +28 cmp|op<<8, +32 poll.
private static bool TryParseWaitRegMem(
CpuContext ctx,
ulong addr,
bool is64,
out ulong waitAddr,
out ulong refVal,
out ulong mask,
out uint cmpFunc)
{
waitAddr = refVal = mask = 0;
cmpFunc = 0;
if (!ctx.TryReadUInt32(addr + 4, out var lo) ||
!ctx.TryReadUInt32(addr + 8, out var hi))
{
return false;
}
waitAddr = ((ulong)hi << 32) | lo;
if (!is64)
{
if (!ctx.TryReadUInt32(addr + 12, out var mask32) ||
!ctx.TryReadUInt32(addr + 16, out var cmpRaw) ||
!ctx.TryReadUInt32(addr + 20, out var refVal32))
{
return false;
}
mask = mask32;
refVal = refVal32;
cmpFunc = cmpRaw & 0x7;
return true;
}
if (!ctx.TryReadUInt32(addr + 12, out var maskLo) ||
!ctx.TryReadUInt32(addr + 16, out var maskHi) ||
!ctx.TryReadUInt32(addr + 20, out var refLo) ||
!ctx.TryReadUInt32(addr + 24, out var refHi) ||
!ctx.TryReadUInt32(addr + 28, out var cmpRaw64))
{
return false;
}
mask = ((ulong)maskHi << 32) | maskLo;
refVal = ((ulong)refHi << 32) | refLo;
cmpFunc = cmpRaw64 & 0x7;
return true;
}
// Standard ItWaitRegMem (7 dwords, 32-bit compare):
// +4 cmp|(op&1)<<8, +8 addrLo, +12 addrHi, +16 ref, +20 mask, +24 poll.
private static bool TryParseStandardWaitRegMem(
CpuContext ctx,
ulong addr,
out ulong waitAddr,
out ulong refVal,
out ulong mask,
out uint cmpFunc)
{
waitAddr = refVal = mask = 0;
cmpFunc = 0;
if (!ctx.TryReadUInt32(addr + 4, out var cmpRaw) ||
!ctx.TryReadUInt32(addr + 8, out var lo) ||
!ctx.TryReadUInt32(addr + 12, out var hi) ||
!ctx.TryReadUInt32(addr + 16, out var reference) ||
!ctx.TryReadUInt32(addr + 20, out var mask32))
{
return false;
}
cmpFunc = cmpRaw & 0x7;
waitAddr = ((ulong)hi << 32) | lo;
refVal = reference;
mask = mask32;
return true;
}
}
+10 -4
View File
@@ -621,6 +621,8 @@ internal static class Gen5ShaderTranslator
0x10 => "SSendmsg",
0x16 => "STtraceData",
0x20 => "SInstPrefetch",
0x21 => "SClause",
0x23 => "SWaitcntDepctr",
_ => string.Empty,
};
@@ -718,7 +720,7 @@ internal static class Gen5ShaderTranslator
}
var src0 = word & 0x1FF;
sizeDwords = opcode is 0x20 or 0x21 || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
error = string.Empty;
name = opcode switch
{
@@ -757,7 +759,9 @@ internal static class Gen5ShaderTranslator
0x28 => "VAddcU32",
0x29 => "VSubbU32",
0x2A => "VSubbrevU32",
0x2B => "VLdexpF32",
0x2B => "VFmacF32",
0x2C => "VFmamkF32",
0x2D => "VFmaakF32",
0x2F => "VCvtPkrtzF16F32",
0x30 => "VCvtPkU16U32",
0x31 => "VCvtPkI16I32",
@@ -870,6 +874,7 @@ internal static class Gen5ShaderTranslator
0x10F => "VMinF32",
0x110 => "VMaxF32",
0x11F => "VMacF32",
0x12B => "VFmacF32",
0x12F => "VCvtPkrtzF16F32",
0x141 => "VMadF32",
0x143 => "VMadU32U24",
@@ -897,6 +902,7 @@ internal static class Gen5ShaderTranslator
0x169 => "VMulLoU32",
0x16A => "VMulHiU32",
0x16B => "VMulLoI32",
0x16C => "VMulHiI32",
0x360 => "VMadU32U16",
0x361 => "VMulLoU32",
0x362 => "VLdexpF32",
@@ -1379,7 +1385,7 @@ internal static class Gen5ShaderTranslator
Gen5Operand.Source(word & 0x1FF, literal),
Gen5Operand.Vector((word >> 9) & 0xFF),
];
if (opcode == "VMadMkF32" && literal.HasValue)
if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
{
sources =
[
@@ -1388,7 +1394,7 @@ internal static class Gen5ShaderTranslator
sources[1],
];
}
else if (opcode == "VMadAkF32" && literal.HasValue)
else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
{
sources =
[
@@ -255,6 +255,8 @@ internal static partial class Gen5SpirvTranslator
case "VFmaF32":
case "VMadMkF32":
case "VMadAkF32":
case "VFmamkF32":
case "VFmaakF32":
result = EmitFloatResult(
instruction,
Ext(
@@ -265,6 +267,7 @@ internal static partial class Gen5SpirvTranslator
GetFloatSource(instruction, 2)));
break;
case "VMacF32":
case "VFmacF32":
{
var addend = Bitcast(_floatType, LoadV(destination));
result = EmitFloatResult(
@@ -334,6 +337,7 @@ internal static partial class Gen5SpirvTranslator
result = EmitSubtractWithBorrow(instruction, reverse: true);
break;
case "VMulLoU32":
case "VMulLoI32":
case "VMulU32U24":
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
break;
@@ -369,6 +373,29 @@ internal static partial class Gen5SpirvTranslator
_module.Constant64(_ulongType, 32)));
break;
}
case "VMulHiI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, GetRawSource(instruction, 0)));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, GetRawSource(instruction, 1)));
var product = _module.AddInstruction(
SpirvOp.IMul,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
Bitcast(_ulongType, product),
_module.Constant64(_ulongType, 32)));
break;
}
case "VMadU32U24":
{
var left = BitwiseAnd(
@@ -1109,6 +1136,52 @@ internal static partial class Gen5SpirvTranslator
left,
right);
break;
case "SMulHiU32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.UConvert,
_ulongType,
left);
var wideRight = _module.AddInstruction(
SpirvOp.UConvert,
_ulongType,
right);
var product = _module.AddInstruction(
SpirvOp.IMul,
_ulongType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
product,
_module.Constant64(_ulongType, 32)));
break;
}
case "SMulHiI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, left));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, right));
var product = _module.AddInstruction(
SpirvOp.IMul,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
ShiftRightLogical64(
Bitcast(_ulongType, product),
_module.Constant64(_ulongType, 32)));
break;
}
case "SAndB32":
result = BitwiseAnd(left, right);
Store(_scc, IsNotZero(result));
+102
View File
@@ -0,0 +1,102 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
// AgcExports re-checks every waiter against guest memory on each submit and resumes
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
// DmaData packets or by direct CPU writes).
internal static class GpuWaitRegistry
{
public struct WaitingDcb
{
public ulong CommandBufferAddress;
public ulong ResumeAddress;
public uint TotalDwords;
public uint ResumeOffset;
public ulong WaitAddress;
public ulong ReferenceValue;
public ulong Mask;
public uint CompareFunction;
public bool Is64Bit;
public object? State;
}
private static readonly object _gate = new();
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
public static void Register(ulong address, WaitingDcb waiter)
{
waiter.WaitAddress = address;
lock (_gate)
{
if (!_waiters.TryGetValue(address, out var list))
{
list = new List<WaitingDcb>();
_waiters.Add(address, list);
}
list.Add(waiter);
}
}
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
// returns null when the memory is unreadable; such waiters are kept registered.
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
{
List<WaitingDcb>? woken = null;
lock (_gate)
{
List<ulong>? emptied = null;
foreach (var (address, list) in _waiters)
{
for (var i = list.Count - 1; i >= 0; i--)
{
var value = readValue(address, list[i].Is64Bit);
if (value is null || !Compare(list[i], value.Value))
{
continue;
}
woken ??= new List<WaitingDcb>();
woken.Add(list[i]);
list.RemoveAt(i);
}
if (list.Count == 0)
{
emptied ??= new List<ulong>();
emptied.Add(address);
}
}
if (emptied != null)
{
foreach (var address in emptied)
{
_waiters.Remove(address);
}
}
}
return woken;
}
public static bool Compare(in WaitingDcb waiter, ulong value)
{
var masked = value & waiter.Mask;
return waiter.CompareFunction switch
{
1 => masked < waiter.ReferenceValue,
2 => masked <= waiter.ReferenceValue,
3 => masked == waiter.ReferenceValue,
4 => masked != waiter.ReferenceValue,
5 => masked >= waiter.ReferenceValue,
6 => masked > waiter.ReferenceValue,
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
// satisfied keeps a malformed packet from suspending its DCB forever.
_ => true,
};
}
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ajm;
public static class AjmExports
{
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
private static int _nextContextId;
[SysAbiExport(
Nid = "dl+4eHSzUu4",
ExportName = "sceAjmInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int Initialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outContextAddress = ctx[CpuRegister.Rsi];
// AJM requires a zero reserved argument.
if (reserved != 0 || outContextAddress == 0)
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
return ctx.SetReturn(0);
}
}
+38 -4
View File
@@ -267,19 +267,53 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
ulong bytesRead = 0;
// Unregistered/missing files are zero-filled instead of failing: games queue
// speculative reads and only consume the bytes on success paths.
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
{
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
if (destination != 0 && size > 0)
{
int chunkSize = (int)Math.Min(size, 4096);
Span<byte> zeros = stackalloc byte[chunkSize];
zeros.Clear();
while (bytesRead < size)
{
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
{
break;
}
bytesRead += (ulong)currentChunk;
}
}
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
// Offset -1 means "continue after the previous read of this file id".
if (fileOffset == unchecked((ulong)(long)-1))
{
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
}
else if (fileOffset > long.MaxValue)
{
fileOffset = 0;
}
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
return result;
}
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
+1 -1
View File
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
return _hostPathsById.TryGetValue(id, out hostPath!);
}
private static uint ComputeFileId(string guestPath)
internal static uint ComputeFileId(string guestPath)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
@@ -0,0 +1,156 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ampr;
/// <summary>
/// sceAmprAprCommandBufferReadFile carries -1 instead of an absolute offset whenever the guest
/// wants "the next sequential chunk" of a file. For an id Software PACK archive that sequence is
/// always header -> directory table -> individual archived files, and the guest never tells us
/// which archived file it wants next - only the byte count it expects, taken from the very
/// directory entry it already parsed on its side. This tracks a per-fileId read cursor, and once
/// the directory table has streamed past, parses it so later reads can be matched back to their
/// real absolute offset by the size the guest asks for.
/// </summary>
internal static class PakDirectoryTracker
{
private const int DirectoryEntrySize = 64;
private const int DirectoryEntryNameLength = 56;
private static readonly bool _trace =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal) ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
private sealed class DirectoryEntry(string name, uint filePos, uint fileLen)
{
public string Name { get; } = name;
public uint FilePos { get; } = filePos;
public uint FileLen { get; } = fileLen;
public bool Consumed { get; set; }
}
private sealed class FileState
{
public ulong NextOffset;
public bool ExpectingDirectory;
public List<DirectoryEntry>? Directory;
}
private static readonly ConcurrentDictionary<uint, FileState> _state = new();
/// <summary>
/// Resolves what "read the next sequential chunk of size <paramref name="requestedSize"/>"
/// really means for this fileId: an unconsumed directory entry whose length matches, if the
/// directory has already been parsed, otherwise the tracked linear cursor.
/// </summary>
public static ulong ResolveSequentialOffset(uint fileId, ulong requestedSize)
{
var state = _state.GetOrAdd(fileId, static _ => new FileState());
if (state.Directory is { } entries)
{
foreach (var entry in entries)
{
if (!entry.Consumed && entry.FileLen == requestedSize)
{
entry.Consumed = true;
if (_trace)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
}
return entry.FilePos;
}
}
}
return state.NextOffset;
}
/// <summary>
/// Feeds back a completed read so the tracker can advance its cursor, recognize a PACK
/// header and jump straight to its directory table, or parse the directory table itself
/// once it streams past.
/// </summary>
public static void OnReadCompleted(CpuContext ctx, uint fileId, ulong destination, ulong fileOffset, ulong bytesRead)
{
if (bytesRead == 0)
{
return;
}
var state = _state.GetOrAdd(fileId, static _ => new FileState());
if (state.ExpectingDirectory && fileOffset == state.NextOffset)
{
state.Directory = TryParseDirectory(ctx, destination, bytesRead);
state.ExpectingDirectory = false;
state.NextOffset = fileOffset + bytesRead;
if (_trace)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pak.directory_parsed: id=0x{fileId:X8} entries={state.Directory?.Count ?? 0}");
}
return;
}
if (fileOffset == 0 && bytesRead >= 12 && TryReadPackHeader(ctx, destination, out var dirOffset))
{
state.NextOffset = dirOffset;
state.ExpectingDirectory = true;
return;
}
state.NextOffset = fileOffset + bytesRead;
}
private static bool TryReadPackHeader(CpuContext ctx, ulong destination, out ulong dirOffset)
{
dirOffset = 0;
Span<byte> header = stackalloc byte[12];
if (!ctx.Memory.TryRead(destination, header) ||
header[0] != (byte)'P' || header[1] != (byte)'A' || header[2] != (byte)'C' || header[3] != (byte)'K')
{
return false;
}
dirOffset = BinaryPrimitives.ReadUInt32LittleEndian(header[4..8]);
return true;
}
private static List<DirectoryEntry>? TryParseDirectory(CpuContext ctx, ulong destination, ulong length)
{
var count = (int)(length / DirectoryEntrySize);
if (count <= 0)
{
return null;
}
var entries = new List<DirectoryEntry>(count);
Span<byte> record = stackalloc byte[DirectoryEntrySize];
for (var i = 0; i < count; i++)
{
if (!ctx.Memory.TryRead(destination + (ulong)(i * DirectoryEntrySize), record))
{
break;
}
var nameBytes = record[..DirectoryEntryNameLength];
var nullIndex = nameBytes.IndexOf((byte)0);
var name = Encoding.ASCII.GetString(nullIndex >= 0 ? nameBytes[..nullIndex] : nameBytes);
var filePos = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(56, 4));
var fileLen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(60, 4));
entries.Add(new DirectoryEntry(name, filePos, fileLen));
}
return entries;
}
}
+12 -14
View File
@@ -8,9 +8,14 @@ namespace SharpEmu.Libs.Audio;
public static class AudioOut2Exports
{
private const int AudioOut2ContextParamSize = 0x80;
// Sized from guest evidence, not SDK headers: Quake keeps its
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
// and an earlier 0x80-byte ResetParam write zeroed that canary
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
// the whole audio init). Stay well below 0x60 and only write the prefix we
// populate.
private const int AudioOut2ContextParamSize = 0x30;
private const int AudioOut2ContextMemorySize = 0x10000;
private const int AudioOut2ContextMemoryAlignment = 0x10000;
private static long _nextContextHandle = 1;
private static long _nextUserHandle = 1;
private static int _nextPortId;
@@ -59,20 +64,13 @@ public static class AudioOut2Exports
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || outMemorySizeAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> memoryInfo = stackalloc byte[0x20];
memoryInfo.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -204,7 +202,7 @@ public static class AudioOut2Exports
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
@@ -35,6 +35,42 @@ public static class AvPlayerExports
return unchecked((int)handle);
}
[SysAbiExport(
Nid = "JdksQu8pNdQ",
ExportName = "sceAvPlayerGetVideoDataEx",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerGetVideoDataEx(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// "UbQoYawOsfY" is sceAvPlayerIsActive, not sceAvPlayerGetVideoDataEx (the previous NID here
// was wrong - verified by hashing both names against scripts/ps5_names.txt). No player is
// ever actually active in this HLE implementation, so report false/inactive.
[SysAbiExport(
Nid = "UbQoYawOsfY",
ExportName = "sceAvPlayerIsActive",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerIsActive(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "Wnp1OVcrZgk",
ExportName = "sceAvPlayerGetAudioData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerGetAudioData(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "HD1YKVU26-M",
ExportName = "sceAvPlayerPostInit",
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.CommonDialog;
public static class CommonDialogExports
{
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
private static int _initialized;
[SysAbiExport(
@@ -18,11 +17,12 @@ public static class CommonDialogExports
LibraryName = "libSceCommonDialog")]
public static int CommonDialogInitialize(CpuContext ctx)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0
? 0
: AlreadySystemInitialized;
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
// Games can initialize the common-dialog service defensively from more than one
// subsystem. Keeping this HLE initialization idempotent avoids turning an error
// reporting path into a second, unrelated failure.
Interlocked.Exchange(ref _initialized, 1);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
@@ -8,8 +8,13 @@ namespace SharpEmu.Libs.CommonDialog;
public static class MsgDialogExports
{
private const int AlreadyInitialized = unchecked((int)0x80B80002);
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusFinished = 3;
private const int ResultSize = 0x20;
private static int _initialized;
private static int _status;
[SysAbiExport(
Nid = "lDqxaY1UbEo",
@@ -18,9 +23,162 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogInitialize(CpuContext ctx)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0
? 0
: AlreadyInitialized;
// Treat repeated initialization as success. The dialog service is process-global in
// this HLE implementation and has no per-call resources to recreate.
Interlocked.Exchange(ref _initialized, 1);
Interlocked.Exchange(ref _status, StatusInitialized);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ePw-kqZmelo",
ExportName = "sceMsgDialogTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogTerminate(CpuContext ctx)
{
Interlocked.Exchange(ref _initialized, 0);
Interlocked.Exchange(ref _status, StatusNone);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "b06Hh0DPEaE",
ExportName = "sceMsgDialogOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogOpen(CpuContext ctx)
{
LogDialogMessage(ctx, ctx[CpuRegister.Rdi]);
// There is no host popup to actually show. Complete immediately with "finished" so a
// guest polling loop (GetStatus/UpdateStatus -> GetResult -> Close) sees a dismissed
// dialog on its very first poll instead of spinning forever waiting for user input.
Interlocked.Exchange(ref _status, StatusFinished);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
// log even though no host dialog is shown. The PS5 SceMsgDialogParam layout is not
// fully known, so chase every qword in the struct that points at readable text - one
// level deep, then a second level for nested sub-param structs.
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
{
if (paramAddress == 0)
{
Console.Error.WriteLine("[LOADER][INFO] sceMsgDialogOpen: param=NULL");
return;
}
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
Span<byte> head = stackalloc byte[0xA0];
if (ctx.Memory.TryRead(paramAddress, head))
{
DumpPointerStrings(ctx, paramAddress, head, "param", chaseNested: true);
}
}
private static void DumpPointerStrings(CpuContext ctx, ulong baseAddress, ReadOnlySpan<byte> bytes, string label, bool chaseNested)
{
Span<byte> nested = stackalloc byte[0x40];
for (var offset = 0; offset + 8 <= bytes.Length; offset += 8)
{
var value = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(bytes[offset..]);
if (value < 0x10000 || value == baseAddress)
{
continue;
}
var text = TryReadPrintableText(ctx, value);
if (text is not null)
{
Console.Error.WriteLine($"[LOADER][INFO] {label}+0x{offset:X2} -> 0x{value:X12} text=\"{text}\"");
}
else if (chaseNested && ctx.Memory.TryRead(value, nested))
{
DumpPointerStrings(ctx, value, nested, $"{label}+0x{offset:X2}", chaseNested: false);
}
}
}
private static string? TryReadPrintableText(CpuContext ctx, ulong address)
{
Span<byte> bytes = stackalloc byte[256];
if (!ctx.Memory.TryRead(address, bytes))
{
return null;
}
var nullIndex = bytes.IndexOf((byte)0);
if (nullIndex < 2)
{
return null;
}
var candidate = bytes[..nullIndex];
foreach (var b in candidate)
{
if (b is < 0x20 or > 0x7E && b != (byte)'\n' && b != (byte)'\r' && b != (byte)'\t')
{
return null;
}
}
return System.Text.Encoding.ASCII.GetString(candidate).Replace("\n", "\\n").Replace("\r", "\\r");
}
[SysAbiExport(
Nid = "CWVW78Qc3fI",
ExportName = "sceMsgDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "6fIC3XKt2k0",
ExportName = "sceMsgDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "Lr8ovHH9l6A",
ExportName = "sceMsgDialogGetResult",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogGetResult(CpuContext ctx)
{
var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> result = stackalloc byte[ResultSize];
result.Clear();
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "HTrcDKlFKuM",
ExportName = "sceMsgDialogClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogClose(CpuContext ctx)
{
Interlocked.Exchange(ref _status, StatusFinished);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
+2 -2
View File
@@ -93,7 +93,7 @@ public static class CxaGuardExports
}
[SysAbiExport(
Nid = "",
Nid = "9rAeANT2tyE",
ExportName = "__cxa_guard_release",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
@@ -142,7 +142,7 @@ public static class CxaGuardExports
}
[SysAbiExport(
Nid = "",
Nid = "2emaaluWzUw",
ExportName = "__cxa_guard_abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
+104
View File
@@ -0,0 +1,104 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.DiscMap;
/// <summary>
/// HLE implementation of libSceDiscMap, ported from Kyty's LibDiscMap.cpp
/// (https://github.com/InoriRus/Kyty, MIT). Titles installed to internal
/// storage query these entry points on nearly every file access to decide
/// whether a read must be redirected to the Blu-ray drive. Reporting every
/// request as already resident on HDD lets the regular file system path
/// service the read.
/// </summary>
public static class DiscMapExports
{
private const int DiscMapErrorInvalidArgument = unchecked((int)0x81100001);
private const int DiscMapErrorLocationNotMapped = unchecked((int)0x81100002);
private const int DiscMapErrorFileNotFound = unchecked((int)0x81100003);
private const int DiscMapErrorNoBitmapInfo = unchecked((int)0x81100004);
[SysAbiExport(
Nid = "lbQKqsERhtE",
ExportName = "sceDiscMapIsRequestOnHDD",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapIsRequestOnHDD(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var offset = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
var resultAddress = ctx[CpuRegister.Rcx];
if (pathAddress == 0 || resultAddress == 0)
{
return DiscMapErrorInvalidArgument;
}
if (!ctx.TryWriteInt32(resultAddress, 1))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceDiscMap(ctx, "sceDiscMapIsRequestOnHDD", pathAddress, offset, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fJgP+wqifno",
ExportName = "sceDiscMapUnknownFJgP",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
[SysAbiExport(
Nid = "ioKMruft1ek",
ExportName = "sceDiscMapUnknownIoKM",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
private static int WriteMappingTriple(CpuContext ctx, string exportName)
{
var pathAddress = ctx[CpuRegister.Rdi];
var offset = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
var flagsAddress = ctx[CpuRegister.Rcx];
var outAddress1 = ctx[CpuRegister.R8];
var outAddress2 = ctx[CpuRegister.R9];
if (pathAddress == 0 || flagsAddress == 0 || outAddress1 == 0 || outAddress2 == 0)
{
return DiscMapErrorInvalidArgument;
}
if (!ctx.TryWriteUInt64(flagsAddress, 0) ||
!ctx.TryWriteUInt64(outAddress1, 0) ||
!ctx.TryWriteUInt64(outAddress2, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceDiscMap(ctx, exportName, pathAddress, offset, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceDiscMap(CpuContext ctx, string exportName, ulong pathAddress, ulong offset, ulong size)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCMAP"), "1", StringComparison.Ordinal))
{
return;
}
var path = ctx.TryReadNullTerminatedUtf8(pathAddress, 1024, out var value)
? value
: $"<unreadable 0x{pathAddress:X16}>";
Console.Error.WriteLine($"[HLE][DISCMAP] {exportName} path={path} offset=0x{offset:X} size=0x{size:X}");
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace SharpEmu.Libs;
public static class HostTimerResolution
{
private const uint TargetPeriodMilliseconds = 1;
private static int _requested;
public static void Request()
{
if (Interlocked.Exchange(ref _requested, 1) != 0)
{
return;
}
if (!OperatingSystem.IsWindows())
{
return;
}
try
{
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
{
Console.Error.WriteLine(
"[LOADER][WARN] Host timer resolution request rejected; " +
"timed waits keep the default ~15.6 ms granularity.");
}
}
catch (DllNotFoundException exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
}
}
[SupportedOSPlatform("windows")]
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
private static extern uint TimeBeginPeriod(uint uPeriod);
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ime;
public static class ImeExports
{
// Quake (KEX) calls this from its main loop and from the audio bring-up path with
// an event-handler pointer. No IME session ever exists here, so report success
// without invoking the handler ("no pending IME events"). This NID was previously
// misbound as an sceNgs2VoiceControl alias, which fed the game NGS2 errors.
[SysAbiExport(
Nid = "-4GCfYdNF1s",
ExportName = "sceImeUpdate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceIme")]
public static int ImeUpdate(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eaFXjfJv3xs",
ExportName = "sceImeKeyboardOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceIme")]
public static int ImeKeyboardOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "dKadqZFgKKQ",
ExportName = "sceImeKeyboardGetResourceId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceIme")]
public static int ImeKeyboardGetResourceId(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+52
View File
@@ -78,6 +78,58 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json15InitParameter2C2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json15InitParameter212setAllocatorERNS0_12MemAllocatorE",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetAllocator(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json15InitParameter217setFileBufferSizeEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json12Initializer2C1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int Initializer2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
@@ -80,14 +80,8 @@ public static class KernelAprCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
{
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
// Completion output was written when the command was submitted.
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -149,6 +143,19 @@ public static class KernelAprCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Success stub: the argument layout is unknown and callers tolerate the
// empty answer (Quake streams fine), so no output payload is written until
// the real signature is reversed.
[SysAbiExport(
Nid = "WvEu7yl3Ivg",
ExportName = "sceKernelAprGetFileSize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprGetFileSize(CpuContext ctx)
{
return ctx.SetReturn(0);
}
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
{
Span<byte> result = stackalloc byte[sizeof(ulong)];
@@ -156,27 +163,6 @@ public static class KernelAprCompatExports
return ctx.Memory.TryWrite(resultAddress, result);
}
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
{
if (waitArg2 == 0)
{
return submittedResultAddress;
}
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
{
return submittedResultAddress;
}
return waitArg2;
}
private static bool IsAmprCompletionToken(ulong value)
{
var tag = value >> 56;
return tag is 0x0C or 0x10;
}
private static void TraceApr(
CpuContext ctx,
string operation,
@@ -11,6 +11,7 @@ public static class KernelEventQueueCompatExports
{
private const int KernelEventSize = 0x20;
public const short KernelEventFilterGraphics = -14;
public const short KernelEventFilterUser = -11;
public const short KernelEventFilterAmpr = -16;
public const short KernelEventFilterAmprSystem = -17;
@@ -89,8 +90,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddUserEventEdge(CpuContext ctx)
{
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
0);
TraceEventQueue(ctx, "add_user_edge", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -100,8 +109,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
0);
TraceEventQueue(ctx, "add_user", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -111,8 +128,15 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelDeleteUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var deleted = DeleteRegisteredEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser);
TraceEventQueue(ctx, "delete_user", handle);
return deleted
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -122,8 +146,18 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelTriggerUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var triggered = TriggerRegisteredEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
flags: 0x21,
fflags: 0,
data: ctx[CpuRegister.Rdx]);
TraceEventQueue(ctx, "trigger_user", handle);
return triggered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -475,6 +509,43 @@ public static class KernelEventQueueCompatExports
return triggeredCount;
}
private static bool TriggerRegisteredEvent(
ulong handle,
ulong ident,
short filter,
ushort flags,
uint fflags,
ulong data)
{
lock (_eventQueueGate)
{
if (!_registeredEvents.TryGetValue(handle, out var registrations) ||
!registrations.TryGetValue((ident, filter), out var registration))
{
return false;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new LinkedList<KernelQueuedEvent>();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
flags,
fflags,
data,
registration.UserData));
}
WakeEventQueue(handle);
return true;
}
public static bool TriggerDisplayEvent(
ulong handle,
ulong ident,
@@ -656,6 +727,6 @@ public static class KernelEventQueueCompatExports
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
}
+45 -13
View File
@@ -13,6 +13,8 @@ public static class KernelExports
private static readonly object _coredumpGate = new();
private static ulong _coredumpHandler;
private static ulong _coredumpHandlerContext;
private const uint Gen4CompiledSdkVersion = 0x05000000;
private const uint Gen5CompiledSdkVersion = 0x09000000;
private readonly record struct CxaDestructorEntry(
ulong Function,
@@ -26,7 +28,24 @@ public static class KernelExports
LibraryName = "libKernel")]
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
{
_ = ctx;
var versionAddress = ctx[CpuRegister.Rdi];
if (versionAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
? Gen5CompiledSdkVersion
: Gen4CompiledSdkVersion;
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -54,6 +73,21 @@ public static class KernelExports
LibraryName = "libKernel")]
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
[SysAbiExport(
Nid = "L1SBTkC+Cvw",
ExportName = "abort",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Abort(CpuContext ctx)
{
// Route through the same graceful guest-entry-exit path as exit(): letting the call
// fall through to the host's native abort() does not unwind the guest thread cleanly.
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "XKRegsFpEpk",
ExportName = "catchReturnFromMain",
@@ -244,20 +278,14 @@ public static class KernelExports
ExportName = "pthread_create",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreate(CpuContext ctx)
{
return PthreadCreate(ctx);
}
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
[SysAbiExport(
Nid = "Jmi+9w9u0E4",
ExportName = "pthread_create_name_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreateNameNp(CpuContext ctx)
{
return PthreadCreate(ctx);
}
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
[SysAbiExport(
Nid = "3kg7rT0NQIs",
@@ -334,10 +362,7 @@ public static class KernelExports
ExportName = "pthread_join",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadJoin(CpuContext ctx)
{
return PthreadJoin(ctx);
}
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
[SysAbiExport(
Nid = "wuCroIGjt2g",
@@ -435,4 +460,11 @@ public static class KernelExports
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
// shadowing them breaks every C++ static-init guard in the game.
}
@@ -53,6 +53,8 @@ public static class KernelMemoryCompatExports
private const int OrbisVirtualQueryInfoSize = 72;
private const int OrbisKernelMaximumNameLength = 32;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint MemRelease = 0x8000;
private const uint HostPageNoAccess = 0x01;
private const uint HostPageReadOnly = 0x02;
private const uint HostPageReadWrite = 0x04;
@@ -67,13 +69,6 @@ public static class KernelMemoryCompatExports
private const int Einval = 22;
private const int Erange = 34;
private const int Struncate = 80;
private const int ClockRealtime = 0;
private const int ClockVirtual = 1;
private const int ClockProf = 2;
private const int ClockMonotonic = 4;
private const int ClockUptime = 5;
private const int ClockRealtimeFast = 10;
private const int ClockMonotonicFast = 12;
private const nuint DefaultLibcHeapAlignment = 16;
private const ushort KernelStatModeDirectory = 0x41FF;
private const ushort KernelStatModeRegular = 0x81FF;
@@ -115,6 +110,15 @@ public static class KernelMemoryCompatExports
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
private static long _nextFileDescriptor = 2;
internal static int AllocateGuestFileDescriptor()
{
lock (_fdGate)
{
return (int)Interlocked.Increment(ref _nextFileDescriptor);
}
}
private static ulong _nextPhysicalAddress;
private static ulong _nextVirtualAddress;
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
@@ -128,6 +132,7 @@ public static class KernelMemoryCompatExports
private static int _hostMemoryWriteFallbackCount;
private static int _hostMemoryReadFallbackCount;
private static int _nullWcscpyRecoveryCount;
private static int _nullStrcasecmpRecoveryCount;
private static string? _cachedApp0Root;
private static string? _cachedDownload0Root;
@@ -150,6 +155,13 @@ public static class KernelMemoryCompatExports
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
private sealed class OpenDirectory
{
public required string Path { get; init; }
@@ -158,7 +170,7 @@ public static class KernelMemoryCompatExports
}
private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType);
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment);
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment, bool IsGuarded);
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
@@ -193,6 +205,14 @@ public static class KernelMemoryCompatExports
ulong length,
ulong alignment,
out ulong address)
=> TryAllocateHleData(ctx, length, alignment, OrbisProtCpuReadWrite, out address);
internal static bool TryAllocateHleData(
CpuContext ctx,
ulong length,
ulong alignment,
int protection,
out ulong address)
{
address = 0;
if (length == 0 || length > int.MaxValue)
@@ -207,7 +227,7 @@ public static class KernelMemoryCompatExports
var desiredAddress = AlignUp(
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
effectiveAlignment);
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, effectiveAlignment, out address) ||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, protection, effectiveAlignment, out address) ||
address == 0)
{
return false;
@@ -217,7 +237,7 @@ public static class KernelMemoryCompatExports
_mappedRegions[address] = new MappedRegion(
address,
mappedLength,
OrbisProtCpuReadWrite,
protection,
IsFlexible: false,
IsDirect: false,
DirectStart: 0);
@@ -238,6 +258,69 @@ public static class KernelMemoryCompatExports
return true;
}
private static ulong _dummyVtableAddress;
private const int DummyVtableSlotCount = 64;
// Some KexEngine objects (mutex wrappers, NGS2 system/rack/voice handles, etc.) are treated
// by guest code as C++ instances with a vtable pointer at offset 0. When an HLE constructor
// leaves that slot zeroed, the game's own virtual dispatch (call [ [obj]+N ]) reads a null
// vtable and jumps through address N, crashing (e.g. "CALL qword ptr [RAX+0x58]" with RAX=0).
// This allocates one shared, executable no-op stub plus a vtable of pointers to it, so any
// HLE object constructor can make virtual calls on its output land safely.
internal static bool TryWriteDummyVtable(CpuContext ctx, ulong objectAddress)
{
if (objectAddress == 0 || !TryEnsureDummyVtable(ctx, out var vtableAddress))
{
return false;
}
return ctx.TryWriteUInt64(objectAddress, vtableAddress);
}
private static bool TryEnsureDummyVtable(CpuContext ctx, out ulong vtableAddress)
{
lock (_memoryGate)
{
if (_dummyVtableAddress != 0)
{
vtableAddress = _dummyVtableAddress;
return true;
}
const int executableReadWrite = OrbisProtCpuRead | OrbisProtCpuWrite | OrbisProtCpuExec;
if (!TryAllocateHleData(ctx, 0x1000, 0x1000, executableReadWrite, out var block))
{
vtableAddress = 0;
return false;
}
// xor eax, eax; ret - every dummy virtual method just returns 0 and does nothing else.
if (!ctx.Memory.TryWrite(block, new byte[] { 0x31, 0xC0, 0xC3 }))
{
vtableAddress = 0;
return false;
}
var table = new byte[DummyVtableSlotCount * sizeof(ulong)];
for (var i = 0; i < DummyVtableSlotCount; i++)
{
BinaryPrimitives.WriteUInt64LittleEndian(table.AsSpan(i * sizeof(ulong), sizeof(ulong)), block);
}
var tableAddress = block + 0x100;
if (!ctx.Memory.TryWrite(tableAddress, table))
{
vtableAddress = 0;
return false;
}
Console.Error.WriteLine($"[LOADER][INFO] Dummy vtable ready: stub=0x{block:X16} vtable=0x{tableAddress:X16} slots={DummyVtableSlotCount}");
_dummyVtableAddress = tableAddress;
vtableAddress = tableAddress;
return true;
}
}
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
{
if (address == 0 || length == 0)
@@ -288,6 +371,18 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Longer null-dst memsets are unrecoverable, but RAX must still be set - leaving it
// stale here previously let callers that do `buf = memset(...)` carry on with a
// garbage "buffer" pointer instead of a clean NULL, causing a *different*,
// confusingly-located crash further downstream.
var largeRecoveryIndex = Interlocked.Increment(ref _nullMemsetRecoveryCount);
if (largeRecoveryIndex <= 8)
{
Console.Error.WriteLine(
$"[LOADER][WARNING] memset null-dst (len>0x20) recovery#{largeRecoveryIndex}: rip=0x{ctx.Rip:X16} len=0x{length:X} val=0x{value:X2}");
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
@@ -311,6 +406,7 @@ public static class KernelMemoryCompatExports
Console.WriteLine("!!! CRITICAL: Bad Memset Call !!!");
Console.WriteLine($"Called from RIP: 0x{ctx.Rip:X}");
Console.WriteLine($"dst=0x{destination:X} val=0x{value:X2} len=0x{length:X}");
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -543,6 +639,80 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "AV6ipCNa4Rw",
ExportName = "strcasecmp",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Strcasecmp(CpuContext ctx)
{
var left = ctx[CpuRegister.Rdi];
var right = ctx[CpuRegister.Rsi];
if (left == 0 || right == 0)
{
var recoveryIndex = Interlocked.Increment(ref _nullStrcasecmpRecoveryCount);
if (recoveryIndex <= 16)
{
var otherAddress = left == 0 ? right : left;
var otherText = otherAddress != 0 && TryReadNullTerminatedUtf8(ctx, otherAddress, 256, out var text)
? text
: "<unreadable>";
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
Console.Error.WriteLine(
$"[LOADER][WARNING] strcasecmp null-arg recovery#{recoveryIndex}: ret=0x{returnRip:X16} left=0x{left:X16} right=0x{right:X16} other=\"{otherText}\"");
}
// Real strcasecmp(NULL, x) is undefined behaviour and previously crashed inside the
// LLE-routed implementation. Treat it as "not equal" instead so callers doing
// `if (strcasecmp(a, b) == 0)` degrade gracefully rather than taking down the guest.
ctx[CpuRegister.Rax] = left == right ? 0uL : 1uL;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryCompareStringsCaseInsensitive(ctx, left, right, limit: ulong.MaxValue, out var compare))
{
ctx[CpuRegister.Rax] = 1;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryCompareStringsCaseInsensitive(CpuContext ctx, ulong left, ulong right, ulong limit, out int compare)
{
compare = 0;
if (left == 0 || right == 0)
{
return false;
}
var max = limit == ulong.MaxValue ? 1_048_576UL : Math.Min(limit, 1_048_576UL);
Span<byte> leftByte = stackalloc byte[1];
Span<byte> rightByte = stackalloc byte[1];
for (ulong i = 0; i < max; i++)
{
if (!TryReadCompat(ctx, left + i, leftByte) ||
!TryReadCompat(ctx, right + i, rightByte))
{
return false;
}
var leftLower = ToAsciiLower(leftByte[0]);
var rightLower = ToAsciiLower(rightByte[0]);
compare = leftLower - rightLower;
if (compare != 0 || leftByte[0] == 0 || rightByte[0] == 0)
{
return true;
}
}
compare = 0;
return true;
}
private static byte ToAsciiLower(byte value) => value is >= (byte)'A' and <= (byte)'Z' ? (byte)(value + 32) : value;
[SysAbiExport(
Nid = "0nV21JjYCH8",
ExportName = "wcsncpy",
@@ -594,6 +764,54 @@ public static class KernelMemoryCompatExports
return VsnprintfCore(ctx);
}
// sprintf/vsprintf are served from the same HLE formatting engine as snprintf/vsnprintf
// instead of falling through to the game's bundled libc.
[SysAbiExport(
Nid = "tcVi5SivF7Q",
ExportName = "sprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Sprintf(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var format = Encoding.UTF8.GetString(formatBytes);
var rendered = FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
}
[SysAbiExport(
Nid = "jbz9I9vkqkk",
ExportName = "vsprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Vsprintf(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
var vaListAddress = ctx[CpuRegister.Rdx];
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var format = Encoding.UTF8.GetString(formatBytes);
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
{
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, formatBytes);
}
var rendered = FormatString(ctx, format, ref vaCursor);
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
}
[SysAbiExport(
Nid = "nJz16JE1txM",
ExportName = "swprintf",
@@ -952,15 +1170,27 @@ public static class KernelMemoryCompatExports
{
var destination = ctx[CpuRegister.Rdi];
var source = ctx[CpuRegister.Rsi];
var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
if (count < 0)
var rawCount = ctx[CpuRegister.Rdx];
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
// upstream bug that also feeds bad lengths to memset) must not reach
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
// exception. Reject anything above a sane bound before allocating.
const ulong maxSaneCount = 512UL * 1024 * 1024;
if (rawCount > maxSaneCount)
{
Console.Error.WriteLine($"[LOADER][WARNING] memcpy oversized count rejected: dst=0x{destination:X16} src=0x{source:X16} count=0x{rawCount:X}");
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var count = (int)rawCount;
var payload = GC.AllocateUninitializedArray<byte>(count);
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
{
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1203,6 +1433,24 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Was unresolved (returning the 0x80020002 sentinel, then crashing when the guest
// dereferenced it) - the game's own heap instrumentation calls this hook when it
// detects a corrupted/invalid block, not the emulator's allocator, so this is purely
// a diagnostic sink: log what was reported and return success so the caller continues.
[SysAbiExport(
Nid = "al3JzFI9MQ0",
ExportName = "sceLibcInternalHeapErrorReportForGame",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceLibcInternal")]
public static int LibcInternalHeapErrorReportForGame(CpuContext ctx)
{
Console.Error.WriteLine(
$"[LOADER][WARN] sceLibcInternalHeapErrorReportForGame: rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16}");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "DfivPArhucg",
ExportName = "memcmp",
@@ -1316,9 +1564,10 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var directoryFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
int directoryFd;
lock (_fdGate)
{
directoryFd = AllocateGuestFileDescriptor();
_openDirectories[directoryFd] = new OpenDirectory
{
Path = hostPath,
@@ -1339,9 +1588,10 @@ public static class KernelMemoryCompatExports
stream.Seek(0, SeekOrigin.End);
}
var fd = (int)Interlocked.Increment(ref _nextFileDescriptor);
int fd;
lock (_fdGate)
{
fd = AllocateGuestFileDescriptor();
_openFiles[fd] = stream;
}
@@ -1450,15 +1700,15 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var entryCount = (int)count;
Span<uint> localIds = count <= 256 ? stackalloc uint[entryCount] : new uint[entryCount];
Span<ulong> localSizes = count <= 128 ? stackalloc ulong[entryCount] : new ulong[entryCount];
var resolvedGuestPaths = new string[entryCount];
var resolvedHostPaths = new string[entryCount];
for (ulong i = 0; i < count; i++)
{
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var index = (int)i;
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
@@ -1473,23 +1723,47 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
var fileId = AmprFileRegistry.ComputeFileId(guestPath);
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
localIds[index] = fileId;
localSizes[index] = fileSize;
resolvedGuestPaths[index] = guestPath;
resolvedHostPaths[index] = hostPath;
}
Span<byte> sizePayload = count <= 64 ? stackalloc byte[entryCount * sizeof(ulong)] : new byte[entryCount * sizeof(ulong)];
for (ulong i = 0; i < count; i++)
{
BinaryPrimitives.WriteUInt64LittleEndian(sizePayload[(int)(i * sizeof(ulong))..], localSizes[(int)i]);
}
if (!TryWriteCompat(ctx, sizesAddress, sizePayload))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (idsAddress != 0)
{
Span<byte> idPayload = count <= 128 ? stackalloc byte[entryCount * sizeof(uint)] : new byte[entryCount * sizeof(uint)];
for (ulong i = 0; i < count; i++)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
BinaryPrimitives.WriteUInt32LittleEndian(idPayload[(int)(i * sizeof(uint))..], localIds[(int)i]);
}
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
if (!TryWriteCompat(ctx, idsAddress, idPayload))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
for (var i = 0; i < entryCount; i++)
{
AmprFileRegistry.Register(resolvedGuestPaths[i], resolvedHostPaths[i]);
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1690,6 +1964,12 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (KernelSocketCompatExports.TryCloseSocketFd(fd))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
FileStream? stream;
lock (_fdGate)
{
@@ -1733,6 +2013,17 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (KernelSocketCompatExports.TryReadSocketFd(ctx, fd, bufferAddress, requested, out var socketBytesRead, out var socketError))
{
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
{
return (int)socketError;
}
ctx[CpuRegister.Rax] = socketBytesRead;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
FileStream? stream;
lock (_fdGate)
{
@@ -1962,6 +2253,17 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (KernelSocketCompatExports.TryWriteSocketFd(ctx, fd, payload, out var socketError))
{
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
{
return (int)socketError;
}
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
FileStream? stream;
lock (_fdGate)
{
@@ -2011,29 +2313,11 @@ public static class KernelMemoryCompatExports
long seconds;
long nanoseconds;
switch (clockId)
if (!KernelRuntimeCompatExports.ResolveClockTime(clockId, out seconds, out nanoseconds))
{
case ClockRealtime:
case ClockRealtimeFast:
case ClockVirtual:
case ClockProf:
{
var now = DateTimeOffset.UtcNow;
seconds = now.ToUnixTimeSeconds();
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
break;
}
case ClockMonotonic:
case ClockMonotonicFast:
case ClockUptime:
KernelRuntimeCompatExports.GetProcessMonotonicTime(out seconds, out nanoseconds);
break;
default:
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
return -1;
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
return -1;
}
Span<byte> timespecBuffer = stackalloc byte[16];
@@ -3196,6 +3480,18 @@ public static class KernelMemoryCompatExports
return true;
}
internal static bool TryFormatStringFromVaList(CpuContext ctx, string format, ulong vaListAddress, out string rendered)
{
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
{
rendered = format;
return false;
}
rendered = FormatString(ctx, format, ref vaCursor);
return true;
}
private static int WriteSnprintfOutput(
CpuContext ctx,
ulong destination,
@@ -4176,7 +4472,7 @@ public static class KernelMemoryCompatExports
return FileMode.Open;
}
private static string ResolveGuestPath(string guestPath)
public static string ResolveGuestPath(string guestPath)
{
if (string.IsNullOrWhiteSpace(guestPath))
{
@@ -4500,7 +4796,7 @@ public static class KernelMemoryCompatExports
private static bool IsMutatingOpen(int flags) =>
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
private static bool IsReadOnlyGuestMutationPath(string guestPath)
public static bool IsReadOnlyGuestMutationPath(string guestPath)
{
var normalized = NormalizeGuestStatCachePath(guestPath);
return normalized is not null &&
@@ -4779,7 +5075,7 @@ public static class KernelMemoryCompatExports
return destination == 0 || destinationCount == 0 || TryWriteWideTerminator(ctx, destination);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
public static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0 || maxLength <= 0)
@@ -5568,6 +5864,26 @@ public static class KernelMemoryCompatExports
alignment = NormalizeLibcAlignment(alignment);
var actualSize = requestedSize == 0 ? 1u : requestedSize;
// This intentionally uses host guard pages for allocations made through the libc HLE.
// It turns an overrun that reaches the next page into an immediate access violation at
// the offending guest instruction, rather than allowing it to poison a later import.
var guardHeap = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_GUARD_HEAP"),
"1",
StringComparison.Ordinal);
if (guardHeap && TryAllocateGuardedLibcHeap(actualSize, alignment, zeroFill, out address))
{
return true;
}
// Do not silently fall back to an unguarded allocation when guard heap was explicitly
// requested: a failed setup must remain visible to the caller/reproducer.
if (guardHeap)
{
return false;
}
nuint totalSize;
try
{
@@ -5603,7 +5919,7 @@ public static class KernelMemoryCompatExports
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)IntPtr.Size, (ulong)alignment);
lock (_libcAllocGate)
{
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment);
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: false);
}
try
@@ -5623,6 +5939,51 @@ public static class KernelMemoryCompatExports
return true;
}
private static unsafe bool TryAllocateGuardedLibcHeap(nuint actualSize, nuint alignment, bool zeroFill, out ulong address)
{
address = 0;
const nuint pageSize = 0x1000;
try
{
var effectiveAlignment = Math.Max(alignment, pageSize);
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
if (baseAddress == 0)
{
return false;
}
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
var guardAddress = alignedAddress + (ulong)usableSize;
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
{
_ = VirtualFree(baseAddress, 0, MemRelease);
return false;
}
lock (_libcAllocGate)
{
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: true);
}
if (zeroFill)
{
NativeMemory.Clear((void*)alignedAddress, actualSize);
}
Console.Error.WriteLine(
$"[LOADER][TRACE] guard-heap alloc: ptr=0x{alignedAddress:X16} size=0x{actualSize:X} guard=0x{guardAddress:X16}");
address = alignedAddress;
return true;
}
catch (OverflowException)
{
return false;
}
}
private static unsafe bool TryReallocateLibcHeap(ulong existingAddress, ulong requestedSize, out ulong resizedAddress)
{
resizedAddress = 0;
@@ -5731,7 +6092,14 @@ public static class KernelMemoryCompatExports
}
}
Marshal.FreeHGlobal(allocation.BaseAddress);
if (allocation.IsGuarded)
{
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
}
else
{
Marshal.FreeHGlobal(allocation.BaseAddress);
}
}
private static bool TryMultiplyAllocationSize(ulong left, ulong right, out nuint size)
@@ -6057,19 +6425,19 @@ public static class KernelMemoryCompatExports
}
var currentIndex = directory.NextIndex;
if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (currentIndex >= directory.Entries.Length)
{
if (basePointerAddress != 0 &&
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var entryName = directory.Entries[currentIndex];
directory.NextIndex = currentIndex + 1;
var entryBytes = Encoding.UTF8.GetBytes(entryName);
var nameLength = Math.Min(entryBytes.Length, 255);
@@ -6088,6 +6456,13 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (basePointerAddress != 0 &&
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
directory.NextIndex = currentIndex + 1;
ctx[CpuRegister.Rax] = 512;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -6337,4 +6712,15 @@ public static class KernelMemoryCompatExports
sum = left + right;
return sum >= left;
}
[SysAbiExport(
Nid = "KMcEa+rHsIo",
ExportName = "sceKernelMapMemory",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelMapMemory(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -3,6 +3,7 @@
using System.Collections.Concurrent;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -24,6 +25,8 @@ public static class KernelPthreadCompatExports
private static readonly object _stateGate = new();
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
private static readonly ConcurrentDictionary<ulong, string> _mutexWakeKeys = new();
private static readonly ConcurrentDictionary<ulong, string> _condWakeKeys = new();
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
private static readonly Dictionary<ulong, object> _onceGates = new();
@@ -35,6 +38,15 @@ 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 readonly bool _enableMutexLockBlocking =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_MUTEX_LOCK_BLOCKING"), "1", StringComparison.Ordinal);
// On the outer class deliberately: a static on the nested state classes gives them a
// type initializer that first runs on a guest thread and fail-fasts the CLR.
private static long _nextMutexWakeId;
private static long _nextCondWakeId;
private static readonly ConcurrentDictionary<ulong, long> _posixTimedWaitCounts = new();
private static readonly ConcurrentDictionary<ulong, byte> _reportedContendedMutexes = new();
private sealed class PthreadMutexState
{
@@ -43,6 +55,10 @@ public static class KernelPthreadCompatExports
public int RecursionCount { get; set; }
public int Type { get; set; } = MutexTypeErrorCheck;
public int Protocol { get; set; }
// Keyed on state identity, not the resolved address (which can differ between a
// lock and its unlock for the same state, stranding the waiter).
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
}
private sealed class PthreadMutexWaiter
@@ -56,6 +72,23 @@ public static class KernelPthreadCompatExports
public object SyncRoot { get; } = new();
public ulong SignalEpoch { get; set; }
public int Waiters { get; set; }
// See PthreadMutexState.WakeKey.
public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X");
// A signal with no waiter stays pending; the guest often signals before the wait.
public int PendingSignals { get; set; }
public bool TryConsumePendingSignal()
{
if (PendingSignals <= 0)
{
return false;
}
PendingSignals--;
return true;
}
}
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
@@ -107,11 +140,32 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PthreadYield(CpuContext ctx)
{
_ = ctx;
GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield");
Thread.Yield();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GBUY7ywdULE",
ExportName = "scePthreadRename",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadRename(CpuContext ctx)
{
if (_tracePthreads)
{
var nameAddress = ctx[CpuRegister.Rsi];
var name = nameAddress != 0 &&
KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, nameAddress, 64, out var value)
? value
: "<unreadable>";
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread.rename thread=0x{ctx[CpuRegister.Rdi]:X16} name=\"{name}\"");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "EI-5-jlq2dE",
ExportName = "scePthreadGetthreadid",
@@ -293,6 +347,104 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
[SysAbiExport(
Nid = "27bAgiJmOh0",
ExportName = "pthread_cond_timedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondTimedwait(CpuContext ctx)
{
var absoluteTimeAddress = ctx[CpuRegister.Rdx];
if (absoluteTimeAddress == 0 ||
!ctx.TryReadUInt64(absoluteTimeAddress, out var secondsRaw) ||
!ctx.TryReadUInt64(absoluteTimeAddress + sizeof(ulong), out var nanosecondsRaw))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var deadlineSeconds = unchecked((long)secondsRaw);
var deadlineNanoseconds = unchecked((long)nanosecondsRaw);
if (deadlineSeconds < 0 || deadlineNanoseconds is < 0 or >= 1_000_000_000)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
long nowSeconds;
long nowNanoseconds;
if (deadlineSeconds >= 1_000_000_000)
{
var now = DateTimeOffset.UtcNow;
nowSeconds = now.ToUnixTimeSeconds();
nowNanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
}
else
{
// Engine condition variables use monotonic deadlines.
KernelRuntimeCompatExports.GetProcessMonotonicTime(out nowSeconds, out nowNanoseconds);
}
var remainingNanoseconds =
((Int128)deadlineSeconds * 1_000_000_000 + deadlineNanoseconds) -
((Int128)nowSeconds * 1_000_000_000 + nowNanoseconds);
var timeoutUsec = remainingNanoseconds <= 0
? 0u
: (uint)Int128.Min(uint.MaxValue, (remainingNanoseconds + 999) / 1_000);
TracePosixTimedWait(
ctx,
deadlineSeconds,
deadlineNanoseconds,
nowSeconds,
nowNanoseconds,
remainingNanoseconds,
timeoutUsec);
return PthreadCondWaitCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rsi],
timed: true,
timeoutUsec,
posixResult: true);
}
private static void TracePosixTimedWait(
CpuContext ctx,
long deadlineSeconds,
long deadlineNanoseconds,
long nowSeconds,
long nowNanoseconds,
Int128 remainingNanoseconds,
uint timeoutUsec)
{
var condAddress = ctx[CpuRegister.Rdi];
var count = _posixTimedWaitCounts.AddOrUpdate(condAddress, 1, static (_, current) => current + 1);
if (count != 1 && count % 1_000_000 != 0)
{
return;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_cond_timedwait#{count}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
$"cond=0x{condAddress:X16} mutex=0x{ctx[CpuRegister.Rsi]:X16} abstime=0x{ctx[CpuRegister.Rdx]:X16} " +
$"deadline={deadlineSeconds}.{deadlineNanoseconds:D9} now={nowSeconds}.{nowNanoseconds:D9} " +
$"remaining_ns={remainingNanoseconds} timeout_us={timeoutUsec} ret=0x{returnRip:X16}");
if (GuestThreadExecution.Scheduler is not { } scheduler)
{
return;
}
foreach (var snapshot in scheduler.SnapshotThreads())
{
Console.Error.WriteLine(
$"[LOADER][DIAG] guest_thread handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
}
}
[SysAbiExport(
Nid = "kDh-NfxgMtE",
ExportName = "scePthreadCondSignal",
@@ -327,12 +479,6 @@ public static class KernelPthreadCompatExports
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
[SysAbiExport(
Nid = "27bAgiJmOh0",
ExportName = "pthread_cond_timedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
[SysAbiExport(
Nid = "m5-2bsNfv7s",
@@ -559,42 +705,43 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
// Normal/adaptive mutexes do not report EDEADLK on self-lock.
// Under the current single-host-thread guest execution model,
// treating them as nested ownership keeps init paths moving
// without turning a would-block path into a hard error.
state.RecursionCount++;
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var ownedResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
return ownedResult;
else
{
var ownedResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
return ownedResult;
}
}
}
var acquired = state.Semaphore.Wait(0);
if (!acquired)
{
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
// Demon's Souls deadlocks during PS5SyncEvent initialization.
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
if (!tryOnly &&
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
// Fibers retain the synchronous fallback to preserve switch state.
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
if (canCooperativelyBlock &&
!tryOnly &&
GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_mutex_lock",
GetMutexWakeKey(resolvedAddress),
state.WakeKey,
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
{
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} */
}
if (!tryOnly)
{
@@ -611,6 +758,7 @@ public static class KernelPthreadCompatExports
lock (state)
{
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "lock");
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
}
@@ -619,6 +767,53 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceContendedMutex(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
ulong currentThreadId)
{
if (!_reportedContendedMutexes.TryAdd(resolvedAddress, 0))
{
return;
}
ulong ownerThreadId;
int recursionCount;
int type;
lock (state)
{
ownerThreadId = state.OwnerThreadId;
recursionCount = state.RecursionCount;
type = state.Type;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_mutex_contended: waiter=0x{currentThreadId:X16} owner=0x{ownerThreadId:X16} " +
$"mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} recursion={recursionCount} " +
$"type={type} semaphore_count={state.Semaphore.CurrentCount} ret=0x{returnRip:X16}");
if (GuestThreadExecution.Scheduler is not { } scheduler)
{
return;
}
foreach (var snapshot in scheduler.SnapshotThreads())
{
if (snapshot.ThreadHandle != currentThreadId && snapshot.ThreadHandle != ownerThreadId)
{
continue;
}
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_mutex_party handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
}
}
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
{
if (mutexAddress == 0)
@@ -633,16 +828,24 @@ public static class KernelPthreadCompatExports
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
var lenientOwnership = state.Type is MutexTypeNormal or MutexTypeAdaptiveNp;
var shouldRelease = false;
lock (state)
{
if (state.RecursionCount <= 0)
{
if (lenientOwnership)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (requireOwner && state.OwnerThreadId != currentThreadId)
if (requireOwner && !lenientOwnership && state.OwnerThreadId != currentThreadId)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
@@ -661,12 +864,15 @@ public static class KernelPthreadCompatExports
try
{
state.Semaphore.Release();
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey, 1);
}
catch (SemaphoreFullException)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
if (!lenientOwnership)
{
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
}
@@ -987,6 +1193,13 @@ public static class KernelPthreadCompatExports
lock (_stateGate)
{
if (_condStates.TryGetValue(condAddress, out var raced))
{
resolvedAddress = condAddress;
state = raced;
return true;
}
_condStates[condAddress] = createdState;
_condStates[handle] = createdState;
}
@@ -1083,27 +1296,41 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed, uint timeoutUsec = 0)
private static int PthreadCondWaitCore(
CpuContext ctx,
ulong condAddress,
ulong mutexAddress,
bool timed,
uint timeoutUsec = 0,
bool posixResult = false)
{
if (condAddress == 0 || mutexAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
var spuriousWake = false;
var releasedRecursion = 0;
lock (state.SyncRoot)
{
state.Waiters++;
var observedEpoch = state.SignalEpoch;
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, waitResult);
var consumedPendingSignal = state.TryConsumePendingSignal();
TracePthreadCond(
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
condAddress,
mutexAddress,
state,
timed,
waitResult);
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
var unlockResult = PthreadMutexFullUnlockForCondWait(ctx, mutexAddress, out releasedRecursion);
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
state.Waiters--;
@@ -1111,10 +1338,36 @@ public static class KernelPthreadCompatExports
return unlockResult;
}
var scheduler = GuestThreadExecution.Scheduler;
if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait"))
if (consumedPendingSignal)
{
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, waitResult);
state.Waiters = Math.Max(0, state.Waiters - 1);
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
// Relock outside SyncRoot to preserve lock ordering.
Monitor.Exit(state.SyncRoot);
try
{
var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult;
}
finally
{
// Balance the surrounding lock statement.
Monitor.Enter(state.SyncRoot);
}
}
var scheduler = GuestThreadExecution.Scheduler;
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
state.WakeKey,
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
() => state.SignalEpoch != observedEpoch,
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
{
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
return waitResult;
}
@@ -1137,6 +1390,21 @@ public static class KernelPthreadCompatExports
{
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
{
if (scheduler is not null && !GuestThreadExecution.IsGuestThread)
{
Monitor.Exit(state.SyncRoot);
try
{
scheduler.Pump(ctx, "pthread_cond_wait");
}
finally
{
Monitor.Enter(state.SyncRoot);
}
continue;
}
spuriousWake = true;
break;
}
@@ -1163,7 +1431,7 @@ public static class KernelPthreadCompatExports
waitResult);
}
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
@@ -1179,7 +1447,7 @@ public static class KernelPthreadCompatExports
state,
timed,
waitResult);
return waitResult;
return TranslatePthreadResult(waitResult, posixResult);
}
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
@@ -1189,16 +1457,19 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var shouldWakeScheduler = false;
lock (state.SyncRoot)
{
// Advance the epoch even with no waiter registered — it's the wait predicate.
state.SignalEpoch++;
if (state.Waiters > 0)
{
state.SignalEpoch++;
shouldWakeScheduler = true;
if (broadcast)
{
Monitor.PulseAll(state.SyncRoot);
@@ -1208,15 +1479,161 @@ public static class KernelPthreadCompatExports
Monitor.Pulse(state.SyncRoot);
}
}
else
{
state.PendingSignals++;
}
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
if (shouldWakeScheduler)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(
state.WakeKey,
broadcast ? int.MaxValue : 1);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int ResumePthreadCondWait(
CpuContext ctx,
ulong condAddress,
ulong mutexAddress,
PthreadCondState state,
ulong observedEpoch,
bool timed,
int releasedRecursion,
bool posixResult)
{
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
lock (state.SyncRoot)
{
state.Waiters = Math.Max(0, state.Waiters - 1);
if (timed && state.SignalEpoch == observedEpoch)
{
waitResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
}
TracePthreadCond(
waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-resume" : "wait-resume-timeout",
condAddress,
mutexAddress,
state,
timed,
waitResult);
}
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
return TranslatePthreadResult(
lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult,
posixResult);
}
private static int TranslatePthreadResult(int result, bool posixResult)
{
if (!posixResult || result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
return result switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => 1, // EPERM
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND => 2, // ENOENT
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => 22, // EINVAL
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK => 11, // EDEADLK
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => 13, // EACCES
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY => 16, // EBUSY
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => 35, // EAGAIN
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => 60, // ETIMEDOUT
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => 85, // ECANCELED
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => 14, // EFAULT
_ => result
};
}
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
_condWakeKeys.GetOrAdd(resolvedCondAddress, static address => string.Create(
31,
address,
static (destination, value) =>
{
"pthread_cond:0x".AsSpan().CopyTo(destination);
_ = value.TryFormat(destination[15..], out _, "X16");
}));
private static int PthreadMutexFullUnlockForCondWait(CpuContext ctx, ulong mutexAddress, out int releasedRecursion)
{
releasedRecursion = 0;
if (!TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
{
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
lock (state)
{
if (state.RecursionCount <= 0 || state.OwnerThreadId != currentThreadId)
{
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
}
releasedRecursion = state.RecursionCount;
state.RecursionCount = 1;
}
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
}
private static int PthreadMutexRelockForCondWait(CpuContext ctx, ulong mutexAddress, int releasedRecursion)
{
var result = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
if (releasedRecursion > 1 &&
TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
{
lock (state)
{
if (state.OwnerThreadId == KernelPthreadState.GetCurrentThreadHandle())
{
state.RecursionCount = releasedRecursion;
}
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static string? DumpMutexStateForStall(ulong mutexAddress)
{
if (!_mutexStates.TryGetValue(mutexAddress, out var state))
{
return null;
}
var waiterThreads = string.Join(",", _mutexStates
.Where(pair => ReferenceEquals(pair.Value, state))
.Select(pair => $"0x{pair.Key:X}"));
return $"mutex=0x{mutexAddress:X16} owner=0x{state.OwnerThreadId:X} recursion={state.RecursionCount} " +
$"type={state.Type} semaphore_count={state.Semaphore.CurrentCount} aliases=[{waiterThreads}]";
}
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
_mutexWakeKeys.GetOrAdd(resolvedMutexAddress, static address => string.Create(
32,
address,
static (destination, value) =>
{
"pthread_mutex:0x".AsSpan().CopyTo(destination);
_ = value.TryFormat(destination[16..], out _, "X16");
}));
private static bool TryReserveBlockedMutexLock(
CpuContext ctx,
@@ -1233,6 +1650,13 @@ public static class KernelPthreadCompatExports
return false;
}
if (!state.Semaphore.Wait(0))
{
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return false;
}
DetectMutexDoubleOwner(resolvedAddress, state, waiter.ThreadId, "reserve");
state.OwnerThreadId = waiter.ThreadId;
state.RecursionCount = 1;
Interlocked.Exchange(ref waiter.Reserved, 1);
@@ -1264,6 +1688,7 @@ public static class KernelPthreadCompatExports
lock (state)
{
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "resume");
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
}
@@ -1272,6 +1697,19 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// A nonzero foreign prior owner means two guest threads are in the same critical
// section. Call while holding lock(state).
private static void DetectMutexDoubleOwner(ulong resolvedAddress, PthreadMutexState state, ulong currentThreadId, string site)
{
var priorOwner = state.OwnerThreadId;
if (priorOwner != 0 && priorOwner != currentThreadId)
{
Console.Error.WriteLine(
$"[LOADER][ERROR] MUTEX DOUBLE-OWNER at {site}: resolved=0x{resolvedAddress:X} acquirer=0x{currentThreadId:X} " +
$"prior_owner=0x{priorOwner:X} recursion={state.RecursionCount} type={state.Type} sem_count={state.Semaphore.CurrentCount}");
}
}
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
{
if (timeoutUsec == 0)
@@ -1279,7 +1717,10 @@ public static class KernelPthreadCompatExports
return TimeSpan.Zero;
}
return TimeSpan.FromTicks((long)timeoutUsec * 10L);
// Round positive sub-millisecond waits to the host sleep quantum.
return timeoutUsec < 1_000
? TimeSpan.FromMilliseconds(1)
: TimeSpan.FromTicks((long)timeoutUsec * 10L);
}
private static TimeSpan GetCondSpuriousWakeTimeout()
@@ -1409,7 +1850,8 @@ public static class KernelPthreadCompatExports
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8} " +
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} managed={Environment.CurrentManagedThreadId}");
}
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
@@ -1420,7 +1862,8 @@ public static class KernelPthreadCompatExports
}
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_cond_{operation}: cond=0x{condAddress:X16} mutex=0x{mutexAddress:X16} " +
$"[LOADER][TRACE] pthread_cond_{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
$"cond=0x{condAddress:X16} key={state?.WakeKey ?? "none"} mutex=0x{mutexAddress:X16} " +
$"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}");
}
@@ -31,6 +31,8 @@ public static class KernelPthreadExtendedCompatExports
private static long _nextSyntheticRwlockHandleId = 1;
private static long _nextSyntheticPthreadAttrHandleId = 1;
private static long _nextSyntheticRwlockAttrHandleId = 1;
private static readonly bool _strictRwlockWriterPreference =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE"), "1", StringComparison.Ordinal);
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
@@ -85,14 +87,23 @@ 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();
public Dictionary<ulong, int> ReaderCounts { get; } = new();
public Dictionary<ulong, int> CompatWriterCounts { get; } = new();
public int ReaderTotalCount { get; set; }
public int CompatWriterTotalCount { get; set; }
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)
{
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
@@ -105,6 +116,33 @@ public static class KernelPthreadExtendedCompatExports
ReaderTotalCount++;
}
public void AddCompatWriter(ulong threadId)
{
CompatWriterCounts.TryGetValue(threadId, out var currentCount);
CompatWriterCounts[threadId] = currentCount + 1;
CompatWriterTotalCount++;
}
public bool RemoveCompatWriter(ulong threadId)
{
if (!CompatWriterCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
{
return false;
}
if (currentCount == 1)
{
CompatWriterCounts.Remove(threadId);
}
else
{
CompatWriterCounts[threadId] = currentCount - 1;
}
CompatWriterTotalCount = Math.Max(0, CompatWriterTotalCount - 1);
return true;
}
public bool RemoveReader(ulong threadId)
{
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
@@ -935,7 +973,7 @@ public static class KernelPthreadExtendedCompatExports
lock (state.SyncRoot)
{
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0 || state.CompatWriterTotalCount != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
@@ -1003,7 +1041,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out var resolvedAddress, out var rwlock))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -1014,7 +1052,11 @@ public static class KernelPthreadExtendedCompatExports
{
lock (rwlock.SyncRoot)
{
if (rwlock.WriterThreadId == currentThreadId)
if (rwlock.RemoveCompatWriter(currentThreadId))
{
Monitor.PulseAll(rwlock.SyncRoot);
}
else if (rwlock.WriterThreadId == currentThreadId)
{
rwlock.WriterThreadId = 0;
Monitor.PulseAll(rwlock.SyncRoot);
@@ -1037,6 +1079,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1227,7 +1270,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -1242,20 +1285,60 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
{
rwlock.AddCompatWriter(currentThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (GuestThreadExecution.IsGuestThread &&
!_strictRwlockWriterPreference &&
rwlock.WriterThreadId == 0 &&
rwlock.ReaderTotalCount == 0 &&
rwlock.CompatWriterTotalCount == 0)
{
rwlock.AddCompatWriter(currentThreadId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (rwlock.WriterThreadId == 0 && rwlock.ReaderTotalCount == 0 && rwlock.CompatWriterTotalCount == 0)
{
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "wrlock");
rwlock.WriterThreadId = currentThreadId;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
rwlock.WaitingWriters++;
var transferredToScheduler = false;
try
{
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_wrlock",
rwlock.WakeKey,
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
() => TryAcquireBlockedRwlock(rwlock, 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);
}
rwlock.WriterThreadId = currentThreadId;
}
finally
{
rwlock.WaitingWriters--;
if (!transferredToScheduler)
{
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
}
}
rwlock.WriterThreadId = currentThreadId;
}
else
{
@@ -1264,12 +1347,30 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
while (rwlock.WriterThreadId != 0 ||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
{
if (GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_rwlock_rdlock",
rwlock.WakeKey,
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Monitor.Wait(rwlock.SyncRoot);
}
if (rwlock.WriterThreadId != 0 ||
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] RWLOCK READER/WRITER COEXIST: resolved=0x{resolvedAddress:X} reader=0x{currentThreadId:X} " +
$"writer=0x{rwlock.WriterThreadId:X} compat_total={rwlock.CompatWriterTotalCount} readers_total={rwlock.ReaderTotalCount}");
}
rwlock.AddReader(currentThreadId);
}
}
@@ -1277,6 +1378,86 @@ 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)
{
if (rwlock.WriterThreadId != 0 ||
rwlock.ReaderTotalCount != 0 ||
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] RWLOCK WRITER CONFLICT at {site}: resolved=0x{resolvedAddress:X} writer=0x{currentThreadId:X} " +
$"existing_writer=0x{rwlock.WriterThreadId:X} readers_total={rwlock.ReaderTotalCount} compat_total={rwlock.CompatWriterTotalCount}");
}
}
private static bool ReaderMustWaitForRwlock(PthreadRwlockState rwlock, ulong currentThreadId)
{
if (rwlock.WriterThreadId != 0)
{
return true;
}
if (rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
{
return true;
}
return rwlock.WaitingWriters > 0 &&
rwlock.GetReaderCount(currentThreadId) == 0;
}
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
{
PthreadRwlockState? rwlock;
lock (_stateGate)
{
if (!_rwlockStates.TryGetValue(rwlockAddress, out rwlock))
{
return null;
}
}
lock (rwlock.SyncRoot)
{
var readers = string.Join(",", rwlock.ReaderCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
var compatWriters = string.Join(",", rwlock.CompatWriterCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
return $"rwlock=0x{rwlockAddress:X16} writer=0x{rwlock.WriterThreadId:X} waiting_writers={rwlock.WaitingWriters} " +
$"readers_total={rwlock.ReaderTotalCount} readers=[{readers}] " +
$"compat_writers_total={rwlock.CompatWriterTotalCount} compat_writers=[{compatWriters}]";
}
}
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
{
if (rwlockAddress == 0)
@@ -14,6 +14,22 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelRuntimeCompatExports
{
internal const int ClockRealtime = 0;
internal const int ClockVirtual = 1;
internal const int ClockProf = 2;
internal const int ClockMonotonic = 4;
internal const int ClockUptime = 5;
internal const int ClockUptimePrecise = 7;
internal const int ClockUptimeFast = 8;
internal const int ClockRealtimePrecise = 9;
internal const int ClockRealtimeFast = 10;
internal const int ClockMonotonicPrecise = 11;
internal const int ClockMonotonicFast = 12;
internal const int ClockSecond = 13;
internal const int ClockThreadCputimeId = 14;
internal const int ClockProcTime = 15;
private const int Efault = 14;
private const int Einval = 22;
private const ulong TlsErrnoOffset = 0x40;
private const ulong TlsStackChkGuardBaseOffset = 0x800;
private const ulong StackChkGuardFieldOffset = 0x10;
@@ -64,6 +80,98 @@ public static class KernelRuntimeCompatExports
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate ulong RdtscDelegate();
[SysAbiExport(
Nid = "QvsZxomvUHs",
ExportName = "sceKernelNanosleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
[SysAbiExport(
Nid = "yS8U2TGCe1A",
ExportName = "nanosleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: true);
private static int NanosleepCore(CpuContext ctx, bool posix)
{
var requestAddress = ctx[CpuRegister.Rdi];
var remainAddress = ctx[CpuRegister.Rsi];
if (requestAddress == 0)
{
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> timespecBuffer = stackalloc byte[16];
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
{
return NanosleepFailure(ctx, posix, Efault, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
var tvNsec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer[sizeof(long)..]);
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
{
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (tvSec == 0 && tvNsec == 0)
{
WriteRemainingTime(ctx, remainAddress, 0, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
// a single tick rather than collapsing to a zero-length (no-op) sleep.
var totalTicks = tvSec * TimeSpan.TicksPerSecond + Math.Max(tvNsec / 100L, 1L);
try
{
Thread.Sleep(TimeSpan.FromTicks(totalTicks));
}
catch (ArgumentOutOfRangeException)
{
Thread.Sleep(TimeSpan.FromMilliseconds(int.MaxValue));
}
WriteRemainingTime(ctx, remainAddress, 0, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int NanosleepFailure(CpuContext ctx, bool posix, int errnoValue, OrbisGen2Result sceResult)
{
if (posix)
{
TrySetErrno(ctx, errnoValue);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
}
else
{
ctx[CpuRegister.Rax] = unchecked((ulong)errnoValue);
}
return (int)sceResult;
}
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
{
if (remainAddress == 0)
{
return;
}
Span<byte> remainBuffer = stackalloc byte[16];
BinaryPrimitives.WriteInt64LittleEndian(remainBuffer, seconds);
BinaryPrimitives.WriteInt64LittleEndian(remainBuffer[sizeof(long)..], nanoseconds);
ctx.Memory.TryWrite(remainAddress, remainBuffer);
}
[SysAbiExport(
Nid = "1jfXLRVzisc",
ExportName = "sceKernelUsleep",
@@ -105,6 +213,13 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "QcteRwbsnV0",
ExportName = "usleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixUsleep(CpuContext ctx) => KernelUsleep(ctx);
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
{
if (!_traceUsleep)
@@ -186,15 +301,9 @@ public static class KernelRuntimeCompatExports
long seconds;
long nanoseconds;
if (clockId == 0)
if (!ResolveClockTime(clockId, out seconds, out nanoseconds))
{
var now = DateTimeOffset.UtcNow;
seconds = now.ToUnixTimeSeconds();
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
}
else
{
GetProcessMonotonicTime(out seconds, out nanoseconds);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> timespecBuffer = stackalloc byte[16];
@@ -226,8 +335,11 @@ public static class KernelRuntimeCompatExports
var now = DateTimeOffset.UtcNow;
var seconds = now.ToUnixTimeSeconds();
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds)))
Span<byte> timevalBuffer = stackalloc byte[16];
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -249,18 +361,28 @@ public static class KernelRuntimeCompatExports
var seconds = now.ToUnixTimeSeconds();
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
if (timeAddress != 0 &&
(!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds))))
if (timeAddress != 0)
{
return -1;
Span<byte> timevalBuffer = stackalloc byte[16];
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
{
TrySetErrno(ctx, Efault);
return -1;
}
}
if (timezoneAddress != 0 &&
(!ctx.TryWriteInt32(timezoneAddress, 0) ||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
if (timezoneAddress != 0)
{
return -1;
Span<byte> timezoneBuffer = stackalloc byte[8];
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer, 0);
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer[sizeof(int)..], 0);
if (!ctx.Memory.TryWrite(timezoneAddress, timezoneBuffer))
{
TrySetErrno(ctx, Efault);
return -1;
}
}
ctx[CpuRegister.Rax] = 0;
@@ -618,6 +740,50 @@ public static class KernelRuntimeCompatExports
return address != 0 && ctx.TryWriteInt32(address, value);
}
internal static bool ResolveClockTime(int clockId, out long seconds, out long nanoseconds)
{
switch (clockId)
{
case ClockRealtime:
case ClockRealtimePrecise:
case ClockRealtimeFast:
case ClockVirtual:
case ClockProf:
{
var now = DateTimeOffset.UtcNow;
seconds = now.ToUnixTimeSeconds();
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
return true;
}
// CLOCK_SECOND is FreeBSD's cached whole-second realtime clock (Quake's
// audio_output_thread polls it and treated the previous EINVAL as a fatal
// init failure, exiting and getting respawned in a loop).
case ClockSecond:
seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
nanoseconds = 0;
return true;
case ClockMonotonic:
case ClockMonotonicPrecise:
case ClockMonotonicFast:
case ClockUptime:
case ClockUptimePrecise:
case ClockUptimeFast:
// Per-thread/process CPU time approximated with the monotonic clock; games
// use these for profiling deltas where monotonicity matters, not absolutes.
case ClockThreadCputimeId:
case ClockProcTime:
GetProcessMonotonicTime(out seconds, out nanoseconds);
return true;
default:
seconds = 0;
nanoseconds = 0;
return false;
}
}
internal static void GetProcessMonotonicTime(out long seconds, out long nanoseconds)
{
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Diagnostics;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
@@ -12,6 +13,9 @@ public static class KernelSemaphoreCompatExports
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
private static int _nextSemaphoreHandle = 1;
[ThreadStatic]
private static int _semaPollBackoffCount;
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
@@ -19,9 +23,23 @@ public static class KernelSemaphoreCompatExports
public required int MaxCount { get; init; }
public int Count { get; set; }
public int WaitingThreads { get; set; }
public int CancelEpoch { get; set; }
public bool Deleted { get; set; }
public object Gate { get; } = new();
}
private sealed class SemaphoreWaiter
{
public required int NeedCount { get; init; }
public required int CancelEpochAtBlock { get; init; }
public bool Timed { get; init; }
// Written and read only under the owning semaphore's Gate.
public int? Result { get; set; }
}
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
[SysAbiExport(
Nid = "188x57JYp0g",
ExportName = "sceKernelCreateSema",
@@ -58,17 +76,27 @@ public static class KernelSemaphoreCompatExports
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
}
_semaphores[handle] = new KernelSemaphoreState
var state = new KernelSemaphoreState
{
Name = name,
InitialCount = initialCount,
MaxCount = maxCount,
Count = initialCount,
};
_semaphores[handle] = state;
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
// Handles are sequential and guest-predictable, so a hostile guest can
// race a WaitSema onto the handle between publication above and this
// rollback. Strand-proof that waiter exactly like DeleteSema does.
lock (state.Gate)
{
state.Deleted = true;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@@ -97,6 +125,7 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var pollTimedOut = false;
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
@@ -108,26 +137,82 @@ public static class KernelSemaphoreCompatExports
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt32(timeoutAddress, out _))
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
if (timeoutMicros == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
pollTimedOut = true;
}
else
{
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
var timedWaiter = new SemaphoreWaiter
{
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
Timed = true,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
blockDeadlineTimestamp: deadline))
{
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Host-owned threads cannot park in the guest scheduler; degrade to the
// immediate-timeout poll the callers already tolerate.
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
pollTimedOut = true;
}
}
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
if (!pollTimedOut)
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
var waiter = new SemaphoreWaiter
{
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
};
if (!GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
if ((++_semaPollBackoffCount & 255) == 0)
{
Thread.Sleep(0);
}
else
{
Thread.Yield();
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
[SysAbiExport(
@@ -193,8 +278,14 @@ public static class KernelSemaphoreCompatExports
semaphore.Count += signalCount;
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
// whose needCount exceeds the remaining count stays parked while a smaller
// waiter can proceed.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -226,10 +317,16 @@ public static class KernelSemaphoreCompatExports
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.WaitingThreads = 0;
semaphore.CancelEpoch++;
// WaitingThreads is NOT zeroed here: each canceled waiter decrements it
// exactly once in its wake handler. Zeroing here as well would double-count
// and silently absorb the increment of a waiter that parks between this
// gate release and the wake-all below.
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -245,10 +342,120 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
// Delete succeeds even with blocked waiters; they wake with the deleted
// result (the SCE kernel wakes waiters with the EACCES-class code).
lock (semaphore.Gate)
{
semaphore.Deleted = true;
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Wake handler: runs under the scheduler's guest-thread gate (lock order:
// scheduler gate -> semaphore gate). Returns true iff the waiter has a final
// result and should be re-readied; false leaves it parked.
private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
{
lock (semaphore.Gate)
{
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
}
}
private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
{
if (waiter.Result is not null)
{
return true;
}
if (semaphore.Deleted)
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
return true;
}
if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
return true;
}
if (semaphore.Count >= waiter.NeedCount)
{
semaphore.Count -= waiter.NeedCount;
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return true;
}
return false;
}
// Resume handler: runs on the woken guest thread outside the scheduler gate;
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
{
lock (semaphore.Gate)
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
// Nothing readies a parked semaphore waiter without the wake handler
// resolving it, so reaching here means the scheduler contract changed.
Console.Error.WriteLine(
$"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
}
return waiter.Result!.Value;
}
}
private static int CompleteBlockedTimedSemaWait(
CpuContext ctx,
KernelSemaphoreState semaphore,
SemaphoreWaiter waiter,
ulong timeoutAddress,
long deadlineTimestamp)
{
int result;
lock (semaphore.Gate)
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
result = waiter.Result!.Value;
}
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
}
return result;
}
private static void TraceSemaphore(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
@@ -0,0 +1,558 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
internal static class KernelSocketCompatExports
{
private sealed class EmulatedSocketState
{
public TcpClient? Client;
public NetworkStream? Stream;
public IPAddress BoundAddress = IPAddress.Any;
public int BoundPort;
public bool Bound;
public bool Connected;
}
private static readonly object Gate = new();
private static readonly Dictionary<int, EmulatedSocketState> Sockets = new();
internal static bool IsEmulatedSocketFd(int fd)
{
lock (Gate)
{
return Sockets.ContainsKey(fd);
}
}
internal static bool TryCloseSocketFd(int fd)
{
lock (Gate)
{
if (!Sockets.Remove(fd, out var state))
{
return false;
}
DisposeEmulatedSocket(state);
return true;
}
}
internal static bool TryReadSocketFd(
CpuContext ctx,
int fd,
ulong bufferAddress,
int requested,
out ulong bytesRead,
out OrbisGen2Result error)
{
bytesRead = 0;
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
if (!TryGetEmulatedSocketState(fd, out var state) ||
state is null ||
!state.Connected ||
state.Stream is null)
{
return false;
}
var socketBuffer = GC.AllocateUninitializedArray<byte>(requested);
int socketRead;
try
{
socketRead = state.Stream.Read(socketBuffer, 0, requested);
}
catch (IOException)
{
return false;
}
if (socketRead > 0 && !ctx.Memory.TryWrite(bufferAddress, socketBuffer.AsSpan(0, socketRead)))
{
error = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return true;
}
bytesRead = unchecked((ulong)socketRead);
error = OrbisGen2Result.ORBIS_GEN2_OK;
return true;
}
internal static bool TryWriteSocketFd(
CpuContext ctx,
int fd,
byte[] payload,
out OrbisGen2Result error)
{
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
if (!TryGetEmulatedSocketState(fd, out var state) ||
state is null ||
!state.Connected ||
state.Stream is null)
{
return false;
}
try
{
state.Stream.Write(payload, 0, payload.Length);
state.Stream.Flush();
}
catch (IOException)
{
return false;
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
Console.Out.Write(Encoding.UTF8.GetString(payload));
Console.Out.Flush();
}
error = OrbisGen2Result.ORBIS_GEN2_OK;
return true;
}
[SysAbiExport(
Nid = "TU-d9PfIHPM",
ExportName = "socket",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Socket(CpuContext ctx)
{
var fd = KernelMemoryCompatExports.AllocateGuestFileDescriptor();
lock (Gate)
{
Sockets[fd] = new EmulatedSocketState();
}
ctx[CpuRegister.Rax] = unchecked((ulong)fd);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "XVL8So3QJUk",
ExportName = "connect",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Connect(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
if (!TryGetEmulatedSocketState(fd, out _))
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
{
LogNet($"connect sockaddr parse failed: fd={fd} addr=0x{sockaddrAddress:X} len={addrlen}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var redirectApplied = TryApplyNetRedirect(ref ipAddress);
if (redirectApplied)
{
LogNet($"connect redirect: fd={fd} ip={ipAddress} port={port}");
}
if (!IsGuestTcpOutboundAllowed(ipAddress, redirectApplied))
{
LogNet($"connect denied by outbound policy: fd={fd} ip={ipAddress} port={port}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryEstablishHostTcpConnection(ipAddress, port, out var client, out var stream))
{
LogNet($"connect failed: fd={fd} ip={ipAddress} port={port}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
LogNet($"connect ok: fd={fd} ip={ipAddress} port={port}");
lock (Gate)
{
if (!Sockets.TryGetValue(fd, out var state) || state is null)
{
try { stream.Dispose(); } catch (IOException) { }
try { client.Dispose(); } catch (IOException) { }
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
DisposeEmulatedSocket(state);
state.Client = client;
state.Stream = stream;
state.Connected = true;
state.BoundAddress = ipAddress;
state.BoundPort = port;
state.Bound = true;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "KuOmgKoqCdY",
ExportName = "bind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Bind(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
if (!TryGetEmulatedSocketState(fd, out var state) || state is null)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
state.BoundAddress = ipAddress;
state.BoundPort = port;
state.Bound = true;
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "RenI1lL1WFk",
ExportName = "getsockname",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Getsockname(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlenAddress = ctx[CpuRegister.Rdx];
if (!TryGetEmulatedSocketState(fd, out var state) || state is null || !state.Bound)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> addrlenBuffer = stackalloc byte[4];
if (!ctx.Memory.TryRead(addrlenAddress, addrlenBuffer))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var addrlen = BinaryPrimitives.ReadInt32LittleEndian(addrlenBuffer);
if (addrlen < 8)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> sockaddr = stackalloc byte[16];
sockaddr[0] = 16;
sockaddr[1] = 2;
BinaryPrimitives.WriteUInt16BigEndian(sockaddr.Slice(2, 2), (ushort)state.BoundPort);
var addressBytes = state.BoundAddress.GetAddressBytes();
if (addressBytes.Length == 4)
{
addressBytes.CopyTo(sockaddr.Slice(4, 4));
}
var writeLength = Math.Min(addrlen, 16);
if (!ctx.Memory.TryWrite(sockaddrAddress, sockaddr.Slice(0, writeLength)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
BinaryPrimitives.WriteInt32LittleEndian(addrlenBuffer, writeLength);
if (!ctx.Memory.TryWrite(addrlenAddress, addrlenBuffer))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "9oiX1kyeedA",
ExportName = "bzero",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Bzero(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
var length = unchecked((int)ctx[CpuRegister.Rsi]);
if (length > 0 && address != 0)
{
var zeros = new byte[length];
if (!ctx.Memory.TryWrite(address, zeros))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4n51s0zEf0c",
ExportName = "inet_pton",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int InetPton(CpuContext ctx)
{
var af = unchecked((int)ctx[CpuRegister.Rdi]);
var srcAddress = ctx[CpuRegister.Rsi];
var dstAddress = ctx[CpuRegister.Rdx];
if (af != 2 || srcAddress == 0 || dstAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadCString(srcAddress, ctx, out var text) ||
!TryParseIpv4Address(text, out var octets))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> packed = stackalloc byte[4];
packed[0] = octets[0];
packed[1] = octets[1];
packed[2] = octets[2];
packed[3] = octets[3];
if (!ctx.Memory.TryWrite(dstAddress, packed))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 1;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jogUIsOV3-U",
ExportName = "htons",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Htons(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
var swapped = (ushort)(((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF));
ctx[CpuRegister.Rax] = swapped;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryGetEmulatedSocketState(int fd, out EmulatedSocketState? state)
{
lock (Gate)
{
return Sockets.TryGetValue(fd, out state);
}
}
private static bool TryParseGuestSockaddrIn(
ulong address,
int addrlen,
CpuContext ctx,
out IPAddress ipAddress,
out int port)
{
ipAddress = IPAddress.None;
port = 0;
if (address == 0 || addrlen < 8)
{
return false;
}
Span<byte> buffer = stackalloc byte[16];
var readLength = Math.Min(addrlen, buffer.Length);
if (!ctx.Memory.TryRead(address, buffer.Slice(0, readLength)))
{
return false;
}
if (buffer[1] != 2)
{
return false;
}
port = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2, 2));
ipAddress = new IPAddress(buffer.Slice(4, 4).ToArray());
return true;
}
private static void DisposeEmulatedSocket(EmulatedSocketState state)
{
try { state.Stream?.Dispose(); } catch (IOException) { }
try { state.Client?.Dispose(); } catch (IOException) { }
state.Stream = null;
state.Client = null;
state.Connected = false;
}
private static void RemoveEmulatedSocketFd(int fd)
{
lock (Gate)
{
if (Sockets.Remove(fd, out var socketState))
{
DisposeEmulatedSocket(socketState);
}
}
}
private static void LogNet(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][DEBUG] {message}");
}
}
private static bool TryApplyNetRedirect(ref IPAddress ipAddress)
{
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
if (string.IsNullOrWhiteSpace(redirect))
{
return false;
}
if (!IPAddress.TryParse(redirect.Trim(), out var redirectAddress))
{
return false;
}
ipAddress = redirectAddress;
return true;
}
private static bool IsNetRedirectConfigured()
{
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
return !string.IsNullOrWhiteSpace(redirect);
}
private static bool IsGuestTcpOutboundAllowed(IPAddress ipAddress, bool redirectApplied)
{
return redirectApplied || IsNetRedirectConfigured() || IPAddress.IsLoopback(ipAddress);
}
private static bool TryEstablishHostTcpConnection(
IPAddress ipAddress,
int port,
out TcpClient client,
out NetworkStream stream)
{
client = null!;
stream = null!;
if (!TryConnectTcpClient(ipAddress, port, out client))
{
return false;
}
stream = client.GetStream();
return true;
}
private static bool TryConnectTcpClient(IPAddress ipAddress, int port, out TcpClient client)
{
client = new TcpClient();
try
{
var connectTask = client.ConnectAsync(ipAddress, port);
if (!connectTask.Wait(TimeSpan.FromMilliseconds(500)))
{
client.Dispose();
client = null!;
return false;
}
return true;
}
catch (SocketException)
{
client.Dispose();
client = null!;
return false;
}
catch (IOException)
{
client.Dispose();
client = null!;
return false;
}
}
private static bool TryReadCString(ulong address, CpuContext ctx, out string text)
{
const int maxLength = 64;
var buffer = new byte[maxLength];
var length = 0;
for (; length < maxLength; length++)
{
if (!ctx.Memory.TryRead(address + (ulong)length, buffer.AsSpan(length, 1)))
{
text = string.Empty;
return false;
}
if (buffer[length] == 0)
{
break;
}
}
text = Encoding.ASCII.GetString(buffer, 0, length);
return true;
}
private static bool TryParseIpv4Address(string text, out byte[] octets)
{
octets = Array.Empty<byte>();
var parts = text.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length != 4)
{
return false;
}
var parsed = new byte[4];
for (var i = 0; i < 4; i++)
{
if (!byte.TryParse(parts[i], out parsed[i]))
{
return false;
}
}
octets = parsed;
return true;
}
}
+820
View File
@@ -0,0 +1,820 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
namespace SharpEmu.Libs.LibcStdio;
public static class LibcStdioExports
{
private const int MaxPathLength = 4096;
private const int MaxModeLength = 16;
private const int ReadChunkSize = 1024 * 1024;
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
private static long _nextHandle = 0x1000 - 8;
private static readonly bool _traceStdio =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
private const int CtypeTableLowerBound = -128;
private const int CtypeTableUpperBound = 255;
private const int CtypeTableEntryCount = CtypeTableUpperBound - CtypeTableLowerBound + 1; // 384 entries * 2 bytes = 768 bytes
// Mirrors the Dinkumware ctype bitmask layout (the CRT Sony's libc is based on;
// _Getpctype/_Getptolower/_Getptoupper are Dinkumware accessor names). This is NOT the
// MSVC/UCRT layout: e.g. Dinkumware puts digit at 0x20 where UCRT puts control, so
// serving a UCRT-shaped table made the game's bundled printf engine misparse every
// %-directive (fatal-error messages rendered empty) and made its mcpp preprocessor
// treat 'a'-'f' as control characters (UCRT _HEX=0x80 reads as Dinkumware _BB) and
// drop them from identifiers ("texture" -> "txtur"). Titles that bundle their own
// libc bypass this table entirely (see IsSafeLleLibcExport in DirectExecutionBackend);
// this fallback only serves titles that import _Getpctype without shipping one.
private const ushort CtypeXDigit = 0x001; // _XD '0'-'9', 'A'-'F', 'a'-'f'
private const ushort CtypeUpper = 0x002; // _UP 'A'-'Z'
private const ushort CtypeSpace = 0x004; // _SP ' ' (isspace = _CN|_SP|_XS)
private const ushort CtypePunct = 0x008; // _PU punctuation
private const ushort CtypeLower = 0x010; // _LO 'a'-'z'
private const ushort CtypeDigit = 0x020; // _DI '0'-'9'
private const ushort CtypeControlSpace = 0x040; // _CN '\t','\n','\v','\f','\r'
private const ushort CtypeControl = 0x080; // _BB other control characters
private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t'
private static readonly object _ctypeTableGate = new();
private static nint _ctypeTableBase;
[SysAbiExport(
Nid = "xeYO4u7uyJ0",
ExportName = "fopen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fopen(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var modeAddress = ctx[CpuRegister.Rsi];
if (pathAddress == 0 || modeAddress == 0 ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
{
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> PERMISSION_DENIED (read-only path)");
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
try
{
if (fileAccess != FileAccess.Read)
{
var parentDirectory = Path.GetDirectoryName(hostPath);
if (!string.IsNullOrWhiteSpace(parentDirectory))
{
Directory.CreateDirectory(parentDirectory);
}
}
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
{
stream.Seek(0, SeekOrigin.End);
}
var handle = unchecked((ulong)Interlocked.Add(ref _nextHandle, 8));
_fileHandles[handle] = stream;
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X} length={stream.Length}");
}
ctx[CpuRegister.Rax] = handle;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
}
ctx[CpuRegister.Rax] = 0;
return ex is UnauthorizedAccessException
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
}
[SysAbiExport(
Nid = "rQFVBXp-Cxg",
ExportName = "fseek",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fseek(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var offset = unchecked((long)ctx[CpuRegister.Rsi]);
var whence = unchecked((int)ctx[CpuRegister.Rdx]);
if (!_fileHandles.TryGetValue(handle, out var stream) || !TryGetSeekOrigin(whence, out var origin))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
stream.Seek(offset, origin);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "Qazy8LmXTvw",
ExportName = "ftell",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Ftell(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
ctx[CpuRegister.Rax] = unchecked((ulong)stream.Position);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "uodLYyUip20",
ExportName = "fclose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fclose(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_fileHandles.TryRemove(handle, out var stream))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
stream.Dispose();
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "lbB+UlZqVG0",
ExportName = "fread",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fread(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var elementSize = ctx[CpuRegister.Rsi];
var elementCount = ctx[CpuRegister.Rdx];
var handle = ctx[CpuRegister.Rcx];
if (elementSize == 0 || elementCount == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (destination == 0 || !_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var totalRequested = elementSize * elementCount;
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
ulong totalRead = 0;
try
{
while (totalRead < totalRequested)
{
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalRead);
var read = stream.Read(buffer, 0, request);
if (read <= 0)
{
break;
}
if (!ctx.Memory.TryWrite(destination + totalRead, buffer.AsSpan(0, read)))
{
ctx[CpuRegister.Rax] = totalRead / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
totalRead += (ulong)read;
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = totalRead / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fread: handle=0x{handle:X} requested={totalRequested} read={totalRead} pos={stream.Position}");
}
ctx[CpuRegister.Rax] = totalRead / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "KdP-nULpuGw",
ExportName = "fgets",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fgets(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var maxCount = unchecked((int)ctx[CpuRegister.Rsi]);
var handle = ctx[CpuRegister.Rdx];
if (destination == 0 || maxCount <= 0 || !_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var buffer = ArrayPool<byte>.Shared.Rent(maxCount - 1);
var count = 0;
try
{
while (count < maxCount - 1)
{
var b = stream.ReadByte();
if (b < 0)
{
break;
}
buffer[count++] = (byte)b;
if (b == '\n')
{
break;
}
}
if (count == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
Span<byte> withNul = stackalloc byte[count + 1];
buffer.AsSpan(0, count).CopyTo(withNul);
withNul[count] = 0;
if (!ctx.Memory.TryWrite(destination, withNul))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8Q60JLJ6Rv4",
ExportName = "getc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Getc(CpuContext ctx) => FgetcCore(ctx);
[SysAbiExport(
Nid = "AEuF3F2f8TA",
ExportName = "fgetc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fgetc(CpuContext ctx) => FgetcCore(ctx);
private static int FgetcCore(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
var value = stream.ReadByte();
ctx[CpuRegister.Rax] = value < 0 ? unchecked((ulong)(-1L)) : (ulong)value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "LxcEU+ICu8U",
ExportName = "feof",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Feof(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var atEnd = _fileHandles.TryGetValue(handle, out var stream) &&
stream.CanRead && stream.Position >= stream.Length;
ctx[CpuRegister.Rax] = atEnd ? 1UL : 0UL;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "AHxyhN96dy4",
ExportName = "ferror",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Ferror(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "3QIPIh-GDjw",
ExportName = "rewind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Rewind(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.Seek(0, SeekOrigin.Begin);
}
catch (IOException)
{
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "aZK8lNei-Qw",
ExportName = "fputc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fputc(CpuContext ctx)
{
var character = unchecked((byte)ctx[CpuRegister.Rdi]);
var handle = ctx[CpuRegister.Rsi];
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.WriteByte(character);
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
else
{
// Unknown streams are the bundled libc's real stdout/stderr FILE objects;
// mirror the fputs HLE and forward them to the host console.
Console.Out.Write((char)character);
}
ctx[CpuRegister.Rax] = character;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MpxhMh8QFro",
ExportName = "fwrite",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fwrite(CpuContext ctx)
{
var source = ctx[CpuRegister.Rdi];
var elementSize = ctx[CpuRegister.Rsi];
var elementCount = ctx[CpuRegister.Rdx];
var handle = ctx[CpuRegister.Rcx];
if (elementSize == 0 || elementCount == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var knownHandle = _fileHandles.TryGetValue(handle, out var stream);
if (source == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var totalRequested = elementSize * elementCount;
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
ulong totalWritten = 0;
try
{
while (totalWritten < totalRequested)
{
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalWritten);
if (!ctx.Memory.TryRead(source + totalWritten, buffer.AsSpan(0, request)))
{
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (knownHandle)
{
stream!.Write(buffer, 0, request);
}
else
{
Console.Out.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, request));
}
totalWritten += (ulong)request;
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MUjC4lbHrK4",
ExportName = "fflush",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fflush(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
try
{
if (handle == 0)
{
foreach (var stream in _fileHandles.Values)
{
stream.Flush();
}
}
else if (_fileHandles.TryGetValue(handle, out var stream))
{
stream.Flush();
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fffwELXNVFA",
ExportName = "fprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fprintf(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
return WriteRenderedText(ctx, handle, rendered);
}
[SysAbiExport(
Nid = "pDBDcY6uLSA",
ExportName = "vfprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Vfprintf(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
var vaListAddress = ctx[CpuRegister.Rdx];
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
KernelMemoryCompatExports.TryFormatStringFromVaList(ctx, format, vaListAddress, out var rendered);
return WriteRenderedText(ctx, handle, rendered);
}
private static int WriteRenderedText(CpuContext ctx, ulong handle, string rendered)
{
var payload = System.Text.Encoding.UTF8.GetBytes(rendered);
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.Write(payload, 0, payload.Length);
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
else
{
Console.Out.Write(rendered);
}
ctx[CpuRegister.Rax] = (ulong)payload.Length;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "gkWgn0p1AfU",
ExportName = "freopen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Freopen(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var modeAddress = ctx[CpuRegister.Rsi];
var handle = ctx[CpuRegister.Rdx];
if (pathAddress == 0 || modeAddress == 0 || handle == 0 ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (_fileHandles.TryRemove(handle, out var previousStream))
{
previousStream.Dispose();
}
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
try
{
if (fileAccess != FileAccess.Read)
{
var parentDirectory = Path.GetDirectoryName(hostPath);
if (!string.IsNullOrWhiteSpace(parentDirectory))
{
Directory.CreateDirectory(parentDirectory);
}
}
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
// freopen keeps the caller's FILE* identity, so rebind the same handle value.
_fileHandles[handle] = stream;
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X}");
}
ctx[CpuRegister.Rax] = handle;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
}
ctx[CpuRegister.Rax] = 0;
return ex is UnauthorizedAccessException
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
}
[SysAbiExport(
Nid = "sUP1hBaouOw",
ExportName = "_Getpctype",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int GetPctype(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeTable());
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static unsafe nint EnsureCtypeTable()
{
lock (_ctypeTableGate)
{
if (_ctypeTableBase != 0)
{
return _ctypeTableBase;
}
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
for (var i = 0; i < CtypeTableEntryCount; i++)
{
var c = i + CtypeTableLowerBound;
entries[i] = c is >= 0 and <= 0x7F ? ComputeCtypeFlags(c) : (ushort)0;
}
// Table is indexed as base[c] for c in [-128, 255], so the pointer handed to the
// guest must point at the c == 0 entry, not the start of the allocation.
_ctypeTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
return _ctypeTableBase;
}
}
private static ushort ComputeCtypeFlags(int c)
{
var isUpper = c is >= 'A' and <= 'Z';
var isLower = c is >= 'a' and <= 'z';
var isDigit = c is >= '0' and <= '9';
var isHex = isDigit || (c is >= 'A' and <= 'F') || (c is >= 'a' and <= 'f');
var isAlnum = isUpper || isLower || isDigit;
// In the Dinkumware table '\t'..'\r' carry _CN (isspace and iscntrl both match via
// _CN) while plain ' ' carries _SP; keeping them distinct is what keeps isprint(' ')
// true and isprint('\t') false.
var isControlSpace = c is >= 0x09 and <= 0x0D;
var isControl = (c is >= 0x00 and <= 0x08) || (c is >= 0x0E and <= 0x1F) || c == 0x7F;
var isPrintable = c is >= 0x20 and <= 0x7E;
var isPunct = isPrintable && !isAlnum && c != ' ';
ushort flags = 0;
if (isUpper) flags |= CtypeUpper;
if (isLower) flags |= CtypeLower;
if (isDigit) flags |= CtypeDigit;
if (isHex) flags |= CtypeXDigit;
if (c == ' ') flags |= CtypeSpace | CtypeBlank;
if (c == '\t') flags |= CtypeBlank;
if (isControlSpace) flags |= CtypeControlSpace;
if (isPunct) flags |= CtypePunct;
if (isControl) flags |= CtypeControl;
return flags;
}
private static bool TryParseFopenMode(string mode, out FileMode fileMode, out FileAccess fileAccess)
{
fileMode = FileMode.Open;
fileAccess = FileAccess.Read;
if (string.IsNullOrEmpty(mode))
{
return false;
}
var plus = mode.Contains('+');
switch (mode[0])
{
case 'r':
fileMode = FileMode.Open;
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Read;
return true;
case 'w':
fileMode = FileMode.Create;
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Write;
return true;
case 'a':
if (plus)
{
fileMode = FileMode.OpenOrCreate;
fileAccess = FileAccess.ReadWrite;
}
else
{
fileMode = FileMode.Append;
fileAccess = FileAccess.Write;
}
return true;
default:
return false;
}
}
private static bool TryGetSeekOrigin(int whence, out SeekOrigin origin)
{
switch (whence)
{
case 0:
origin = SeekOrigin.Begin;
return true;
case 1:
origin = SeekOrigin.Current;
return true;
case 2:
origin = SeekOrigin.End;
return true;
default:
origin = default;
return false;
}
}
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Mouse;
public static class MouseExports
{
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
// as an sceNgs2VoiceGetState alias.
[SysAbiExport(
Nid = "x8qnXqh-tiM",
ExportName = "sceMouseRead",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMouse")]
public static int MouseRead(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "RaqxZIf6DvE",
ExportName = "sceMouseOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMouse")]
public static int MouseOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
+48
View File
@@ -95,6 +95,54 @@ public static class NetExports
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "9T2pDF2Ryqg",
ExportName = "sceNetHtonl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetHtonl(CpuContext ctx)
{
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "iWQWrwiSt8A",
ExportName = "sceNetHtons",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetHtons(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "pQGpHYopAIY",
ExportName = "sceNetNtohl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetNtohl(CpuContext ctx)
{
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "Rbvt+5Y2iEw",
ExportName = "sceNetNtohs",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetNtohs(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "C4UgDHHPvdw",
ExportName = "sceNetResolverCreate",
+39 -6
View File
@@ -13,7 +13,7 @@ public static class Ngs2Exports
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
private const ulong HandleStorageSize = 0x20;
private const ulong HandleStorageSize = 0x1000;
private const int RenderBufferInfoSize = 0x18;
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
@@ -197,12 +197,18 @@ public static class Ngs2Exports
lock (StateGate)
{
return ctx.SetReturn(
Voices.ContainsKey(ctx[CpuRegister.Rdi])
? 0
Voices.ContainsKey(ctx[CpuRegister.Rdi])
? 0
: OrbisNgs2ErrorInvalidVoiceHandle);
}
}
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
// their real libraries; the NIDs below are verified against the hash.
[SysAbiExport(
Nid = "AbYvTOZ8Pts",
ExportName = "sceNgs2VoiceRunCommands",
@@ -210,6 +216,27 @@ public static class Ngs2Exports
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
[SysAbiExport(
Nid = "-TOuuAQ-buE",
ExportName = "sceNgs2VoiceGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "rEh728kXk3w",
ExportName = "sceNgs2VoiceGetStateFlags",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "xa8oL9dmXkM",
ExportName = "sceNgs2PanInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "i0VnXM-C9fc",
ExportName = "sceNgs2SystemRender",
@@ -263,7 +290,6 @@ public static class Ngs2Exports
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
{
handle = 0;
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
{
return false;
@@ -271,11 +297,18 @@ public static class Ngs2Exports
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
data.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle);
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
return ctx.Memory.TryWrite(handle, data);
if (!ctx.Memory.TryWrite(handle, data))
{
return false;
}
// Offset 0 doubles as a vtable slot for guest code that treats this handle as a C++
// object; stamp it with the shared dummy vtable instead of a self-referential value.
KernelMemoryCompatExports.TryWriteDummyVtable(ctx, handle);
return true;
}
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
+61
View File
@@ -22,6 +22,17 @@ public static class NpManagerExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "S7QTn72PrDw",
ExportName = "sceNpDeleteRequest",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpDeleteRequest(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JELHf4xPufo",
ExportName = "sceNpCheckCallbackForLib",
@@ -33,6 +44,41 @@ public static class NpManagerExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Offline profile: the online id payload is left untouched and the call
// reports success, matching the other offline NpManager stubs here.
[SysAbiExport(
Nid = "XDncXQIJUSk",
ExportName = "sceNpGetOnlineId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetOnlineId(CpuContext ctx)
{
// Gen5 ABI: user ID, then output structure.
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
}
[SysAbiExport(
Nid = "rbknaUjpqWo",
ExportName = "sceNpGetOnlineIdA",
Target = Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetOnlineIdA(CpuContext ctx)
{
// The A variant takes a user ID before OnlineId.
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
}
[SysAbiExport(
Nid = "e-ZuhGEoeC4",
ExportName = "sceNpGetNpReachabilityState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetNpReachabilityState(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "VfRSmPmj8Q8",
ExportName = "sceNpRegisterStateCallback",
@@ -134,4 +180,19 @@ public static class NpManagerExports
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}");
}
private static int WriteOfflineOnlineId(CpuContext ctx, ulong address)
{
if (address == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// SceNpOnlineId is a 16-byte handle plus four trailing bytes.
Span<byte> onlineId = stackalloc byte[20];
"Player"u8.CopyTo(onlineId);
return ctx.Memory.TryWrite(address, onlineId)
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
+108
View File
@@ -0,0 +1,108 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Np;
public static class NpTrophy2Exports
{
private static int _nextContext = 1;
private static int _nextHandle = 1;
[SysAbiExport(
Nid = "Bagshr7OQ6Q",
ExportName = "sceNpTrophy2CreateContext",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2CreateContext(CpuContext ctx)
{
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextContext);
}
[SysAbiExport(
Nid = "Gz1rmUZpROM",
ExportName = "sceNpTrophy2CreateHandle",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2CreateHandle(CpuContext ctx)
{
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextHandle);
}
[SysAbiExport(
Nid = "sysY2FHYff4",
ExportName = "sceNpTrophy2DestroyContext",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2DestroyContext(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "d8P11CI40KE",
ExportName = "sceNpTrophy2DestroyHandle",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2DestroyHandle(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "fYapWA9xVmA",
ExportName = "sceNpTrophy2AbortHandle",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2AbortHandle(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "bIDov3wBu5Q",
ExportName = "sceNpTrophy2RegisterContext",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2RegisterContext(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "sUXGfNMalIo",
ExportName = "sceNpTrophy2RegisterUnlockCallback",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2RegisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "wVqxM58sIKs",
ExportName = "sceNpTrophy2UnregisterUnlockCallback",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2UnregisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
[SysAbiExport(
Nid = "EHQEDVXZ0TI",
ExportName = "sceNpTrophy2ShowTrophyList",
Target = Generation.Gen5,
LibraryName = "libSceNpTrophy2")]
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
{
if (outAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> idBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(idBytes, nextId);
if (!ctx.Memory.TryWrite(outAddress, idBytes))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
nextId++;
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int ReturnOk(CpuContext ctx) => SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
}
@@ -76,4 +76,14 @@ public static class NpUniversalDataSystemExports
{
return ctx.SetReturn(0, typeof(long));
}
[SysAbiExport(
Nid = "AUIHb7jUX3I",
ExportName = "sceNpUniversalDataSystemDestroyHandle",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemDestroyHandle(CpuContext ctx)
{
return ctx.SetReturn(0, typeof(long));
}
}
+12
View File
@@ -31,6 +31,18 @@ public static class NpWebApi2Exports
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "WV1GwM32NgY",
ExportName = "sceNpWebApi2InitializeForToolkit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpWebApi2")]
public static int NpWebApi2InitializeAlt(CpuContext ctx)
{
Interlocked.Exchange(ref _initialized, 1);
TraceNpWebApi2("init-alt", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "bEvXpcEk200",
ExportName = "sceNpWebApi2Terminate",
+67 -20
View File
@@ -14,11 +14,18 @@ public static class PadExports
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
private const int PrimaryUserId = 1;
private const int PrimaryUserId = 1000;
private const int StandardPortType = 0;
private const int PrimaryPadHandle = 1;
private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78;
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
[ThreadStatic]
private static long _lastInputSampleTicks;
[ThreadStatic]
private static PadState _cachedInputState;
private static bool _initialized;
@@ -178,6 +185,16 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "2JgFB2n9oUM",
ExportName = "scePadSetTriggerEffect",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetTriggerEffect(CpuContext ctx)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "yFVnOdGxvZY",
ExportName = "scePadSetVibration",
@@ -260,6 +277,44 @@ public static class PadExports
{
Span<byte> data = stackalloc byte[PadDataSize];
data.Clear();
var input = ReadHostInputState();
var buttons = input.Buttons;
var leftX = input.LeftX;
var leftY = input.LeftY;
var rightX = input.RightX;
var rightY = input.RightY;
var l2 = input.L2;
var r2 = input.R2;
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
data[0x04] = leftX;
data[0x05] = leftY;
data[0x06] = rightX;
data[0x07] = rightY;
data[0x08] = l2;
data[0x09] = r2;
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
data[0x4C] = 1;
var timestampTicks = Stopwatch.GetTimestamp();
var timestampMicroseconds =
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
BinaryPrimitives.WriteUInt64LittleEndian(
data[0x50..],
timestampMicroseconds);
data[0x68] = 1;
return ctx.Memory.TryWrite(dataAddress, data);
}
private static PadState ReadHostInputState()
{
var now = Stopwatch.GetTimestamp();
if (_lastInputSampleTicks != 0 && now - _lastInputSampleTicks < InputSampleIntervalTicks)
{
return _cachedInputState;
}
var acceptsKeyboardInput = IsEmulatorWindowFocused();
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
@@ -293,25 +348,17 @@ public static class PadExports
r2 = Math.Max(r2, xpad.R2);
}
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
data[0x04] = leftX;
data[0x05] = leftY;
data[0x06] = rightX;
data[0x07] = rightY;
data[0x08] = l2;
data[0x09] = r2;
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
data[0x4C] = 1;
var timestampTicks = Stopwatch.GetTimestamp();
var timestampMicroseconds =
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
BinaryPrimitives.WriteUInt64LittleEndian(
data[0x50..],
timestampMicroseconds);
data[0x68] = 1;
return ctx.Memory.TryWrite(dataAddress, data);
_cachedInputState = new PadState(
Connected: true,
Buttons: buttons,
LeftX: leftX,
LeftY: leftY,
RightX: rightX,
RightY: rightY,
L2: l2,
R2: r2);
_lastInputSampleTicks = now;
return _cachedInputState;
}
[DllImport("user32.dll")]
+3 -1
View File
@@ -682,7 +682,9 @@ public static class PlayGoExports
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
if (!hasMetadata)
{
return PlayGoMetadata.Empty;
// Full installs may omit PlayGo sidecar metadata.
TracePlayGo("metadata_missing; using fully-installed default chunk");
return new PlayGoMetadata(true, [(ushort)0]);
}
var chunkIds = LoadChunkIds(chunkDefsXml);
+820 -67
View File
@@ -9,6 +9,189 @@ namespace SharpEmu.Libs.Rtc;
public static class RtcExports
{
private const long DateTimeTicksPerMicrosecond = 10;
private const ulong MicrosecondsPerSecond = 1_000_000UL;
private const ulong MicrosecondsPerMinute = 60_000_000UL;
private const ulong MicrosecondsPerHour = 3_600_000_000UL;
private const ulong MicrosecondsPerDay = 86_400_000_000UL;
private const ulong MicrosecondsPerWeek = 604_800_000_000UL;
private const ulong UnixEpochTicks = 62_135_596_800_000_000UL;
private const ulong Win32FileTimeEpochTicks = 50_491_123_200_000_000UL;
[SysAbiExport(
Nid = "lPEBYdVX0XQ",
ExportName = "sceRtcCheckValid",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcCheckValid(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
if (timeAddress == 0)
{
return unchecked((int)0x80B50002);
}
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return ValidateRtcDateTime(time);
}
[SysAbiExport(
Nid = "fNaZ4DbzHAE",
ExportName = "sceRtcCompareTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcCompareTick(CpuContext ctx)
{
var tick1Address = ctx[CpuRegister.Rdi];
var tick2Address = ctx[CpuRegister.Rsi];
if (tick1Address == 0 || tick2Address == 0)
{
return unchecked((int)0x80B50002);
}
if (!ctx.TryReadUInt64(tick1Address, out var tick1) || !ctx.TryReadUInt64(tick2Address, out var tick2))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return tick1 < tick2 ? -1 : tick1 > tick2 ? 1 : 0;
}
[SysAbiExport(
Nid = "8Yr143yEnRo",
ExportName = "sceRtcConvertLocalTimeToUtc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcConvertLocalTimeToUtc(CpuContext ctx)
{
var tickLocalAddress = ctx[CpuRegister.Rdi];
var tickUtcAddress = ctx[CpuRegister.Rsi];
if (tickLocalAddress == 0 || tickUtcAddress == 0)
{
return unchecked((int)0x80B50002);
}
if (!ctx.TryReadUInt64(tickLocalAddress, out var tickLocal))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryConvertTickToDateTime(tickLocal, out var localDateTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
DateTime utcDateTime;
try
{
utcDateTime = TimeZoneInfo.ConvertTimeToUtc(localDateTime, TimeZoneInfo.Local);
}
catch (ArgumentException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryWriteTickFromDateTime(ctx, tickUtcAddress, utcDateTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "M1TvFst-jrM",
ExportName = "sceRtcConvertUtcToLocalTime",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcConvertUtcToLocalTime(CpuContext ctx)
{
var tickUtcAddress = ctx[CpuRegister.Rdi];
var tickLocalAddress = ctx[CpuRegister.Rsi];
if (tickUtcAddress == 0 || tickLocalAddress == 0)
{
return unchecked((int)0x80B50002);
}
if (!ctx.TryReadUInt64(tickUtcAddress, out var tickUtc))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryConvertTickToDateTime(tickUtc, out var utcDateTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
DateTime localDateTime;
try
{
localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, TimeZoneInfo.Local);
}
catch (ArgumentException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryWriteTickFromDateTime(ctx, tickLocalAddress, localDateTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8SljQx6pDP8",
ExportName = "sceRtcEnd",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcEnd(CpuContext ctx)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "LN3Zcb72Q0c",
ExportName = "sceRtcGetCurrentAdNetworkTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetCurrentAdNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
[SysAbiExport(
Nid = "8lfvnRMqwEM",
ExportName = "sceRtcGetCurrentClock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetCurrentClock(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
if (timeAddress == 0)
{
return unchecked((int)0x80B50002);
}
var timeZoneMinutes = unchecked((int)ctx[CpuRegister.Rsi]);
DateTimeOffset currentTime;
try
{
currentTime = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromMinutes(timeZoneMinutes));
}
catch (ArgumentOutOfRangeException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryWriteRtcDateTime(ctx, timeAddress, ToRtcDateTime(currentTime.DateTime)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ZPD1YOKI+Kw",
@@ -20,30 +203,38 @@ public static class RtcExports
var timeAddress = ctx[CpuRegister.Rdi];
if (timeAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return unchecked((int)0x80B50002);
}
var now = DateTimeOffset.Now;
Span<byte> rtcDateTime = stackalloc byte[16];
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)now.Year));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)now.Month));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)now.Day));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)now.Hour));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)now.Minute));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)now.Second));
BinaryPrimitives.WriteUInt32LittleEndian(
rtcDateTime[12..16],
checked((uint)((now.Ticks % TimeSpan.TicksPerSecond) / 10)));
if (!ctx.Memory.TryWrite(timeAddress, rtcDateTime))
if (!TryWriteRtcDateTime(ctx, timeAddress, ToRtcDateTime(DateTimeOffset.Now.DateTime)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ot1DE3gif84",
ExportName = "sceRtcGetCurrentDebugNetworkTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetCurrentDebugNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
[SysAbiExport(
Nid = "zO9UL3qIINQ",
ExportName = "sceRtcGetCurrentNetworkTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetCurrentNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
[SysAbiExport(
Nid = "HWxHOdbM-Pg",
ExportName = "sceRtcGetCurrentRawNetworkTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetCurrentRawNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
[SysAbiExport(
Nid = "18B2NS1y9UU",
ExportName = "sceRtcGetCurrentTick",
@@ -54,7 +245,7 @@ public static class RtcExports
var tickAddress = ctx[CpuRegister.Rdi];
if (tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return unchecked((int)0x80B50002);
}
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
@@ -63,7 +254,87 @@ public static class RtcExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "CyIK-i4XdgQ",
ExportName = "sceRtcGetDayOfWeek",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetDayOfWeek(CpuContext ctx)
{
var year = unchecked((int)ctx[CpuRegister.Rdi]);
var month = unchecked((int)ctx[CpuRegister.Rsi]);
var day = unchecked((int)ctx[CpuRegister.Rdx]);
if (!IsValidCalendarDate(year, month, day))
{
return unchecked((int)0x80B50004);
}
return (int)new DateTime(year, month, day).DayOfWeek;
}
[SysAbiExport(
Nid = "3O7Ln8AqJ1o",
ExportName = "sceRtcGetDaysInMonth",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetDaysInMonth(CpuContext ctx)
{
var year = unchecked((int)ctx[CpuRegister.Rdi]);
var month = unchecked((int)ctx[CpuRegister.Rsi]);
if (year < 1 || year > 9999)
{
return unchecked((int)0x80B50008);
}
if (month < 1 || month > 12)
{
return unchecked((int)0x80B50009);
}
return DateTime.DaysInMonth(year, month);
}
[SysAbiExport(
Nid = "E7AR4o7Ny7E",
ExportName = "sceRtcGetDosTime",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetDosTime(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
var dosTimeAddress = ctx[CpuRegister.Rsi];
if (timeAddress == 0 || dosTimeAddress == 0)
{
return unchecked((int)0x80B50002);
}
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var validationResult = ValidateRtcDateTime(time);
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return validationResult;
}
uint dosTime = 0;
dosTime |= (uint)((time.Second / 2) & 0x1F);
dosTime |= (uint)(time.Minute & 0x3F) << 5;
dosTime |= (uint)(time.Hour & 0x1F) << 11;
dosTime |= (uint)(time.Day & 0x1F) << 16;
dosTime |= (uint)(time.Month & 0x0F) << 21;
dosTime |= (uint)((time.Year - 1980) & 0x7F) << 25;
if (!ctx.TryWriteUInt32(dosTimeAddress, dosTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -74,45 +345,220 @@ public static class RtcExports
LibraryName = "libSceRtc")]
public static int RtcGetTick(CpuContext ctx)
{
var dateTimeAddress = ctx[CpuRegister.Rdi];
var timeAddress = ctx[CpuRegister.Rdi];
var tickAddress = ctx[CpuRegister.Rsi];
if (dateTimeAddress == 0 || tickAddress == 0)
if (timeAddress == 0 || tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return unchecked((int)0x80B50002);
}
if (!TryReadRtcDateTime(ctx, dateTimeAddress, out var rtcDateTime))
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong tickValue;
try
var validationResult = ValidateRtcDateTime(time);
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
var baseDateTime = new DateTime(
rtcDateTime.Year,
rtcDateTime.Month,
rtcDateTime.Day,
rtcDateTime.Hour,
rtcDateTime.Minute,
rtcDateTime.Second,
DateTimeKind.Utc);
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
return validationResult;
}
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
if (!TryConvertRtcDateTimeToTick(time, out var tick))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
if (!ctx.TryWriteUInt64(tickAddress, tick))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jMNwqYr4R-k",
ExportName = "sceRtcGetTickResolution",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetTickResolution(CpuContext ctx)
{
return (int)MicrosecondsPerSecond;
}
[SysAbiExport(
Nid = "BtqmpTRXHgk",
ExportName = "sceRtcGetTime_t",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetTimeT(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
var timeTAddress = ctx[CpuRegister.Rsi];
if (timeAddress == 0 || timeTAddress == 0)
{
return unchecked((int)0x80B50002);
}
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var validationResult = ValidateRtcDateTime(time);
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return validationResult;
}
if (!TryConvertRtcDateTimeToTick(time, out var tick))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var unixSeconds = tick < UnixEpochTicks ? 0UL : (tick - UnixEpochTicks) / MicrosecondsPerSecond;
if (!ctx.TryWriteUInt64(timeTAddress, unixSeconds))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jfRO0uTjtzA",
ExportName = "sceRtcGetWin32FileTime",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetWin32FileTime(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
var fileTimeAddress = ctx[CpuRegister.Rsi];
if (timeAddress == 0 || fileTimeAddress == 0)
{
return unchecked((int)0x80B50002);
}
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var validationResult = ValidateRtcDateTime(time);
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return validationResult;
}
if (!TryConvertRtcDateTimeToTick(time, out var tick))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var win32Time = tick < Win32FileTimeEpochTicks ? 0UL : (tick - Win32FileTimeEpochTicks) * 10UL;
if (!ctx.TryWriteUInt64(fileTimeAddress, win32Time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "LlodCMDbk3o",
ExportName = "sceRtcInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcInit(CpuContext ctx)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ug8pCwQvh0c",
ExportName = "sceRtcIsLeapYear",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcIsLeapYear(CpuContext ctx)
{
var year = unchecked((int)ctx[CpuRegister.Rdi]);
if (year < 1 || year > 9999)
{
return unchecked((int)0x80B50008);
}
return DateTime.IsLeapYear(year) ? 1 : 0;
}
[SysAbiExport(
Nid = "NR1J0N7L2xY",
ExportName = "sceRtcTickAddDays",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddDays(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerDay);
[SysAbiExport(
Nid = "MDc5cd8HfCA",
ExportName = "sceRtcTickAddHours",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddHours(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerHour);
[SysAbiExport(
Nid = "XPIiw58C+GM",
ExportName = "sceRtcTickAddMicroseconds",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddMicroseconds(CpuContext ctx) => AddTickDelta(ctx, 1UL);
[SysAbiExport(
Nid = "mn-tf4QiFzk",
ExportName = "sceRtcTickAddMinutes",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddMinutes(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerMinute);
[SysAbiExport(
Nid = "CL6y9q-XbuQ",
ExportName = "sceRtcTickAddMonths",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddMonths(CpuContext ctx)
{
return AddCalendarDelta(ctx, dateTime => dateTime.AddMonths(unchecked((int)ctx[CpuRegister.Rdx])));
}
[SysAbiExport(
Nid = "07O525HgICs",
ExportName = "sceRtcTickAddSeconds",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddSeconds(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerSecond);
[SysAbiExport(
Nid = "AqVMssr52Rc",
ExportName = "sceRtcTickAddTicks",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddTicks(CpuContext ctx) => AddTickDelta(ctx, 1UL);
[SysAbiExport(
Nid = "gI4t194c2W8",
ExportName = "sceRtcTickAddWeeks",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddWeeks(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerWeek);
[SysAbiExport(
Nid = "-5y2uJ62qS8",
ExportName = "sceRtcTickAddYears",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcTickAddYears(CpuContext ctx)
{
return AddCalendarDelta(ctx, dateTime => dateTime.AddYears(unchecked((int)ctx[CpuRegister.Rdx])));
}
[SysAbiExport(
Nid = "ueega6v3GUw",
ExportName = "sceRtcSetTick",
@@ -120,11 +566,11 @@ public static class RtcExports
LibraryName = "libSceRtc")]
public static int RtcSetTick(CpuContext ctx)
{
var dateTimeAddress = ctx[CpuRegister.Rdi];
var timeAddress = ctx[CpuRegister.Rdi];
var tickAddress = ctx[CpuRegister.Rsi];
if (dateTimeAddress == 0 || tickAddress == 0)
if (timeAddress == 0 || tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return unchecked((int)0x80B50002);
}
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
@@ -132,40 +578,249 @@ public static class RtcExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
if (!TryConvertTickToRtcDateTime(tickValue, out var time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return unchecked((int)0x80B50004);
}
DateTime dateTime;
try
{
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
}
catch (ArgumentOutOfRangeException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryReadRtcDateTime(CpuContext ctx, ulong address, out RtcDateTime rtcDateTime)
[SysAbiExport(
Nid = "aYPCd1cChyg",
ExportName = "sceRtcSetDosTime",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcSetDosTime(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
if (timeAddress == 0)
{
return unchecked((int)0x80B50002);
}
var dosTime = unchecked((uint)ctx[CpuRegister.Rsi]);
var time = new RtcDateTime(
(ushort)(1980 + ((dosTime >> 25) & 0x7F)),
(ushort)((dosTime >> 21) & 0x0F),
(ushort)((dosTime >> 16) & 0x1F),
(ushort)((dosTime >> 11) & 0x1F),
(ushort)((dosTime >> 5) & 0x3F),
(ushort)((dosTime << 1) & 0x3E),
0);
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bDEVVP4bTjQ",
ExportName = "sceRtcSetTime_t",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcSetTimeT(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
if (timeAddress == 0)
{
return unchecked((int)0x80B50002);
}
var timeSeconds = unchecked((long)ctx[CpuRegister.Rsi]);
if (timeSeconds < 0)
{
return unchecked((int)0x80B50003);
}
var tick = UnixEpochTicks + unchecked((ulong)timeSeconds) * MicrosecondsPerSecond;
if (!TryConvertTickToRtcDateTime(tick, out var time))
{
return unchecked((int)0x80B50003);
}
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "n5JiAJXsbcs",
ExportName = "sceRtcSetWin32FileTime",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcSetWin32FileTime(CpuContext ctx)
{
var timeAddress = ctx[CpuRegister.Rdi];
if (timeAddress == 0)
{
return unchecked((int)0x80B50002);
}
var fileTime = unchecked((long)ctx[CpuRegister.Rsi]);
if (fileTime < 0)
{
return unchecked((int)0x80B50003);
}
var tick = Win32FileTimeEpochTicks + unchecked((ulong)fileTime / 10UL);
if (!TryConvertTickToRtcDateTime(tick, out var time))
{
return unchecked((int)0x80B50003);
}
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int AddTickDelta(CpuContext ctx, ulong microsecondsPerUnit)
{
var destinationAddress = ctx[CpuRegister.Rdi];
var sourceAddress = ctx[CpuRegister.Rsi];
var delta = unchecked((long)ctx[CpuRegister.Rdx]);
if (destinationAddress == 0 || sourceAddress == 0)
{
return unchecked((int)0x80B50002);
}
if (!ctx.TryReadUInt64(sourceAddress, out var sourceTick))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
try
{
var resultTick = checked((long)sourceTick + checked(delta * (long)microsecondsPerUnit));
if (resultTick < 0)
{
return unchecked((int)0x80B50003);
}
if (!ctx.TryWriteUInt64(destinationAddress, unchecked((ulong)resultTick)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
catch (OverflowException)
{
return unchecked((int)0x80B50003);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int AddCalendarDelta(CpuContext ctx, Func<DateTime, DateTime> transform)
{
var destinationAddress = ctx[CpuRegister.Rdi];
var sourceAddress = ctx[CpuRegister.Rsi];
if (destinationAddress == 0 || sourceAddress == 0)
{
return unchecked((int)0x80B50002);
}
if (!ctx.TryReadUInt64(sourceAddress, out var sourceTick))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryConvertTickToDateTime(sourceTick, out var sourceDateTime))
{
return unchecked((int)0x80B50003);
}
DateTime resultDateTime;
try
{
resultDateTime = transform(sourceDateTime);
}
catch (ArgumentOutOfRangeException)
{
return unchecked((int)0x80B50003);
}
if (!TryWriteTickFromDateTime(ctx, destinationAddress, resultDateTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int ValidateRtcDateTime(RtcDateTime time)
{
if (time.Year < 1 || time.Year > 9999)
{
return unchecked((int)0x80B50008);
}
if (time.Month < 1 || time.Month > 12)
{
return unchecked((int)0x80B50009);
}
if (time.Day < 1 || time.Day > DateTime.DaysInMonth(time.Year, time.Month))
{
return unchecked((int)0x80B5000A);
}
if (time.Hour > 23)
{
return unchecked((int)0x80B5000B);
}
if (time.Minute > 59)
{
return unchecked((int)0x80B5000C);
}
if (time.Second > 59)
{
return unchecked((int)0x80B5000D);
}
if (time.Microsecond > 999_999)
{
return unchecked((int)0x80B5000E);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool IsValidCalendarDate(int year, int month, int day)
{
if (year < 1 || year > 9999 || month < 1 || month > 12)
{
return false;
}
return day >= 1 && day <= DateTime.DaysInMonth(year, month);
}
private static bool TryReadRtcDateTime(CpuContext ctx, ulong address, out RtcDateTime time)
{
Span<byte> buffer = stackalloc byte[16];
if (!ctx.Memory.TryRead(address, buffer))
{
rtcDateTime = default;
time = default;
return false;
}
rtcDateTime = new RtcDateTime(
time = new RtcDateTime(
BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]),
BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4]),
BinaryPrimitives.ReadUInt16LittleEndian(buffer[4..6]),
@@ -176,19 +831,117 @@ public static class RtcExports
return true;
}
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, RtcDateTime dateTime)
{
Span<byte> rtcDateTime = stackalloc byte[16];
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
BinaryPrimitives.WriteUInt32LittleEndian(
rtcDateTime[12..16],
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
return ctx.Memory.TryWrite(address, rtcDateTime);
Span<byte> buffer = stackalloc byte[16];
BinaryPrimitives.WriteUInt16LittleEndian(buffer[0..2], dateTime.Year);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[2..4], dateTime.Month);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..6], dateTime.Day);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[6..8], dateTime.Hour);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[8..10], dateTime.Minute);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[10..12], dateTime.Second);
BinaryPrimitives.WriteUInt32LittleEndian(buffer[12..16], dateTime.Microsecond);
return ctx.Memory.TryWrite(address, buffer);
}
private static RtcDateTime ToRtcDateTime(DateTime dateTime)
{
return new RtcDateTime(
(ushort)dateTime.Year,
(ushort)dateTime.Month,
(ushort)dateTime.Day,
(ushort)dateTime.Hour,
(ushort)dateTime.Minute,
(ushort)dateTime.Second,
(uint)(dateTime.Ticks % TimeSpan.TicksPerSecond / DateTimeTicksPerMicrosecond));
}
private static RtcDateTime ToRtcDateTime(DateTimeOffset dateTimeOffset)
{
return ToRtcDateTime(dateTimeOffset.DateTime);
}
private static bool TryWriteTickFromDateTime(CpuContext ctx, ulong address, DateTime dateTime)
{
return ctx.TryWriteUInt64(address, unchecked((ulong)(dateTime.Ticks / DateTimeTicksPerMicrosecond)));
}
private static bool TryConvertTickToDateTime(ulong tick, out DateTime dateTime)
{
var maxSupportedTick = unchecked((ulong)(DateTime.MaxValue.Ticks / DateTimeTicksPerMicrosecond));
if (tick > maxSupportedTick)
{
dateTime = default;
return false;
}
try
{
dateTime = new DateTime(checked((long)(tick * (ulong)DateTimeTicksPerMicrosecond)), DateTimeKind.Utc);
return true;
}
catch (ArgumentOutOfRangeException)
{
dateTime = default;
return false;
}
}
private static bool TryConvertRtcDateTimeToTick(RtcDateTime time, out ulong tick)
{
try
{
var year = time.Year;
var month = time.Month;
var day = time.Day;
if (month > 2)
{
month -= 3;
}
else
{
month += 9;
year -= 1;
}
var century = year / 100;
var yearOfCentury = year - (100 * century);
ulong days = ((146097UL * (ulong)century) >> 2)
+ ((1461UL * (ulong)yearOfCentury) >> 2)
+ ((153UL * (ulong)month + 2UL) / 5UL)
+ day;
days -= 307UL;
days *= MicrosecondsPerDay;
var timeOfDay = (ulong)time.Hour * MicrosecondsPerHour
+ (ulong)time.Minute * MicrosecondsPerMinute
+ (ulong)time.Second * MicrosecondsPerSecond
+ time.Microsecond;
tick = days + timeOfDay;
return true;
}
catch (OverflowException)
{
tick = 0;
return false;
}
}
private static bool TryConvertTickToRtcDateTime(ulong tick, out RtcDateTime time)
{
try
{
var dateTime = new DateTime(checked((long)(tick * DateTimeTicksPerMicrosecond)), DateTimeKind.Utc);
time = ToRtcDateTime(dateTime);
return true;
}
catch (ArgumentOutOfRangeException)
{
time = default;
return false;
}
}
private readonly record struct RtcDateTime(
@@ -244,7 +244,6 @@ public static class SaveDataExports
}
}
private static int _nextTransactionResource;
[SysAbiExport(
Nid = "gjRZNnw0JPE",
ExportName = "sceSaveDataCreateTransactionResource",
@@ -252,24 +251,25 @@ public static class SaveDataExports
LibraryName = "libSceSaveData")]
public static int SaveDataCreateTransactionResource(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var reserved = ctx[CpuRegister.Rsi];
var resourceAddress = ctx[CpuRegister.Rdx];
// Gen5 ABI: memory size, resource output, reserved.
var memorySize = ctx[CpuRegister.Rdi];
var resourceAddress = ctx[CpuRegister.Rsi];
var reserved = ctx[CpuRegister.Rdx];
if (resourceAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
if (!ctx.TryWriteUInt32(resourceAddress, id))
// Offline HLE operations carry no transaction state.
if (!ctx.TryWriteUInt64(resourceAddress, 0))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSaveData(
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
$"create_transaction_resource memory_size=0x{memorySize:X} reserved=0x{reserved:X} " +
$"resource_addr=0x{resourceAddress:X} resource=0x0");
return ctx.SetReturn(0);
}
@@ -72,4 +72,26 @@ public static class SystemGestureExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "1MMK0W-kMgA",
ExportName = "sceSystemGestureAppendTouchRecognizer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureAppendTouchRecognizer(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "wPJGwI2RM2I",
ExportName = "sceSystemGestureUpdateAllTouchRecognizer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureUpdateAllTouchRecognizer(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -96,4 +96,11 @@ public static class SystemServiceExports
VulkanVideoPresenter.HideSplashScreen();
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "3s8cHiCBKBE",
ExportName = "sceSystemServiceReportAbnormalTermination",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceReportAbnormalTermination(CpuContext ctx) => ctx.SetReturn(0);
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ult;
public static class UltExports
{
// Games initialize the ULT (user-level thread) runtime before spinning up
// their job systems; an unresolved import here (0x80020002) makes engines
// treat the whole task system as unavailable. The emulator schedules guest
// pthreads natively, so accepting the initialization is sufficient.
[SysAbiExport(
Nid = "hZIg1EWGsHM",
ExportName = "sceUltInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUlt")]
public static int UltInitialize(CpuContext ctx)
{
return ctx.SetReturn(0, typeof(long));
}
}
@@ -13,11 +13,15 @@ public static class UserServiceExports
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
private const int PrimaryUserId = 1;
// Retail user-service IDs begin at 0x3E8.
private const int PrimaryUserId = 1000;
private const int InvalidUserId = -1;
private const string PrimaryUserName = "SharpEmu";
private static int _loginEventDelivered;
private static readonly bool _traceUserService =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"), "1", StringComparison.Ordinal);
[SysAbiExport(
Nid = "j3YMu1MVNNo",
ExportName = "sceUserServiceInitialize",
@@ -42,9 +46,11 @@ public static class UserServiceExports
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
}
return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
var result = ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
TraceUserService($"get_initial_user out=0x{userIdAddress:X16} value={PrimaryUserId} result=0x{result:X8}");
return ctx.SetReturn(result);
}
[SysAbiExport(
@@ -106,7 +112,8 @@ public static class UserServiceExports
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var nameAddress = ctx[CpuRegister.Rsi];
var capacity = ctx[CpuRegister.Rdx];
if (userId != PrimaryUserId)
// Zero selects the current user.
if (userId != 0 && userId != PrimaryUserId)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
}
@@ -124,9 +131,34 @@ public static class UserServiceExports
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
nameBytes.CopyTo(output);
return ctx.Memory.TryWrite(nameAddress, output)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
var result = ctx.Memory.TryWrite(nameAddress, output)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
TraceUserService(
$"get_user_name user={userId} out=0x{nameAddress:X16} capacity=0x{capacity:X} value='{PrimaryUserName}' result=0x{result:X8}");
return ctx.SetReturn(result);
}
[SysAbiExport(
Nid = "-sD02mFDBh4",
ExportName = "sceUserServiceGetGamePresets",
Target = Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetGamePresets(CpuContext ctx)
{
// Return deterministic defaults for the offline profile.
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var resultAddress = ctx[CpuRegister.Rcx];
Span<byte> defaults = stackalloc byte[0x18];
if (resultAddress == 0 || !ctx.Memory.TryWrite(resultAddress, defaults))
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
}
TraceUserService(
$"get_game_presets user={userId} title=0x{ctx[CpuRegister.Rsi]:X16} " +
$"key=0x{ctx[CpuRegister.R9]:X} out=0x{resultAddress:X16} result=0x00000000");
return ctx.SetReturn(0);
}
[SysAbiExport(
@@ -152,4 +184,12 @@ public static class UserServiceExports
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static void TraceUserService(string message)
{
if (_traceUserService)
{
Console.Error.WriteLine($"[LOADER][TRACE] user_service.{message}");
}
}
}
+26 -5
View File
@@ -14,6 +14,17 @@ internal static class PngSplashLoader
];
public static bool TryLoad(out byte[] pixels, out uint width, out uint height)
=> TryLoad("pic0.png", requestRgba: false, out pixels, out width, out height);
public static bool TryLoadIcon(out byte[] pixels, out uint width, out uint height)
=> TryLoad("icon0.png", requestRgba: true, out pixels, out width, out height);
private static bool TryLoad(
string fileName,
bool requestRgba,
out byte[] pixels,
out uint width,
out uint height)
{
pixels = [];
width = 0;
@@ -27,13 +38,18 @@ internal static class PngSplashLoader
return false;
}
var path = Path.Combine(app0Root, "sce_sys", "pic0.png");
var path = Path.Combine(app0Root, "sce_sys", fileName);
if (!File.Exists(path))
{
return false;
}
return TryDecode(File.ReadAllBytes(path), out pixels, out width, out height);
return TryDecode(
File.ReadAllBytes(path),
out pixels,
out width,
out height,
requestRgba);
}
catch
{
@@ -48,7 +64,8 @@ internal static class PngSplashLoader
ReadOnlySpan<byte> png,
out byte[] pixels,
out uint width,
out uint height)
out uint height,
bool requestRgba)
{
pixels = [];
width = 0;
@@ -154,9 +171,13 @@ internal static class PngSplashLoader
sourceOffset < reconstructed.Length;
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
{
pixels[targetOffset] = reconstructed[sourceOffset + 2];
pixels[targetOffset] = requestRgba
? reconstructed[sourceOffset]
: reconstructed[sourceOffset + 2];
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
pixels[targetOffset + 2] = reconstructed[sourceOffset];
pixels[targetOffset + 2] = requestRgba
? reconstructed[sourceOffset + 2]
: reconstructed[sourceOffset];
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
? reconstructed[sourceOffset + 3]
: (byte)0xFF;
+317 -13
View File
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using SharpEmu.Logging;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -38,26 +39,143 @@ public static class VideoOutExports
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
private const ulong SceVideoOutInternalEventVblank = 0x5;
private const ulong SceVideoOutInternalEventFlip = 0x6;
private const short OrbisKernelEventFilterVideoOut = -13;
private static readonly object _stateGate = new();
private static readonly object _frameDumpGate = new();
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
// Hardware raises vblank autonomously; UE blocks its frame loop on it.
private const double VblankHz = 60.0;
private const int VblankWaitTimeoutMilliseconds = 100;
private static Thread? _vblankPumpThread;
private static int _vblankPumpStarted;
private static readonly object _vblankEdgeGate = new();
private static ulong _vblankEdgeSequence;
private static long _vblankMissedEdges;
private static void EnsureVblankPumpStarted()
{
if (Interlocked.Exchange(ref _vblankPumpStarted, 1) != 0)
{
return;
}
HostTimerResolution.Request();
_vblankPumpThread = new Thread(VblankPumpLoop)
{
IsBackground = true,
Name = "SharpEmu VideoOut vblank",
Priority = ThreadPriority.AboveNormal,
};
_vblankPumpThread.Start();
}
private static void VblankPumpLoop()
{
var intervalTicks = Math.Max(1L, (long)(Stopwatch.Frequency / VblankHz));
var nextEdge = Stopwatch.GetTimestamp() + intervalTicks;
while (true)
{
WaitUntilTimestamp(nextEdge);
PumpVblanks();
nextEdge += intervalTicks;
var now = Stopwatch.GetTimestamp();
if (nextEdge < now)
{
var missed = (now - nextEdge) / intervalTicks + 1;
Interlocked.Add(ref _vblankMissedEdges, missed);
nextEdge = now + intervalTicks;
}
}
}
private static void WaitUntilTimestamp(long deadlineTicks)
{
var spinThresholdTicks = Stopwatch.Frequency * 2L / 1000L;
while (true)
{
var remaining = deadlineTicks - Stopwatch.GetTimestamp();
if (remaining <= 0)
{
return;
}
if (remaining > spinThresholdTicks)
{
var sleepMilliseconds =
(int)((remaining - spinThresholdTicks) * 1000L / Stopwatch.Frequency);
if (sleepMilliseconds > 0)
{
Thread.Sleep(sleepMilliseconds);
continue;
}
}
Thread.SpinWait(64);
}
}
private static void PumpVblanks()
{
lock (_vblankEdgeGate)
{
_vblankEdgeSequence++;
Monitor.PulseAll(_vblankEdgeGate);
}
VideoOutPortState[] ports;
lock (_stateGate)
{
if (_ports.Count == 0)
{
return;
}
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
}
foreach (var port in ports)
{
SignalVblank(port);
}
}
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
private static int _nextHandle = 1;
private static int _frameDumpCount;
private static long _nextFrameDumpIndex;
private static string _windowTitleBase = "SharpEmu VideoOut";
private static string _windowTitle = "SharpEmu VideoOut";
private static readonly bool _logFrameRate = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
"1",
StringComparison.Ordinal);
private static readonly bool _logVideoOutSync = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
"1",
StringComparison.Ordinal);
private static readonly bool _dumpVideoOut = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
"1",
StringComparison.Ordinal);
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
private static long _submittedFrameCount;
private static long _presentedFrameCount;
private static long _vblankSignalCount;
private static long _flipSubmitCount;
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version, string? emulatorCommitSha)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title))
@@ -72,9 +190,25 @@ public static class VideoOutExports
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
var hardwareSuffix = $" \u00b7 {HostSystemInfo.CpuName}";
lock (_stateGate)
{
_windowTitle = $"SharpEmu - {application}{versionSuffix}";
_windowTitleBase = $"SharpEmu{commitSuffix} - {application}{versionSuffix}{hardwareSuffix}";
_windowTitle = $"{_windowTitleBase} \u00b7 {HostSystemInfo.GpuName}";
}
}
internal static void SetSelectedGpuName(string gpuName)
{
if (string.IsNullOrWhiteSpace(gpuName))
{
return;
}
lock (_stateGate)
{
_windowTitle = $"{_windowTitleBase} \u00b7 {gpuName.Trim()}";
}
}
@@ -99,6 +233,7 @@ public static class VideoOutExports
public float Gamma { get; set; } = 1.0f;
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
public List<FlipEventRegistration> VblankEvents { get; } = new();
public List<FlipEventRegistration> FlipEvents { get; } = new();
}
@@ -168,6 +303,7 @@ public static class VideoOutExports
{
Handle = handle,
};
EnsureVblankPumpStarted();
return handle;
}
}
@@ -311,12 +447,62 @@ public static class VideoOutExports
return OrbisVideoOutErrorInvalidHandle;
}
Thread.Sleep(1);
lock (_stateGate)
EnsureVblankPumpStarted();
lock (_vblankEdgeGate)
{
port.VblankCount++;
var entryEdge = _vblankEdgeSequence;
while (_vblankEdgeSequence == entryEdge)
{
if (!Monitor.Wait(_vblankEdgeGate, VblankWaitTimeoutMilliseconds))
{
break;
}
}
}
SignalVblank(port);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Xru92wHJRmg",
ExportName = "sceVideoOutAddVblankEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutAddVblankEvent(CpuContext ctx)
{
var equeue = ctx[CpuRegister.Rdi];
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
{
return OrbisVideoOutErrorInvalidEventQueue;
}
var userData = ctx[CpuRegister.Rdx];
lock (_stateGate)
{
var existingIndex = port.VblankEvents.FindIndex(registration => registration.Equeue == equeue);
if (existingIndex >= 0)
{
port.VblankEvents[existingIndex] = new FlipEventRegistration(equeue, userData);
}
else
{
port.VblankEvents.Add(new FlipEventRegistration(equeue, userData));
}
}
// Some engines wait on this queue before issuing their first flip. Provide a first
// edge now; later calls to WaitVblank advance the same notification sequence.
SignalVblank(port);
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -371,6 +557,53 @@ public static class VideoOutExports
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: true);
}
// Struct layout matches the classic SceVideoOutFlipStatus (40 bytes):
// count, processTime, tsc, flipArg, currentBuffer, flipPendingNum.
[SysAbiExport(
Nid = "SbU3dwp80lQ",
ExportName = "sceVideoOutGetFlipStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutGetFlipStatus(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
VideoOutPortState? port;
lock (_stateGate)
{
_ports.TryGetValue(handle, out port);
}
if (port is null)
{
return OrbisVideoOutErrorInvalidHandle;
}
ulong count;
long flipArg;
uint currentBuffer;
lock (_stateGate)
{
count = port.FlipCount;
flipArg = 0;
currentBuffer = unchecked((uint)port.CurrentBuffer);
}
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x00, count);
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x08, 0);
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0);
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "zgXifHT9ErY",
ExportName = "sceVideoOutIsFlipPending",
@@ -407,7 +640,8 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
if (filter != OrbisKernelEventFilterVideoOut ||
ident is not (SceVideoOutInternalEventVblank or SceVideoOutInternalEventFlip))
{
return OrbisVideoOutErrorInvalidEvent;
}
@@ -436,7 +670,8 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
if (filter != OrbisKernelEventFilterVideoOut ||
ident is not (SceVideoOutInternalEventVblank or SceVideoOutInternalEventFlip))
{
return OrbisVideoOutErrorInvalidEvent;
}
@@ -756,6 +991,38 @@ public static class VideoOutExports
return groupIndex < 0 ? groupIndex : setIndex;
}
private static void SignalVblank(VideoOutPortState port)
{
List<FlipEventRegistration> vblankEvents;
ulong eventHint;
lock (_stateGate)
{
port.VblankCount++;
eventHint = SceVideoOutInternalEventVblank |
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
vblankEvents = new List<FlipEventRegistration>(port.VblankEvents);
}
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
foreach (var vblankEvent in vblankEvents)
{
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
vblankEvent.Equeue,
SceVideoOutInternalEventVblank,
OrbisKernelEventFilterVideoOut,
eventHint,
vblankEvent.UserData);
}
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
{
Console.Error.WriteLine(
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
$"queues={vblankEvents.Count} hint=0x{eventHint:X16}");
}
}
private static int SubmitFlip(
CpuContext ctx,
int handle,
@@ -790,21 +1057,21 @@ public static class VideoOutExports
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
}
var guestImageSubmitted = false;
ulong guestImageAddress = 0;
if (submitGpuImage &&
bufferIndex >= 0 &&
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
{
_ = VulkanVideoPresenter.TrySubmitGuestImage(
guestImageAddress = displayBuffer.Address;
guestImageSubmitted = VulkanVideoPresenter.TrySubmitGuestImage(
displayBuffer.Address,
displayBuffer.Width,
displayBuffer.Height,
displayBuffer.PitchInPixel);
}
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
"1",
StringComparison.Ordinal))
if (_dumpVideoOut)
{
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
}
@@ -819,6 +1086,15 @@ public static class VideoOutExports
flipEvent.UserData);
}
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
if (_logVideoOutSync && (flipCount <= 8 || flipCount % 60 == 0))
{
Console.Error.WriteLine(
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
$"flipQueues={flipEvents.Count}");
}
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
ReportFrameRate(presented: false);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -855,9 +1131,11 @@ public static class VideoOutExports
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
var missedEdges = Interlocked.Exchange(ref _vblankMissedEdges, 0);
Console.Error.WriteLine(
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
$"presented_fps={presentedCount / elapsedSeconds:F1}");
$"presented_fps={presentedCount / elapsedSeconds:F1} " +
$"vblank_missed={missedEdges}");
}
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
@@ -902,6 +1180,16 @@ public static class VideoOutExports
TraceVideoOut(
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
if (guestFormat != 0)
{
foreach (var address in addresses)
{
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
}
}
return groupIndex;
}
}
@@ -1123,6 +1411,22 @@ public static class VideoOutExports
? 4u
: 0u;
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
// VulkanVideoPresenter._availableGuestImages keys on (see VulkanVideoPresenter.
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
NormalizePixelFormat(pixelFormat) switch
{
SceVideoOutPixelFormatA8R8G8B8Srgb or
SceVideoOutPixelFormatA8B8G8R8Srgb or
SceVideoOutPixelFormatB8G8R8A8Unorm or
SceVideoOutPixelFormatR8G8B8A8Unorm => 56u,
SceVideoOutPixelFormatA2R10G10B10 or
SceVideoOutPixelFormatA2R10G10B10Srgb or
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq => 9u,
_ => 0u,
};
private static ulong NormalizePixelFormat(ulong pixelFormat)
{
if (GetBytesPerPixel(pixelFormat) != 0)
@@ -11,6 +11,7 @@ using Silk.NET.Vulkan.Extensions.EXT;
using Silk.NET.Windowing;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using VkBuffer = Silk.NET.Vulkan.Buffer;
@@ -178,10 +179,18 @@ internal static unsafe class VulkanVideoPresenter
private static uint _windowHeight;
private static bool _closed;
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
private const uint NvidiaVendorId = 0x10DE;
private static bool _splashHidden;
private static long _enqueuedGuestWorkSequence;
private static long _completedGuestWorkSequence;
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
{
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
return string.Equals(mode, "1", StringComparison.Ordinal) ||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
}
public static void EnsureStarted(uint width, uint height)
{
if (width == 0 || height == 0)
@@ -279,6 +288,11 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=Submit {width}x{height}");
}
lock (_gate)
{
if (_closed)
@@ -319,6 +333,11 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=SubmitGuestDraw({drawKind}) {width}x{height}");
}
lock (_gate)
{
if (_closed ||
@@ -376,6 +395,12 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=SubmitTranslatedDraw {width}x{height} textures={textures.Count}");
}
lock (_gate)
{
if (_closed)
@@ -442,6 +467,13 @@ internal static unsafe class VulkanVideoPresenter
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
$"target=0x{target.Address:X16} {target.Width}x{target.Height} textures={textures.Count}");
}
lock (_gate)
{
if (_closed)
@@ -569,9 +601,32 @@ internal static unsafe class VulkanVideoPresenter
var traceSubmission = false;
lock (_gate)
{
if (_closed ||
!_availableGuestImages.ContainsKey(address))
// VideoOut registration does not imply a rendered Vulkan image.
var known = _gpuGuestImages.ContainsKey(address);
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=TrySubmitGuestImage addr=0x{address:X16} " +
$"{width}x{height} known={known}");
}
if (_closed)
{
return false;
}
// The caller (VideoOutExports.SubmitFlip) reports the flip as successful either
// way, so an unregistered address means the frame is dropped silently; warn once
// per address so that shows up in the log.
if (!known)
{
if (_tracedGuestImageSubmissions.Add((address, width, height)))
{
Console.Error.WriteLine(
$"[LOADER][WARN] vk.submit_guest_image_unknown addr=0x{address:X16} " +
$"{width}x{height} - flip target has no completed GPU render output");
}
return false;
}
@@ -588,6 +643,19 @@ internal static unsafe class VulkanVideoPresenter
RequiredGuestWorkSequence: 0,
IsSplash: false,
GuestImageAddress: address);
if (_thread is not null)
{
return true;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
if (traceSubmission)
@@ -601,6 +669,21 @@ internal static unsafe class VulkanVideoPresenter
return true;
}
// Display buffers registered through sceVideoOutRegisterBuffers are valid flip targets
// even when no AGC render-target write to them was ever observed.
internal static void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
{
if (address == 0 || guestFormat == 0)
{
return;
}
lock (_gate)
{
_availableGuestImages[address] = guestFormat;
}
}
internal static bool IsGpuGuestImageAvailable(
ulong address,
uint format,
@@ -852,6 +935,8 @@ internal static unsafe class VulkanVideoPresenter
private ExtDebugUtils? _debugUtils;
private PhysicalDevice _physicalDevice;
private Device _device;
private PipelineCache _pipelineCache;
private string? _pipelineCachePath;
private Queue _queue;
private uint _queueFamilyIndex;
private SwapchainKHR _swapchain;
@@ -869,6 +954,9 @@ internal static unsafe class VulkanVideoPresenter
private CommandBuffer _presentationCommandBuffer;
private VkSemaphore _imageAvailable;
private VkSemaphore _renderFinished;
private Fence _presentationFence;
private bool _presentationInFlight;
private TranslatedDrawResources? _pendingPresentationResources;
private VkBuffer _stagingBuffer;
private DeviceMemory _stagingMemory;
private ulong _stagingSize;
@@ -885,6 +973,10 @@ internal static unsafe class VulkanVideoPresenter
private int _directPresentationCount;
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new();
private static readonly bool _dumpTextures = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"),
"1",
StringComparison.Ordinal);
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureUploads = new();
private readonly HashSet<(ulong Address, uint Width, uint Height, uint Format)> _dumpedTextures = new();
private readonly HashSet<(ulong Address, int Size)> _tracedGlobalBuffers = new();
@@ -907,7 +999,7 @@ internal static unsafe class VulkanVideoPresenter
private readonly record struct GraphicsPipelineKey(
string VertexShader,
string FragmentShader,
ulong RenderPass,
Format RenderTargetFormat,
PrimitiveTopology Topology,
VulkanGuestBlendState Blend,
string ResourceLayout,
@@ -1057,12 +1149,19 @@ internal static unsafe class VulkanVideoPresenter
private void Initialize()
{
if (PngSplashLoader.TryLoadIcon(out var iconPixels, out var iconWidth, out var iconHeight))
{
var icon = new RawImage((int)iconWidth, (int)iconHeight, iconPixels);
_window.SetWindowIcon(ref icon);
}
WaitForRenderDocAttachIfRequested();
_vk = Vk.GetApi();
CreateInstance();
CreateSurface();
SelectPhysicalDevice();
CreateDevice();
CreatePipelineCache();
CreateSwapchain();
CreateCommandResources();
CreateGuestDrawResources();
@@ -1422,6 +1521,13 @@ internal static unsafe class VulkanVideoPresenter
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices");
}
// Hybrid laptops enumerate the iGPU first, and AMD's integrated driver
// segfaults compiling some translated shaders, so rank rather than take
// the first hit. SHARPEMU_VK_DEVICE=<substring> pins an adapter by name.
var deviceOverride = Environment.GetEnvironmentVariable("SHARPEMU_VK_DEVICE");
var bestScore = int.MinValue;
var found = false;
foreach (var device in devices)
{
uint queueCount = 0;
@@ -1441,13 +1547,63 @@ internal static unsafe class VulkanVideoPresenter
continue;
}
_physicalDevice = device;
_queueFamilyIndex = index;
return;
_vk.GetPhysicalDeviceProperties(device, out var properties);
var name = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty;
var score = ScorePhysicalDevice(properties, name, deviceOverride);
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan candidate: {name} ({properties.DeviceType}) score={score}");
if (score > bestScore)
{
bestScore = score;
_physicalDevice = device;
_queueFamilyIndex = index;
found = true;
}
break;
}
}
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
if (!found)
{
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
}
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
VideoOutExports.SetSelectedGpuName(selectedName);
_window.Title = VideoOutExports.GetWindowTitle();
}
private static int ScorePhysicalDevice(
PhysicalDeviceProperties properties,
string name,
string? deviceOverride)
{
if (!string.IsNullOrWhiteSpace(deviceOverride))
{
return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000;
}
var score = properties.DeviceType switch
{
PhysicalDeviceType.DiscreteGpu => 300,
PhysicalDeviceType.VirtualGpu => 100,
PhysicalDeviceType.Cpu => 50,
// Last resort: only picked when nothing else can present.
PhysicalDeviceType.IntegratedGpu => -100,
_ => 10,
};
if (properties.VendorID == NvidiaVendorId)
{
score += 500;
}
return score;
}
private void CreateDevice()
@@ -1602,6 +1758,110 @@ internal static unsafe class VulkanVideoPresenter
}
}
private void CreatePipelineCache()
{
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties);
var directory = Path.Combine(AppContext.BaseDirectory, "pipeline-cache");
_pipelineCachePath = Path.Combine(
directory,
$"vulkan-{properties.VendorID:X4}-{properties.DeviceID:X4}-{properties.DriverVersion:X8}.bin");
byte[] initialData = [];
try
{
if (File.Exists(_pipelineCachePath))
{
initialData = File.ReadAllBytes(_pipelineCachePath);
}
}
catch (IOException exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache could not be read: {exception.Message}");
}
fixed (byte* initialDataPointer = initialData)
{
var createInfo = new PipelineCacheCreateInfo
{
SType = StructureType.PipelineCacheCreateInfo,
InitialDataSize = (nuint)initialData.Length,
PInitialData = initialData.Length == 0 ? null : initialDataPointer,
};
var result = _vk.CreatePipelineCache(
_device,
&createInfo,
null,
out _pipelineCache);
if (result != Result.Success && initialData.Length != 0)
{
createInfo.InitialDataSize = 0;
createInfo.PInitialData = null;
Check(
_vk.CreatePipelineCache(
_device,
&createInfo,
null,
out _pipelineCache),
"vkCreatePipelineCache(empty)");
initialData = [];
}
else
{
Check(result, "vkCreatePipelineCache");
}
}
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan pipeline cache: " +
$"{(initialData.Length == 0 ? "new" : $"loaded {initialData.Length} bytes")}");
}
private void SavePipelineCache()
{
if (_pipelineCache.Handle == 0 || string.IsNullOrWhiteSpace(_pipelineCachePath))
{
return;
}
try
{
nuint size = 0;
Check(
_vk.GetPipelineCacheData(_device, _pipelineCache, &size, null),
"vkGetPipelineCacheData(size)");
if (size == 0 || size > int.MaxValue)
{
return;
}
var data = new byte[(int)size];
fixed (byte* dataPointer = data)
{
Check(
_vk.GetPipelineCacheData(
_device,
_pipelineCache,
&size,
dataPointer),
"vkGetPipelineCacheData");
}
var directory = Path.GetDirectoryName(_pipelineCachePath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllBytes(_pipelineCachePath, data.AsSpan(0, (int)size).ToArray());
}
catch (Exception exception) when (
exception is IOException or UnauthorizedAccessException or InvalidOperationException)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan pipeline cache could not be saved: {exception.Message}");
}
}
private void CreateSwapchain()
{
Check(
@@ -1694,9 +1954,44 @@ internal static unsafe class VulkanVideoPresenter
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _imageAvailable), "vkCreateSemaphore");
Check(_vk.CreateSemaphore(_device, &semaphoreInfo, null, out _renderFinished), "vkCreateSemaphore");
var fenceInfo = new FenceCreateInfo
{
SType = StructureType.FenceCreateInfo,
};
Check(
_vk.CreateFence(_device, &fenceInfo, null, out _presentationFence),
"vkCreateFence(presentation)");
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
}
private void CompletePendingPresentation(bool wait)
{
if (!_presentationInFlight)
{
return;
}
var fence = _presentationFence;
var result = wait
? _vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue)
: _vk.GetFenceStatus(_device, fence);
if (!wait && result == Result.NotReady)
{
return;
}
Check(result, "vkWaitForFences(presentation)");
_presentationInFlight = false;
if (_pendingPresentationResources is not null)
{
MarkSampledImagesInitialized(_pendingPresentationResources);
MarkStorageImagesInitialized(_pendingPresentationResources);
DestroyTranslatedDrawResources(_pendingPresentationResources);
_pendingPresentationResources = null;
}
}
private CommandBuffer AllocateGuestCommandBuffer()
{
var allocateInfo = new CommandBufferAllocateInfo
@@ -2043,7 +2338,7 @@ internal static unsafe class VulkanVideoPresenter
Check(
_vk.CreateGraphicsPipelines(
_device,
default,
_pipelineCache,
1,
&pipelineInfo,
null,
@@ -2078,6 +2373,7 @@ internal static unsafe class VulkanVideoPresenter
private TranslatedDrawResources CreateTranslatedDrawResources(
VulkanTranslatedGuestDraw draw,
RenderPass renderPass,
Format renderTargetFormat,
Extent2D extent)
{
var vertexSpirv = draw.VertexSpirv;
@@ -2144,7 +2440,13 @@ internal static unsafe class VulkanVideoPresenter
CreateTranslatedDescriptorResources(
resources,
ShaderStageFlags.VertexBit | ShaderStageFlags.FragmentBit);
CreateTranslatedPipeline(resources, vertexSpirv, draw.PixelSpirv, renderPass, extent);
CreateTranslatedPipeline(
resources,
vertexSpirv,
draw.PixelSpirv,
renderPass,
renderTargetFormat,
extent);
return resources;
}
catch
@@ -2440,12 +2742,13 @@ internal static unsafe class VulkanVideoPresenter
byte[] vertexSpirv,
byte[] fragmentSpirv,
RenderPass renderPass,
Format renderTargetFormat,
Extent2D extent)
{
var pipelineKey = new GraphicsPipelineKey(
GetShaderDigest(vertexSpirv),
GetShaderDigest(fragmentSpirv),
renderPass.Handle,
renderTargetFormat,
resources.Topology,
resources.Blend,
GetResourceLayoutKey(resources),
@@ -2601,7 +2904,7 @@ internal static unsafe class VulkanVideoPresenter
Check(
_vk.CreateGraphicsPipelines(
_device,
default,
_pipelineCache,
1,
&pipelineInfo,
null,
@@ -2781,7 +3084,7 @@ internal static unsafe class VulkanVideoPresenter
Check(
_vk.CreateComputePipelines(
_device,
default,
_pipelineCache,
1,
&pipelineInfo,
null,
@@ -3307,11 +3610,36 @@ internal static unsafe class VulkanVideoPresenter
{
const ulong offsetBasis = 14695981039346656037;
const ulong prime = 1099511628211;
var hash = offsetBasis;
foreach (var value in pixels)
var h0 = offsetBasis ^ (ulong)pixels.Length;
var h1 = offsetBasis;
var h2 = offsetBasis;
var h3 = offsetBasis;
var words = MemoryMarshal.Cast<byte, ulong>(pixels);
var index = 0;
var blockEnd = words.Length - (words.Length & 3);
for (; index < blockEnd; index += 4)
{
hash ^= value;
hash *= prime;
h0 = (h0 ^ words[index]) * prime;
h1 = (h1 ^ words[index + 1]) * prime;
h2 = (h2 ^ words[index + 2]) * prime;
h3 = (h3 ^ words[index + 3]) * prime;
}
for (; index < words.Length; index++)
{
h0 = (h0 ^ words[index]) * prime;
}
var hash = h0;
hash = (hash ^ h1) * prime;
hash = (hash ^ h2) * prime;
hash = (hash ^ h3) * prime;
foreach (var value in pixels[(words.Length * sizeof(ulong))..])
{
hash = (hash ^ value) * prime;
}
return hash;
@@ -3324,10 +3652,7 @@ internal static unsafe class VulkanVideoPresenter
uint width,
uint height)
{
if (!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_TEXTURES"),
"1",
StringComparison.Ordinal) ||
if (!_dumpTextures ||
texture.IsFallback ||
texture.IsStorage ||
GetTextureBytesPerPixel(texture.Format) != 4 ||
@@ -4348,6 +4673,7 @@ internal static unsafe class VulkanVideoPresenter
resources = CreateTranslatedDrawResources(
work.Draw,
target.RenderPass,
target.Format,
extent);
resources.DebugName =
$"SharpEmu offscreen rt=0x{work.Target.Address:X16} " +
@@ -4984,6 +5310,8 @@ internal static unsafe class VulkanVideoPresenter
return;
}
CompletePendingPresentation(wait: true);
byte[]? pixels = null;
if (presentation.Pixels is { } sourcePixels)
{
@@ -5031,16 +5359,22 @@ internal static unsafe class VulkanVideoPresenter
translatedResources = CreateTranslatedDrawResources(
translatedDraw,
_renderPass,
_swapchainFormat,
_extent);
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_firstGuestDrawPresented &&
translatedResources.Textures is
[
{ GuestImage: { } guestImage },
] &&
_tracedGuestImageContents.Add(guestImage.Address))
!_firstGuestDrawPresented)
{
TraceGuestImageContents(guestImage);
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.translated_draw kind={presentation.DrawKind} " +
$"textures={translatedResources.Textures.Length}");
foreach (var boundTexture in translatedResources.Textures)
{
if (boundTexture.GuestImage is { } guestImage &&
_tracedGuestImageContents.Add(guestImage.Address))
{
TraceGuestImageContents(guestImage);
}
}
}
}
catch (Exception exception)
@@ -5157,7 +5491,16 @@ internal static unsafe class VulkanVideoPresenter
SignalSemaphoreCount = 1,
PSignalSemaphores = &renderFinished,
};
Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit");
var presentationFence = _presentationFence;
Check(
_vk.ResetFences(_device, 1, &presentationFence),
"vkResetFences(presentation)");
Check(
_vk.QueueSubmit(_queue, 1, &submitInfo, presentationFence),
"vkQueueSubmit");
_presentationInFlight = true;
_pendingPresentationResources = translatedResources;
translatedResources = null;
var swapchain = _swapchain;
var presentInfo = new PresentInfoKHR
@@ -5173,6 +5516,7 @@ internal static unsafe class VulkanVideoPresenter
if (presentResult == Result.ErrorOutOfDateKhr)
{
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false);
if (translatedResources is not null)
{
@@ -5185,19 +5529,13 @@ internal static unsafe class VulkanVideoPresenter
CheckSwapchainResult(presentResult, "vkQueuePresentKHR");
recreateAfterPresent |= presentResult == Result.SuboptimalKhr;
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
VideoOutExports.ReportPresentedFrame();
if (_swapchainReadbackPending)
{
CompletePendingPresentation(wait: true);
TraceSwapchainReadback();
}
CollectCompletedGuestSubmissions(waitForOldest: false);
if (translatedResources is not null)
{
MarkSampledImagesInitialized(translatedResources);
MarkStorageImagesInitialized(translatedResources);
DestroyTranslatedDrawResources(translatedResources);
}
_imageInitialized[imageIndex] = true;
_presentedSequence = presentation.Sequence;
@@ -5810,13 +6148,6 @@ internal static unsafe class VulkanVideoPresenter
return false;
}
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
{
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
return string.Equals(mode, "1", StringComparison.Ordinal) ||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldTraceVulkanResources() =>
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"),
@@ -6458,7 +6789,9 @@ internal static unsafe class VulkanVideoPresenter
}
_vulkanReady = false;
_vk.DeviceWaitIdle(_device);
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false);
SavePipelineCache();
foreach (var pipeline in _computePipelines.Values)
{
_vk.DestroyPipeline(_device, pipeline, null);
@@ -6469,6 +6802,11 @@ internal static unsafe class VulkanVideoPresenter
_vk.DestroyPipeline(_device, pipeline, null);
}
_graphicsPipelines.Clear();
if (_pipelineCache.Handle != 0)
{
_vk.DestroyPipelineCache(_device, _pipelineCache, null);
_pipelineCache = default;
}
foreach (var layout in _descriptorLayouts.Values)
{
_vk.DestroyPipelineLayout(_device, layout.PipelineLayout, null);
@@ -6560,6 +6898,7 @@ internal static unsafe class VulkanVideoPresenter
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut recreating swapchain after {operation}: {result}");
_vk.DeviceWaitIdle(_device);
CompletePendingPresentation(wait: false);
CollectCompletedGuestSubmissions(waitForOldest: false);
DestroySwapchainResources();
CreateSwapchain();
@@ -6593,6 +6932,11 @@ internal static unsafe class VulkanVideoPresenter
_vk.DestroySemaphore(_device, _renderFinished, null);
_renderFinished = default;
}
if (_presentationFence.Handle != 0)
{
_vk.DestroyFence(_device, _presentationFence, null);
_presentationFence = default;
}
if (_barycentricPipeline.Handle != 0)
{
_vk.DestroyPipeline(_device, _barycentricPipeline, null);
@@ -0,0 +1,19 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Voice;
public static class VoiceQoSExports
{
[SysAbiExport(
Nid = "U8IfNl6-Css",
ExportName = "sceVoiceQoSInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVoiceQoS")]
public static int VoiceQoSInit(CpuContext ctx)
{
return ctx.SetReturn(0);
}
}
+6
View File
@@ -98,6 +98,12 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
}
}
+32 -10
View File
@@ -6,19 +6,15 @@ using System.Text;
namespace SharpEmu.Logging;
/// <summary>
/// Writes log entries to a file. Thread-safe via an internal lock.
/// <see cref="StreamWriter.AutoFlush"/> is enabled so entries survive a crash.
/// </summary>
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
{
private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500);
private readonly object _sync = new();
private readonly StreamWriter _writer;
private readonly Timer _flushTimer;
private bool _disposed;
/// <param name="path">Absolute or relative file path. Parent directories are created if missing.</param>
/// <param name="append"><see langword="true"/> to append to an existing file; <see langword="false"/> to overwrite.</param>
/// <param name="includeTimestamp">Always recommended for file logs — entries include a full date-time prefix.</param>
public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
@@ -34,14 +30,20 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
append ? FileMode.Append : FileMode.Create,
FileAccess.Write,
FileShare.Read,
bufferSize: 4096,
bufferSize: 65536,
FileOptions.SequentialScan);
_writer = new StreamWriter(fileStream, Encoding.UTF8)
_writer = new StreamWriter(fileStream, Encoding.UTF8, bufferSize: 65536)
{
AutoFlush = true
AutoFlush = false
};
IncludeTimestamp = includeTimestamp;
_flushTimer = new Timer(
static state => ((FileLogSink)state!).FlushBuffered(),
this,
FlushInterval,
FlushInterval);
}
public bool IncludeTimestamp { get; set; }
@@ -84,11 +86,31 @@ public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
{
_writer.WriteLine(entry.Exception);
}
if (entry.Level >= LogLevel.Error)
{
_writer.Flush();
}
}
}
private void FlushBuffered()
{
lock (_sync)
{
if (_disposed)
{
return;
}
_writer.Flush();
}
}
public void Dispose()
{
_flushTimer.Dispose();
lock (_sync)
{
if (_disposed)
+187
View File
@@ -0,0 +1,187 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Microsoft.Win32;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace SharpEmu.Logging;
/// <summary>Provides best-effort information about the host system.</summary>
public static class HostSystemInfo
{
private static readonly Lazy<string> CpuNameValue = new(GetCpuName);
private static readonly Lazy<string> GpuNameValue = new(GetPreferredGpuName);
private static readonly Lazy<string> MemoryDescriptionValue = new(GetMemoryDescription);
/// <summary>Host CPU name, or a safe fallback when it cannot be determined.</summary>
public static string CpuName => CpuNameValue.Value;
/// <summary>Preferred physical GPU name, or a safe fallback when it cannot be determined.</summary>
public static string GpuName => GpuNameValue.Value;
/// <summary>Returns a concise description of the host hardware for diagnostic logs.</summary>
public static string Summary =>
$"Host hardware: CPU: {CpuName}; GPU: {GpuName}; RAM: {MemoryDescriptionValue.Value}.";
private static string GetCpuName()
{
if (!OperatingSystem.IsWindows())
{
return $"{Environment.ProcessorCount} logical processors";
}
try
{
using var key = Registry.LocalMachine.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
if (key?.GetValue("ProcessorNameString") is string name && !string.IsNullOrWhiteSpace(name))
{
name = Regex.Replace(
name.Trim(),
@"\s+\d+-Core Processor$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
return Regex.Replace(
name,
@"\s+with Radeon Graphics$",
string.Empty,
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
}
}
catch (Exception)
{
// Hardware information is diagnostic only.
}
return $"{Environment.ProcessorCount} logical processors";
}
private static string GetPreferredGpuName()
{
if (!OperatingSystem.IsWindows())
{
return "unknown";
}
try
{
var preferredName = "unknown";
var preferredScore = int.MinValue;
for (uint index = 0; ; index++)
{
var device = new DisplayDevice
{
cb = Marshal.SizeOf<DisplayDevice>(),
};
if (!EnumDisplayDevices(null, index, ref device, 0))
{
break;
}
var name = device.DeviceString?.Trim();
var score = ScoreGpu(name);
if (score > preferredScore)
{
preferredName = name!;
preferredScore = score;
}
}
return preferredScore > 0 ? preferredName : "unknown";
}
catch (Exception)
{
// Hardware information is diagnostic only.
return "unknown";
}
}
private static int ScoreGpu(string? name)
{
if (string.IsNullOrWhiteSpace(name) ||
name.Contains("virtual", StringComparison.OrdinalIgnoreCase) ||
name.Contains("remote", StringComparison.OrdinalIgnoreCase) ||
name.Contains("basic display", StringComparison.OrdinalIgnoreCase))
{
return -1;
}
if (name.Contains("nvidia", StringComparison.OrdinalIgnoreCase) ||
name.Contains("geforce", StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (name.Contains("amd", StringComparison.OrdinalIgnoreCase) ||
name.Contains("radeon", StringComparison.OrdinalIgnoreCase))
{
return 2;
}
return 1;
}
private static string GetMemoryDescription()
{
if (OperatingSystem.IsWindows())
{
try
{
var status = new MemoryStatusEx
{
dwLength = (uint)Marshal.SizeOf<MemoryStatusEx>(),
};
if (GlobalMemoryStatusEx(ref status))
{
var megabytes = status.ullTotalPhys / (1024 * 1024);
var gigabytes = status.ullTotalPhys / (1024d * 1024 * 1024);
return $"{megabytes:N0} MB ({gigabytes:N1} GB)";
}
}
catch (Exception)
{
// Hardware information is diagnostic only.
}
}
return "unknown";
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DisplayDevice
{
public int cb;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string? DeviceName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string? DeviceString;
public int StateFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string? DeviceId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string? DeviceKey;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct MemoryStatusEx
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumDisplayDevices(string? deviceName, uint deviceNum, ref DisplayDevice displayDevice, uint flags);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);
}