Compare commits

..

147 Commits

Author SHA1 Message Date
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
Berk 79a7437cd8 [GUI] Added Atrac9 audio decoder and improved GUI with audio preview and controller support (#64)
* [GUI] Added Atrac9 audio decoder and improved GUI with audio preview and controller support

* fix: package.lock.json for SharpEmu.CLI to match the other projects

* fix: packages.lock.json file to include new dependencies for GUI improvements

* rollForward: "disable"
2026-07-11 19:14:08 +03:00
PandaCatz f43f7cde9c [cpu] Implement SysV variadic float ABI (xmm0-7 capture, float returns, printf %f) (#59)
* [cpu] Implement SysV variadic float ABI (xmm0-7 capture, float returns, printf %f)

The import trampoline spilled only xmm0 and never reloaded a return xmm0. The
guest uses the System V AMD64 ABI: variadic float args pass in xmm0..xmm7 and
float/double returns come back in xmm0. As a result variadic float args past
the first were unavailable to HLE handlers, float returns never reached the
guest, and direct printf read %f/%e/%g from GP registers instead of XMM,
printing garbage and desynchronizing every following argument.

- Trampoline: spill xmm0..xmm7 into a 0x80-byte save area below the GP argpack
  (r12 stays at the argpack base) and reload the return xmm0 in the epilogue.
- Gateway: read xmm0..7 from the save area into CpuContext and write the
  handler's xmm0 back. XMM is caller-saved in SysV, so restoring xmm0 on return
  is safe for non-float imports too.
- RegisterPrintfArgumentSource: read float args from xmm0..7 with independent
  GP/FP counters and a shared stack-overflow cursor.

Every emitted byte was decoded; a unit test confirms float args read xmm0..7
(not GP) and interleaved "%d %f %d %f" stays synchronized. Build 0/0.

* [cpu] Document the scalar-only leaf-import constraint at its registration site

- IsLeafImport: spell out the no-XMM-args / no-XMM-return invariant the fast
  path relies on and what breaks if it is violated; record the 2026-07-11 audit.
- Name every previously uncommented NID in the leaf list (mutex lock/unlock,
  usleep, the Ampr/Apr command-buffer block, the unknown AGC packet NID).
- IsNoBlockLeafImport: document that it is a sub-filter of IsLeafImport and
  that its five extra entries currently take the full gateway path; fix the
  mislabeled K-jXhbt2gn4 comment (pthread_mutex_trylock, not
  scePthreadMutexTrylock, which is upoVrzMHFeE).
- Point the DispatchImport call-site note at the audited list.

Comment-only change: the comment-stripped diff is empty and the solution
builds with 0 warnings / 0 errors.
2026-07-11 17:41:25 +03:00
j92580498-max ef74680167 More logger improvements (#58)
* fix

* fix

---------

Co-authored-by: j92580498-max <j92580498-max@users.noreply.github.com>
2026-07-11 17:34:49 +03:00
Mike Saito 65a40773fa core: expand clock_gettime fast clocks, unify timespec writes, fix NULL EINVAL (#62)
Refactored parts of the time subsystem to improve POSIX/Orbis compliance and secure guest memory boundaries.

**1. POSIX `clock_gettime` updates:**
- Added `CLOCK_REALTIME_FAST` (10) and `CLOCK_MONOTONIC_FAST` (12) support for games using FreeBSD fast clock extensions.
- Fixed `NULL` pointer handling for `timespecAddress == 0`. It now returns `-1` with `EINVAL` (22) instead of `EFAULT` to match Orbis runtime behavior.
- Invalid `clock_id` values now properly fallback to `default` -> `-1` + `EINVAL`.

**2. Memory safety & monotonic tracking:**
- Replaced dual 8-byte scalar writes in both POSIX `clock_gettime` and Orbis `sceKernelClockGettime` with a single 16-byte write via `stackalloc byte[16]` and `BinaryPrimitives`. This prevents partial memory corruption at page boundaries.
- Bad non-NULL guest addresses now fail cleanly as `EFAULT` (POSIX) or `MEMORY_FAULT` (Orbis).
- Extracted core monotonic math into `GetProcessMonotonicTime()` in `KernelRuntimeCompatExports.cs` so both clock paths share the exact same `_processStartCounter` base.

**3. Host RDTSC optimization:**
- Fixed `CreateRdtscReader()` to copy stack-allocated opcode bytes into host `VirtualAlloc` memory via `unsafe { Buffer.MemoryCopy(...) }`. This completely gets rid of the redundant `.ToArray()` allocation on the hot path.

**Out of scope:** `sceKernelGettimeofday` / POSIX `gettimeofday` partial-write hardening; stricter clock validation in `sceKernelClockGettime`.
2026-07-11 17:32:48 +03:00
Mike Saito 9ddc09ea91 core: page-aware TryReadUtf8Z and unify exit/_exit handling (#57)
Read guest C strings in page-bounded chunks without heap allocations.
Return false when the buffer fills without a null terminator. Route exit
and _exit through RequestProcessExit.
2026-07-11 15:42:43 +03:00
Brando c4326a1143 Update README.md for Discord (#55)
* Added Discord link
2026-07-11 14:21:06 +03:00
ParantezTech 347e33f3c9 [GUI] update icon to .ico format 2026-07-11 14:03:55 +03:00
Brando 48a694e509 GUI: redesign library as a cover-art grid with game management (#48)
* GUI: redesign library as a cover-art grid with game management

Replace the sidebar game list with a full-width grid of cover tiles.
Cover art is loaded automatically from each game's sce_sys/icon0.png
(pic0.png fallback) and decoded off the UI thread; games without art
get a deterministic gradient placeholder with the title's initials.

- New layout: search/scan toolbar, tile grid with hover and selection
  states, bottom launch bar with cover thumbnail, collapsible launch
  options and console panels (console auto-opens on launch)
- Right-click context menu on tiles: launch, open game folder, copy
  path/title ID, remove from library
- Removed games persist in an ExcludedGames settings list; re-adding
  a folder restores any removed games beneath it
- Search now also matches title IDs
- Fix: placeholder brushes were constructed on the scan thread, which
  throws in Avalonia and was silently swallowed, yielding empty scans

* GUI: show full install folder size instead of eboot.bin size

The library previously displayed the size of eboot.bin alone, which
wildly understates a game's real footprint. The install folder is now
totaled recursively in the existing background pass (after cover art,
which is cheaper and more visible), and each tile updates live once
its size is ready.

* GUI: selection backdrop, smaller tiles, controller navigation

Address review feedback on the library redesign:

- Selecting a game fades its key art (sce_sys/pic0.png, pic1.png
  fallback) in as the window backdrop, dimmed by a gradient scrim;
  decoded off the UI thread and cached per entry
- Cover tiles reduced from 156px to 128px
- The library can be driven with a DualSense: d-pad/left stick moves
  the selection (hold-to-repeat, row-aware), Cross launches, Circle
  stops; input is ignored while the launcher window is unfocused.
  Reuses the pad HID reader by compile-linking its dependency-free
  sources instead of referencing all of SharpEmu.Libs
2026-07-11 13:17:10 +03:00
Brando 165927882b Pad: native DualSense support via raw HID (#52)
* Pad: native DualSense support via raw HID

Read a real DualSense (or DualSense Edge) controller directly over
Win32 HID and feed its state into scePadRead/scePadReadState, replacing
the keyboard-only input path. No new dependencies.

- Device discovery by Sony VID/PID through setupapi/hid.dll, with
  hot-plug: disconnects fall back to keyboard and reconnect
  automatically
- USB input report 0x01 and Bluetooth extended report 0x31 (activated
  via the feature report 0x05 handshake) are both parsed
- Full mapping to SCE_PAD_BUTTON conventions: face buttons, d-pad hat,
  L1/R1/L2/R2 digital bits, analog triggers, L3/R3, Options, touchpad
  click, both sticks
- Controller and keyboard input merge: buttons OR together, controller
  sticks win past a small deadzone, triggers take the max

* Pad: rumble and lightbar output for DualSense

Wire scePadSetVibration, scePadSetLightBar and scePadResetLightBar to
real DualSense output reports. The output payload follows the same
layout as the Linux hid-playstation driver: both rumble motors,
lightbar RGB and the player LED indicator.

- USB uses output report 0x02; Bluetooth uses the 0x31 wrapper with a
  sequence tag and CRC32 (0xA2-seeded) trailer, transport detected
  from the first input report
- Output goes through a dedicated device handle so writes never
  contend with the blocking input read loop
- On connect the controller gets a default state (blue lightbar,
  player 1 LED); rumble state resets on disconnect

Verified on hardware over USB: lightbar color cycling and both motors.
Bluetooth output is implemented per spec but not yet hardware-tested.
2026-07-11 11:54:38 +03:00
kostyaff d9d1aeaef9 [logging] Migrate CpuDispatcher, SharpEmuRuntime, PhysicalVirtualMemory to SharpEmuLog (#51)
Replaces 34 Console.Error.WriteLine call sites across 3 Core files
with structured SharpEmuLog calls (Debug/Info/Warning/Error/Critical).

CpuDispatcher.cs (10 sites):
- DispatchEntry/DispatchModuleInitializer START and entry-point logs -> Debug
- FATAL EXCEPTION catch blocks -> Critical (with exception object)
- Native backend FAILED -> Error

SharpEmuRuntime.cs (23 sites):
- Loading/Entry/Dispatching/DispatchEntry returned -> Info
- Module load/registered/preload summary -> Info
- Initializer dispatch failed/module start failed -> Error
- Imported data unresolved -> Warning, write-failed -> Error
- Import stub conflict -> Warning
- Trace-level rebind logs -> Debug

PhysicalVirtualMemory.cs (1 site):
- TraceVmem helper -> Log.Debug (SHARPEMU_LOG_VMEM env gate preserved)

Build: 0 errors, 0 warnings

Co-authored-by: Hermes Atlas <hermesatlas@example.com>
2026-07-11 11:54:15 +03:00
kostyaff 57e737b5d7 [logging] Add FileLogSink, CompositeLogSink, and SHARPEMU_LOG_FILE env support (#50)
- FileLogSink: thread-safe file writer with AutoFlush, FileShare.Read for
  concurrent read access (tail -f), automatic parent directory creation,
  full date-time timestamps, IDisposable for graceful shutdown
- CompositeLogSink: fan-out to multiple sinks with per-sink exception
  isolation (one broken sink cannot silence the others), IDisposable
  propagates to children
- SharpEmuLog: ResolveSinkFromEnvironment() reads SHARPEMU_LOG_FILE and
  creates CompositeLogSink(console + file) when set; Sink setter now
  disposes the previous IDisposable sink to prevent file handle leaks;
  Shutdown() flushes and disposes the active sink
- Program.cs: Main wrapped in try/finally to guarantee SharpEmuLog.Shutdown()
  runs on all exit paths (GUI, mitigated child, normal, exception)

Co-authored-by: Hermes Atlas <hermesatlas@example.com>
2026-07-11 11:53:59 +03:00
ParantezTech a78b02f22b [dotnet] revert package for Microsoft.NET.ILLink.Tasks 2026-07-11 05:26:24 +03:00
ParantezTech 70ec2928ea [revert] Revert VulkanVideoPresenter.cs to previous version, added new screenshot, and updated packages.lock.json 2026-07-11 05:20:13 +03:00
Berk c618c116ba [shader-decoder] Fix address calculation for SW linear textures (#45) 2026-07-11 05:12:41 +03:00
Dawid 29021b5a71 [fixes] move repeating methods into CpuContext (#41) 2026-07-10 23:46:50 +03:00
kostyaff c0fd6a80e8 Astro Bot shader type 4, pthread_cond_timedwait, and HLE/memory/cpu bug fixes (#40)
* [agc] Add shader type 4 (GS) and register defaults v13 support

Astro Bot (#11) crashes on boot due to two missing GPU features:

1. Shader type 4 (Geometry Shader) — SPI_SHADER_PGM_LO/HI register
   offsets 0x8A/0x8B were missing. Added constants and switch cases
   for shader type 4 in GetExpectedSpiShaderPgmLo/Hi. Also added
   type 4 to IsEsGeometryShaderType (2 or 4 or 6).

2. Register defaults version 13 — was not recognized as supported.
   Added RegisterDefaultsVersion13 constant and included it in
   IsSupportedRegisterDefaultsVersion.

* [kernel] Add POSIX pthread_cond_timedwait export

SILENT HILL (#4) and Poppy Playtime (#3) crash on boot due to
missing POSIX pthread_cond_timedwait (NID 27bAgiJmOh0).

The Sony wrapper scePthreadCondTimedwait (NID BmMjYxmew1w) was
already implemented, but the raw POSIX symbol was not exported.
Added [SysAbiExport] for pthread_cond_timedwait delegating to
existing PthreadCondWaitCore with timed: true.

* [memory] Fix FlushInstructionCache null process handle

PhysicalVirtualMemory.cs called FlushInstructionCache with null as the
process handle in two places (SetProtection and TryWriteExclusive).
On Windows, a null handle does not reliably resolve to the current
process — the correct call is GetCurrentProcess() (pseudo-handle -1).

Also corrected the P/Invoke signature:
- Changed return type from void to bool with [return: MarshalAs(Bool)]
- Added SetLastError = true
- Added GetCurrentProcess() P/Invoke import

This matches the pattern already used in DirectExecutionBackend.cs
which correctly passes GetCurrentProcess() to all FlushInstructionCache
calls.

* [hle] Distinguish NOT_FOUND from NOT_IMPLEMENTED and log duplicate NIDs

Three diagnostic improvements to the HLE dispatch path:

1. ModuleManager.RegisterFromAssembly — duplicate NID registration was
   silently skipped (dispatchTable first-wins, exportTable last-wins,
   causing metadata divergence). Now logs a warning with the NID and
   export name so conflicts are visible.

2. ModuleManager.TryDispatch — generation mismatch returned
   ORBIS_GEN2_ERROR_NOT_FOUND, conflating 'function does not exist'
   with 'function exists but not for this generation'. Now returns
   ORBIS_GEN2_ERROR_NOT_IMPLEMENTED for generation mismatch, matching
   the existing convention in CpuDispatcher. Also adds debug logging
   for both NOT_FOUND and NOT_IMPLEMENTED paths.

3. DirectExecutionBackend.Imports.cs — the import dispatch else-branch
   (the actual hot path that bypasses ModuleManager.TyDispatch via
   cached export) had the same conflation. Split into:
   - else if (export exists but generation mismatch) → NOT_IMPLEMENTED
   - else (no export at all) → NOT_FOUND
   This makes runtime diagnostics correctly distinguish missing exports
   from generation-unsupported exports.

* [cpu] Check VirtualProtect return values in all stub creation paths

9 VirtualProtect calls in DirectExecutionBackend.cs had unchecked
return values. If VirtualProtect silently fails, memory protection
remains incorrect — stubs allocated with PAGE_EXECUTE_READWRITE (0x40)
never get downgraded to PAGE_EXECUTE_READ (0x20), or guest thread
entry stubs never get upgraded to writable. This causes access
violations on next execution or silent data corruption.

Fixed all 9 sites with proper error handling:
- 6 stub creation methods (return 0 on failure + log error)
- 2 guest thread entry methods (set reason + return Exception)
- 1 guest entry method (set LastError + return MEMORY_FAULT)

Stub creation sites fixed:
- CreateImportDispatchStub (line ~1683)
- EnsureTlsHandler (void, log + return)
- CreateUnresolvedReturnStub (return 0)
- CreateGuestReturnStub (return 0)
- CreateExceptionHandlerTrampoline (return 0)
- CreateTlsStoreHelperStub (return 0)

Guest thread entry sites fixed:
- StartGuestThreadNativeCall (return Exception)
- StartGuestContinuationNativeCall (return Exception)
- RunGuestEntryPoint (return MEMORY_FAULT)

* [kernel] Remove unused duplicate _nextFileDescriptor field

KernelExports.cs declared _nextFileDescriptor but never used it.
The actual field used for file descriptor allocation lives in
KernelMemoryCompatExports.cs (lines 1314, 1337). This was a dead
duplicate causing CS0414 warning.

Build is now 0 errors, 0 warnings.

---------

Co-authored-by: Hermes Atlas <hermesatlas@example.com>
2026-07-10 21:48:50 +03:00
Dawid 7337683c16 [fixes] stackalloc warnings, consolidate duplicated methods, minor adjustments in project settings (#39)
* [fixes] stackalloc warnings, consolidate duplicated methods

* [fix] remove unnecessary edit in .slnx file
2026-07-10 20:57:46 +03:00
ParantezTech b36ecc121c [readme] added GUI 2026-07-10 19:23:04 +03:00
Brando 42f0fa0e83 [gui] Add Avalonia desktop frontend (#35)
* [gui] Add Avalonia desktop frontend

Adds SharpEmu.GUI, a dark-themed desktop frontend that drives the
SharpEmu CLI as a child process:

- Game library with folder scanning for eboot.bin, search, and
  persisted settings (%APPDATA%/SharpEmu/gui-settings.json)
- Launch options mapped to CLI flags (log level, strict dynlib
  resolution, import trace limit)
- Live console with severity color-coding, bounded buffer, and
  crash-safe deferred auto-scroll
- EmulatorProcess launches the CLI via CreateProcessW with the same
  CET/CFG mitigation opt-outs the CLI applies to its own relaunched
  child (suppressed via SHARPEMU_DISABLE_MITIGATION_RELAUNCH so
  output is not lost to a detached console), inheritable pipes for
  stdout/stderr capture, a kill-on-close job object, and a fallback
  to an unmitigated launch on Windows builds that reject the policy
  bits

Also pins Tmds.DBus.Protocol 0.21.3 (transitive of Avalonia.Desktop)
to fix GHSA-xrw6-gwf8-vvr9.


* [gui] Integrate GUI into the SharpEmu executable

Per review feedback, the GUI is no longer a separate application.
SharpEmu.exe now opens the desktop frontend when started without
arguments and behaves exactly as the existing CLI when given any
argument:

- SharpEmu.GUI becomes a class library exposing GuiLauncher.Run(),
  hosted by SharpEmu.CLI
- The console window is hidden in GUI mode only when the process is
  its sole owner (double-click launch), never a terminal the user
  launched from
- In GUI mode the frontend spawns this same executable (with
  arguments) as the emulator child process, so the existing launch,
  piping, and mitigation machinery is unchanged


* [gui] Address review feedback: no console, single-file, param.json, portable settings

- Switch SharpEmu.exe to the GUI subsystem so no console window appears
  at startup. CLI mode attaches to the parent terminal console (or
  allocates one when started with arguments but no terminal) and
  rebinds missing std handles to CONOUT$; piped/redirected output is
  used as-is, so scripted and GUI-spawned runs are unaffected.
- Publish as a true single file: native libraries are embedded and
  self-extracted, with glfw kept as the only loose DLL next to the
  executable.
- The game library reads sce_sys/param.json and shows the game title
  with the title id beneath it, falling back to the folder name.
- GUI settings and the crash log now live next to the executable,
  matching the emulator convention, instead of %APPDATA%.


* [build] Add win-x64 sections to package lock files

Generated by dotnet publish -r win-x64 with locked restore enabled.


* [build] Regenerate lock files from project configuration

dotnet restore --force-evaluate; removes the win-x64 runtime sections
that a local RID-specific publish had written into projects that do
not declare a runtime identifier, which broke locked-mode restore in
CI. Verified with dotnet restore -p:RestoreLockedMode=true.


---------
2026-07-10 19:21:24 +03:00
Berk 41d61bde41 [memory] Host memory allocation issue fixed (#34) 2026-07-10 15:07:46 +03:00
Berk ac96fe5292 [sceAudio] added basic audio output support (#33) 2026-07-10 13:16:15 +03:00
Berk 9b9ca8f707 [loader] cut import overhead (#32) 2026-07-10 01:14:17 +03:00
Berk db20387502 [shader-decoder-part1-hotfix] New format support, maintenance8 support, robustness improvements, and bug fixes. This commit includes updates to the AGC exports, Gen5 SPIR-V translator, and Vulkan video presenter to enhance compatibility and performance. (#31) 2026-07-07 21:06:04 +03:00
Berk 44c43c8ebd [kernel] Added KernelExceptionCompatExports and fstat, pthread_equal (#30) 2026-07-07 21:05:54 +03:00
Foued Attar 4b5b27e686 [vulkan] enable required PhysicalDeviceFeatures for translated shaders (#29)
- Enable VertexPipelineStoresAndAtomics/FragmentStoresAndAtomics:
  fixes vkCreateGraphicsPipelines() rejecting guestBuffers storage
  descriptor as NonWritable in vertex/fragment stages.
- Enable ShaderInt64: fixes vkCreateShaderModule() rejecting SPIR-V
  using 64-bit integer capability.
- Query GetPhysicalDeviceFeatures first and only enable what the GPU
  actually reports as supported, with a warning fallback otherwise.

Also adds optional Vulkan Validation Layers (SHARPEMU_VK_VALIDATION=1)
to surface these VUID errors during development instead of silent
VK_ERROR_DEVICE_LOST.
2026-07-06 16:56:03 +03:00
ParantezTech 5c823d0e8b Merge remote-tracking branch 'refs/remotes/origin/main' 2026-07-06 09:53:04 +03:00
ParantezTech 1679897089 update readme 2026-07-06 09:52:46 +03:00
Berk ce8f75411a [unity-fixes] support for Media/Modules/*.prx files (#27) 2026-07-05 22:10:37 +03:00
Berk f75da92fa5 [agc] new agc exports for agc improvements (sceAgcDcbDrawIndex, sceAgcDriverGetResourceRegistrationMaxNameLength, sceAgcDriverGetDefaultOwner, sceAgcDriverRegisterResource, sceAgcDriverUnknown_KRzWekV120(?)) (#26) 2026-07-05 22:10:15 +03:00
Berk 9eaaac7cee [ngs2 implements] Implement NGS2 exports (#25) 2026-07-05 22:09:50 +03:00
Berk d446779d34 [pad improvements] Added support for scePadSetVibrationMode (#24) 2026-07-05 22:09:36 +03:00
Berk 585d11b93b [sceAudio] added simple audio output implementation, needs to be improved (#23) 2026-07-05 22:09:25 +03:00
Berk 17f9535f0e [sceMsgDialog] implemented sceMsgDialogInitialize (#22) 2026-07-05 22:09:14 +03:00
Berk 8bfbb9c7fe Avplayer implements (#21) 2026-07-05 22:09:04 +03:00
Berk 2b8fe71a82 [savedata-improvements-2] added sceSaveDataCreateTransactionResource (#20) 2026-07-05 22:08:53 +03:00
ParantezTech 29b7f086c9 reuse 2026-07-04 19:14:44 +03:00
ParantezTech eb3d8d6eb1 [dotnet] configure local nuget package cache 2026-07-04 19:12:18 +03:00
Berk 787c75b9b3 [fixes-assets] Add SharpEmu icon and fix glfw dependency (temporary fix) (#19)
* [fixes-assets] Add SharpEmu icon and fix glfw dependency (temporary fix)

* [reuse] I forgot to add comma

* [readme] update logo
2026-07-04 17:05:09 +03:00
Berk 2a784aa405 [gpu-hotfix1] Cache improvements have been made (6x performance gain) (#18) 2026-07-04 15:19:32 +03:00
Berk 2649857cb5 Savedata fix 1 (#17)
* [scePad] Just format code

* [sceSaveData] user folder sometimes appeared in the previous folder has been fixed.
2026-07-04 15:18:21 +03:00
ParantezTech c4b4aed2c5 [scePad] Just format code 2026-07-04 15:10:48 +03:00
Vlad Denisov 757f6ea4b5 [padExports]: add basic gamepad mappings (#16) 2026-07-04 14:40:43 +03:00
ParantezTech df3ee02f8d [readme] update screenshot image for dreaming sarah 2026-07-04 14:18:51 +03:00
ParantezTech dfe61ec643 [readme] update screenshot and add new image for dreaming sarah after shader decoder updates 2026-07-04 14:03:30 +03:00
Berk 52e17b5056 [shader-decoder-part1] Implemented a shader decoder (Part 1) (#12)
* [shader-decoder-part1] Implemented a shader decoder for Gen5 shaders, including IR generation, metadata reading, scalar evaluation, and SPIR-V translation. Updated related exports and video output components to support the new shader decoding functionality.

* [shader decoder] correct RDNA2 operands, fixing synchronization problems

* [shader-decoder] RDNA2 decoder improvements

* [shader-decoder] fix RDNA2 shift masking and sprite draws

* [shader-decoder] improve RDNA2 shader decoder to support more instructions and fix some issues with the previous implementation.
2026-07-04 13:51:08 +03:00
Berk c1f3abfd5d [ci] fix workflow dispatch and push triggers for main branch (#15) 2026-07-04 13:48:29 +03:00
Berk b5ee2c2cb7 [saveData] Add support for sceSaveDataMount3 (#14) 2026-07-03 13:25:38 +03:00
Berk 16c1b74636 [core] Update native execution and kernel exports, phtread improvement (#13) 2026-07-03 13:19:24 +03:00
ParantezTech 0d89b2488b [dotnet] remove shaderc package because it is no longer needed 2026-07-02 17:15:51 +03:00
ParantezTech c3019b78d6 [ampr] more improvements to the Ampr library, generally for performance 2026-07-01 13:50:52 +03:00
ParantezTech dd5e524879 [pthread] pthread improvements and fixes 2026-07-01 13:50:24 +03:00
ParantezTech df9d0e6aaf [NpEntitlementAccess] added sceNpEntitlementAccessGetAddcontEntitlementInfoList export 2026-07-01 13:49:49 +03:00
ParantezTech 253d0fae3f [saveData] minimal save data dialog exports 2026-07-01 13:49:20 +03:00
ParantezTech 78d719ef9e [core] more leaf for speedup 2026-07-01 13:48:45 +03:00
ParantezTech be3806cdf6 [packages] added Silk.NET.Shaderc for debugging GLSL 2026-07-01 13:45:13 +03:00
ParantezTech dc47deee93 [agc] new imports for shader translation 2026-06-29 18:29:05 +03:00
ParantezTech 1c838cf8d6 [core] more leaf imports 2026-06-29 18:13:21 +03:00
ParantezTech 2f269566a5 [readme] update Demon's Souls text 2026-06-29 18:10:46 +03:00
ParantezTech 9d7afef6f6 [readme] update Demon's Souls new boot info 2026-06-29 14:40:27 +03:00
ParantezTech daeff21516 [readme] update screenshots 2026-06-29 14:34:36 +03:00
ParantezTech 82619deb34 [ci] ignore image files from actions 2026-06-29 14:32:46 +03:00
ParantezTech 697ad7be80 [libs] Add NP entitlement validation 2026-06-29 14:31:37 +03:00
ParantezTech 73c987e49f [libs] Improve video output handling 2026-06-29 14:31:26 +03:00
ParantezTech cd79275117 [libs] Enhance PlayGo streaming service 2026-06-29 14:31:18 +03:00
ParantezTech b14ecae504 [core] Refine native CPU execution imports 2026-06-29 14:31:10 +03:00
ParantezTech 873f473b65 [video] hide splash 2026-06-29 13:32:17 +03:00
ParantezTech 9a1a3789ef [video] cap presenter ticks 2026-06-29 13:32:03 +03:00
ParantezTech 0f3d4032cb [core] add leaf imports 2026-06-29 13:30:50 +03:00
ParantezTech 0c4a757695 [core] log leaf import args 2026-06-29 13:30:32 +03:00
ParantezTech 5fcc8eaa37 [saveData] add dir search 2026-06-29 13:30:08 +03:00
ParantezTech 21105e0346 [ampr] batch buffer IO 2026-06-29 13:28:37 +03:00
ParantezTech b424df5a64 [kernel] speed up printf 2026-06-29 13:28:30 +03:00
ParantezTech 7f85971e39 [kernel] carry pthread scheduling 2026-06-29 13:27:59 +03:00
ParantezTech e28259a99d [core] speed up guest memory 2026-06-29 13:26:04 +03:00
ParantezTech 3d2a4b74ac [readme] add screenshot running Demon's Souls 2026-06-29 00:17:13 +03:00
ParantezTech af35389bfd [libs] Add audio and system gesture exports 2026-06-28 23:47:00 +03:00
ParantezTech 4ae778ef39 [libs] Update system and video exports 2026-06-28 23:46:56 +03:00
ParantezTech a04e70c0b3 [libs] Update NP and pad exports 2026-06-28 23:46:56 +03:00
ParantezTech 02222c7919 [libs] Update network exports 2026-06-28 23:46:56 +03:00
ParantezTech 46b3a207c1 [libs] Update AMPR and PlayGo exports 2026-06-28 23:46:55 +03:00
ParantezTech 418d46beb5 [libs] Update AGC and fiber exports 2026-06-28 23:46:44 +03:00
ParantezTech 0e922a73ee [core] Improve native execution and guest threading 2026-06-28 23:45:26 +03:00
ParantezTech a5172fd2c0 [kernel] Guest-thread blocking for pthread_mutex_lock is currently disabled 2026-06-28 23:45:05 +03:00
ParantezTech 4ab614e68a [npManager] initial Network Platform implement (just set game title etc.) 2026-06-23 19:30:27 +03:00
ParantezTech e581fe41f4 [kernel] pthread improvements 2026-06-23 19:29:29 +03:00
ParantezTech 0f0ec9a020 [appContent] correct way to get metadata from game 2026-06-23 19:28:53 +03:00
ParantezTech 70048cb49f [agc] fix shader invalid argument 2026-06-23 19:27:57 +03:00
ParantezTech 4777ec4544 [sceShare] initial share exports 2026-06-23 19:27:24 +03:00
ParantezTech caee2ad692 [readme] alignment 2026-06-23 17:54:39 +03:00
ParantezTech 7c83f7f925 [readme] logo align 2026-06-23 17:52:45 +03:00
ParantezTech 0e37bc95e9 upload logo 2026-06-23 17:51:28 +03:00
ParantezTech 318425630a [readme] edit for new milestones 2026-06-23 16:06:58 +03:00
ParantezTech 992ef68ba8 [readme] update 2026-06-23 16:05:16 +03:00
ParantezTech 01ebab90c0 [playgo] In some games, chunk scenarios assertion rewrite 2026-06-23 15:50:43 +03:00
ParantezTech d134f9b9f6 [fiber] synchronization problems have been fixed for such a titles: Demon's Souls
[ampr] new exports
[memory] trampoline fixes
2026-06-23 15:48:45 +03:00
ParantezTech dad85b87c3 [video] Add game title to window 2026-06-21 23:19:16 +03:00
ParantezTech 464422f23e [video] Add Vulkan presentation 2026-06-21 23:19:05 +03:00
ParantezTech 5afa5e1ffa [hle] Add missing service exports 2026-06-21 23:18:55 +03:00
ParantezTech 5512da9e12 [kernel] Add thread and event primitives 2026-06-21 23:18:42 +03:00
ParantezTech 2a52ccfe6a [cpu] Add guest thread continuation 2026-06-21 23:18:29 +03:00
ParantezTech 08b315b5fc [kernel] Add guest memory support 2026-06-21 23:18:18 +03:00
ParantezTech 5d204afd10 [videoOut] add minimal Vulkan swapchain presenter 2026-06-14 12:12:09 +03:00
ParantezTech 7bd1e8dc29 change jpg to sharpemu videoOut test window 2026-06-07 16:15:43 +03:00
ParantezTech fad61e116e reuse 2026-06-07 15:56:07 +03:00
ParantezTech bc297fa175 [readme] update jpg 2026-06-07 15:48:11 +03:00
ParantezTech 79e767ed9f [readme] added rendering texture from Dreaming Sarah (this is just a presenter draw, not GPU swapchain) 2026-06-07 15:45:33 +03:00
ParantezTech 1ea1396979 [kernel/videoOut] extend memory managements and videoOut (this is not a swapchain) 2026-06-07 15:43:26 +03:00
ParantezTech a1dea499bc update readme 2026-05-10 20:37:49 +03:00
ParantezTech fee6af3136 update readme for new titles 2026-05-10 20:02:58 +03:00
ParantezTech bbf5ff7be8 more HLE's and fix cpu execution some titles 2026-05-10 19:51:57 +03:00
ParantezTech 0c859f04ad Added more HLE's 2026-04-27 12:40:50 +03:00
ParantezTech 994438963d [ci] temporary fix 2026-04-13 15:59:19 +03:00
ParantezTech 233af123af More new HLE's and fixes 2026-04-13 15:54:38 +03:00
ParantezTech 02eb9b30e9 correct patch 2026-03-28 18:40:54 +03:00
ParantezTech fa46819030 fix for actions (again) 2026-03-28 18:23:46 +03:00
ParantezTech 7a915e88dc fix dotnet version on Github Actions 2026-03-28 18:15:14 +03:00
ParantezTech 812879aa81 PlayGo, VideoOut minimum HLE implements and fix some direct runner 2026-03-28 18:02:37 +03:00
ParantezTech 71ba5cf1db More HLE implements: wcs* 2026-03-19 23:57:48 +03:00
ParantezTech 8f108ca01d Upgrade GitHub Actions to Node 24 2026-03-19 13:33:19 +03:00
155 changed files with 57909 additions and 1456 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ indent_size = 4
indent_style = space
tab_width = 4
[*.{md,json,yml,props,csproj}]
[*.{md,json,yml,xml,props,csproj}]
indent_size = 2
tab_width = 2
+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.

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

+8 -2
View File
@@ -9,11 +9,17 @@ on:
- "**"
paths-ignore:
- "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
pull_request:
branches:
- main
paths-ignore:
- "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
workflow_dispatch:
permissions:
@@ -109,7 +115,7 @@ jobs:
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
- name: Upload build artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: ${{ needs.init.outputs.artifact-name }}
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
@@ -120,7 +126,7 @@ jobs:
needs:
- init
- build
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
permissions:
contents: write
+6
View File
@@ -26,11 +26,17 @@ arm64/
*.userosscache
*.sln.docstates
.packages
packages/
*.nupkg
.nuget/
.dotnet-home/
.cache/
.DS_Store
Thumbs.db
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.
+14
View File
@@ -32,4 +32,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
top of the log. Populated from GitHub Actions environment variables when
building in CI; empty locally. -->
<ItemGroup>
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
</ItemGroup>
</Project>
+12 -3
View File
@@ -1,13 +1,22 @@
<!--
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.18" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
<PackageVersion Include="Iced" Version="1.21.0" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
</ItemGroup>
</Project>
</Project>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Alex Barney
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
BIN
View File
Binary file not shown.
+10 -1
View File
@@ -3,12 +3,21 @@ version = 1
[[annotations]]
path = [
"REUSE.toml",
"nuget.config",
"global.json",
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
"_logs/**"
"_logs/**",
".github/images/**",
"assets/images/**"
]
precedence = "aggregate"
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
SPDX-License-Identifier = "GPL-2.0-or-later"
[[annotations]]
path = "src/SharpEmu.GUI/Atrac9/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2018 Alex Barney"
SPDX-License-Identifier = "MIT"
+1
View File
@@ -7,6 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Folder Name="/src/">
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestFeature"
"rollForward": "disable"
}
}
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<config>
<add key="globalPackagesFolder" value=".packages" />
</config>
<packageSources>
<clear />
<add key="Nuget" value="https://api.nuget.org/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="Nuget">
<package pattern="*" />
</packageSource>
</packageSourceMapping>
</configuration>
+532 -15
View File
@@ -3,16 +3,20 @@
using SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu;
using SharpEmu.GUI;
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;
@@ -20,32 +24,81 @@ 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;
private const uint GENERIC_WRITE = 0x40000000;
private const uint FILE_SHARE_READ = 0x00000001;
private const uint FILE_SHARE_WRITE = 0x00000002;
private const uint OPEN_EXISTING = 3;
[STAThread]
private static int Main(string[] args)
{
try
{
return Run(args);
}
finally
{
DropConsoleFileMirror();
SharpEmuLog.Shutdown();
}
}
private static int Run(string[] args)
{
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (args.Length == 0 && !isMitigatedChild)
{
// No arguments: open the desktop frontend. Any argument selects
// the classic CLI behavior below.
return GuiLauncher.Run();
}
// The executable uses the GUI subsystem, so CLI mode has to connect
// 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");
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
{
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);
ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
if (!File.Exists(ebootPath))
{
Log.Error($"EBOOT file was not found: {ebootPath}");
@@ -101,9 +154,107 @@ internal static partial class Program
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
}
private static void EnsureCliConsole()
{
if (!OperatingSystem.IsWindows())
{
return;
}
// Standard handles already provided (pipes or file redirection, e.g.
// when the GUI or a script launches us): use them as-is.
if (IsHandleValid(GetStdHandle(STD_OUTPUT_HANDLE)) && IsHandleValid(GetStdHandle(STD_ERROR_HANDLE)))
{
return;
}
// Prefer the console of the parent process (interactive terminal);
// create one only when started with arguments but no terminal at all
// (e.g. a shortcut), so usage and errors remain visible.
if (!AttachConsole(ATTACH_PARENT_PROCESS) && GetConsoleWindow() == 0)
{
_ = AllocConsole();
}
RebindStdHandleToConsole(STD_OUTPUT_HANDLE);
RebindStdHandleToConsole(STD_ERROR_HANDLE);
}
/// <summary>
/// Makes console writes UTF-8 so the GUI's pipe reader (and any modern
/// terminal) decodes non-ASCII text correctly. Without this, redirected
/// output falls back to the OS ANSI code page and characters like "—"
/// arrive mangled.
/// </summary>
private static void UseUtf8ConsoleOutput()
{
try
{
// Also recreates the redirected Console.Out/Error writers with
// the new encoding.
Console.OutputEncoding = Encoding.UTF8;
return;
}
catch (Exception)
{
// No attached console (GUI-subsystem child with piped output):
// wrap the raw handles instead.
}
if (Console.IsOutputRedirected)
{
Console.SetOut(new StreamWriter(
Console.OpenStandardOutput(),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = true,
});
}
if (Console.IsErrorRedirected)
{
Console.SetError(new StreamWriter(
Console.OpenStandardError(),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
{
AutoFlush = true,
});
}
}
private static void RebindStdHandleToConsole(int stdHandle)
{
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
{
return;
}
var conOut = CreateFileW(
"CONOUT$",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
0,
OPEN_EXISTING,
0,
0);
if (IsHandleValid(conOut))
{
_ = SetStdHandle(stdHandle, conOut);
}
}
private static bool IsHandleValid(nint handle)
{
return handle != 0 && handle != -1;
}
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;
@@ -114,7 +265,7 @@ internal static partial class Program
{
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
{
isMitigatedChild = true;
isMitigatedChild = trustedMitigatedChild;
continue;
}
@@ -153,9 +304,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;
@@ -163,7 +316,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;
@@ -171,8 +326,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));
@@ -187,24 +341,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
@@ -258,6 +419,8 @@ internal static partial class Program
}
finally
{
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (attributeList != 0)
{
DeleteProcThreadAttributeList(attributeList);
@@ -271,6 +434,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();
@@ -373,27 +768,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++)
@@ -423,6 +821,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -436,6 +835,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -443,6 +843,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))
{
@@ -466,6 +883,7 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -488,11 +906,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;
}
@@ -504,6 +938,7 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -605,6 +1040,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(
@@ -670,4 +1155,36 @@ internal static partial class Program
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(nint handle);
[DllImport("kernel32.dll")]
private static extern nint GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AttachConsole(int processId);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AllocConsole();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint GetStdHandle(int stdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
[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,
uint desiredAccess,
uint shareMode,
nint securityAttributes,
uint creationDisposition,
uint flagsAndAttributes,
nint templateFile);
}
+26 -1
View File
@@ -7,11 +7,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<!-- GUI subsystem so starting without arguments opens the frontend with no
console window; CLI mode re-attaches to the parent terminal's console. -->
<OutputType>WinExe</OutputType>
<AssemblyName>SharpEmu</AssemblyName>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
<SelfContained>true</SelfContained>
@@ -30,6 +33,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\LICENSE.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
@@ -38,4 +50,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Content>
</ItemGroup>
<!-- Keep glfw as a loose file next to the executable; every other native
library is embedded into the single-file bundle. -->
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
<ItemGroup>
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw'))" />
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
</Project>
+453 -5
View File
@@ -8,6 +8,193 @@
"resolved": "10.0.3",
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "11.3.2",
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
},
"Avalonia.FreeDesktop": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
"dependencies": {
"Avalonia": "11.3.18",
"Tmds.DBus.Protocol": "0.21.3"
}
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
},
"Avalonia.Skia": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
"dependencies": {
"Avalonia": "11.3.18",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
}
},
"Avalonia.Win32": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
}
},
"Avalonia.X11": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.FreeDesktop": "11.3.18",
"Avalonia.Skia": "11.3.18"
}
},
"HarfBuzzSharp": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.core": {
"type": "Project",
"dependencies": {
@@ -17,27 +204,288 @@
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.gui": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.3.18, )",
"Avalonia.Desktop": "[11.3.18, )",
"Avalonia.Fonts.Inter": "[11.3.18, )",
"Avalonia.Themes.Fluent": "[11.3.18, )",
"SharpEmu.Logging": "[1.0.0, )",
"Tmds.DBus.Protocol": "[0.21.3, )"
}
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )"
"SharpEmu.HLE": "[1.0.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
"Silk.NET.Windowing": "[2.23.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Avalonia": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"Silk.NET.Core": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.EXT": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.KHR": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Tmds.DBus.Protocol": {
"type": "CentralTransitive",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
}
},
"net10.0/linux-x64": {},
"net10.0/osx-arm64": {},
"net10.0/win-x64": {}
"net10.0/linux-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-arm64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/win-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
}
}
}
+50 -23
View File
@@ -7,11 +7,14 @@ using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu;
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
private enum EntryFrameKind
{
ProcessEntry,
@@ -22,6 +25,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
private const ulong StackSize = 0x0020_0000UL;
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
private const ulong TlsSize = 0x0001_0000UL;
private const ulong TlsPrefixSize = 0x0000_1000UL;
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
@@ -79,8 +83,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string processImageName = "eboot.bin",
CpuExecutionOptions executionOptions = default)
{
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
Log.Debug("=== DispatchEntry START ===");
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
try
{
@@ -88,8 +92,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
throw;
}
}
@@ -102,8 +105,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string moduleName = "module",
CpuExecutionOptions executionOptions = default)
{
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
Log.Debug("=== DispatchModuleInitializer START ===");
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
try
{
@@ -118,8 +121,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
throw;
}
}
@@ -133,7 +135,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
CpuExecutionOptions executionOptions = default,
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
{
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
Log.Debug("DispatchEntryCore STARTING...");
LastEntryPoint = entryPoint;
LastTrapInfo = null;
@@ -193,6 +195,11 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!InitializeGuestFrameChainSentinel(context))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!InitializeTls(context, tlsBase))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@@ -300,7 +307,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
LastMilestoneLog,
Environment.NewLine,
$"CpuEngine native-only failed: {backendError}");
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
Log.Error($"Native backend FAILED: {backendError}");
return FailEarly(
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
CpuExitReason.NativeBackendUnavailable);
@@ -337,11 +344,12 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
for (var i = 0; i < 32; i++)
{
var candidateBase = TlsBaseAddress - ((ulong)i * tlsStride);
var mappedBase = candidateBase - TlsPrefixSize;
try
{
_virtualMemory.Map(
candidateBase,
TlsSize,
mappedBase,
TlsSize + TlsPrefixSize,
fileOffset: 0,
fileData: ReadOnlySpan<byte>.Empty,
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
@@ -358,9 +366,28 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
private static bool InitializeTls(CpuContext context, ulong tlsBase)
{
return context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
return context.TryWriteUInt64(tlsBase - 0xF0, 0) &&
context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL);
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL) &&
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
}
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
{
var stackTop = context[CpuRegister.Rsp] + sizeof(ulong);
var sentinelFrame = AlignDown(stackTop - 0x20, 16);
var seedRsp = sentinelFrame - sizeof(ulong);
if (!context.TryWriteUInt64(sentinelFrame, 0) ||
!context.TryWriteUInt64(sentinelFrame + sizeof(ulong), 0) ||
!context.TryWriteUInt64(seedRsp, 0))
{
return false;
}
context[CpuRegister.Rbp] = sentinelFrame;
context[CpuRegister.Rsp] = seedRsp;
return true;
}
private static bool InitializeProcessEntryFrame(
@@ -389,8 +416,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
const ulong entryParamsSize = 0x20;
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
@@ -398,6 +425,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return false;
}
var entryStackPointer = entryParamsAddress - sizeof(ulong);
if (!context.TryWriteUInt64(entryStackPointer, 0))
{
return false;
}
context[CpuRegister.Rsp] = entryStackPointer;
context[CpuRegister.Rdi] = entryParamsAddress;
context[CpuRegister.Rsi] = programExitHandlerAddress;
context[CpuRegister.Rdx] = 0;
@@ -423,13 +457,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return value & ~(alignment - 1);
}
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return context.Memory.TryWrite(address, buffer);
}
private static string BuildEntryFrameDiagnostic(
ulong entryPoint,
CpuContext context,
@@ -2,20 +2,35 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
private void RecordRecentImportTrace(string traceLine)
private void RecordRecentImportTrace(
long dispatchIndex,
string nid,
ulong returnRip,
ulong arg0,
ulong arg1,
ulong arg2)
{
_recentImportTrace[_recentImportTraceWriteIndex] = traceLine;
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
dispatchIndex,
nid,
returnRip,
arg0,
arg1,
arg2);
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
if (_recentImportTraceCount < _recentImportTrace.Length)
{
@@ -23,21 +38,88 @@ 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++)
{
int num2 = (num + i) % _recentImportTrace.Length;
string text = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(text))
var entry = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(entry.Nid))
{
Console.Error.WriteLine("[LOADER][INFO] " + text);
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}");
}
}
}
@@ -75,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;
}
}
@@ -90,41 +172,42 @@ 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;
}
private void ProbeReturnRip(ulong returnRip, long dispatchIndex)
{
if (_cpuContext == null || returnRip == 0)
var cpuContext = ActiveCpuContext;
if (cpuContext == null || returnRip == 0)
{
return;
}
Span<byte> destination = stackalloc byte[128];
if (!_cpuContext.Memory.TryRead(returnRip, destination))
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))
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)
{
int num3 = BitConverter.ToInt32(destination.Slice(3, 4));
ulong num4 = returnRip + 7 + (ulong)num3;
if (_cpuContext.TryReadUInt64(num4, out var value3))
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++)
@@ -134,9 +217,80 @@ public sealed partial class DirectExecutionBackend
int num5 = BitConverter.ToInt32(destination.Slice(i + 2, 4));
ulong num6 = returnRip + (ulong)i;
ulong num7 = num6 + 6 + (ulong)num5;
if (_cpuContext.TryReadUInt64(num7, out var value4))
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}");
}
}
}
Span<byte> targetBytes = stackalloc byte[32];
for (int i = 0; i + 5 <= destination.Length; i++)
{
if (destination[i] != 0xE8)
{
continue;
}
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
ulong callRip = returnRip + (ulong)i;
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
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)
{
continue;
}
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Log.Debug(
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Log.Debug(
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
}
break;
}
if (cpuContext.Memory.TryRead(target, targetBytes))
{
Log.Debug(
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
{
int slotRel32 = BitConverter.ToInt32(targetBytes.Slice(2, 4));
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
{
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)
{
continue;
}
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Log.Debug(
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Log.Debug(
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
}
break;
}
}
}
}
}
@@ -231,11 +385,24 @@ public sealed partial class DirectExecutionBackend
private unsafe static bool IsExecutableAddress(ulong address)
{
var pageAddress = address & ~0xFFFUL;
if (_knownExecutablePages.ContainsKey(pageAddress))
{
return true;
}
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
return lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
if (executable)
{
_knownExecutablePages.TryAdd(pageAddress, 0);
}
return executable;
}
private static ulong AlignUp(ulong value, ulong alignment)
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.HLE;
@@ -14,11 +15,19 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
private static int _lazyCommitTraceCount;
private unsafe void SetupExceptionHandler()
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
{
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, RawVectoredHandlerPtrManaged);
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
if (_rawExceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
}
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
}
else
@@ -28,12 +37,22 @@ public sealed partial class DirectExecutionBackend
_handlerDelegate = VectoredHandler;
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
if (_exceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create exception handler trampoline");
}
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
SetUnhandledExceptionFilter(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
if (_unhandledFilterStub == 0)
{
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
}
SetUnhandledExceptionFilter(_unhandledFilterStub);
}
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
@@ -61,7 +80,7 @@ public sealed partial class DirectExecutionBackend
{
if (_vectoredHandlerDepth > 0)
{
Console.Error.WriteLine("[LOADER][TRACE] Nested VEH exception; passing through.");
LogNestedVectoredException(exceptionInfo);
Console.Error.Flush();
return 0;
}
@@ -84,10 +103,27 @@ public sealed partial class DirectExecutionBackend
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord))
// 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;
}
if (IsBenignHostDebugException(exceptionCode))
{
return -1;
}
if (exceptionCode == MSVC_CPP_EXCEPTION)
{
return 0;
}
switch (exceptionCode)
{
@@ -278,6 +314,37 @@ public sealed partial class DirectExecutionBackend
}
}
private static bool IsBenignHostDebugException(uint exceptionCode)
{
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
}
private unsafe static void LogNestedVectoredException(void* exceptionInfo)
{
int count = Interlocked.Increment(ref _nestedVehTraceCount);
if (count > 16 && count % 128 != 0)
{
return;
}
try
{
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
void* contextRecord = pointers->ContextRecord;
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
Console.Error.WriteLine(
$"[LOADER][TRACE] Nested VEH exception#{count}: code=0x{record->ExceptionCode:X8} ex=0x{(ulong)record->ExceptionAddress:X16} rip=0x{rip:X16} rsp=0x{rsp:X16} type={accessType} target=0x{target:X16}; passing through.");
}
catch
{
Console.Error.WriteLine($"[LOADER][TRACE] Nested VEH exception#{count}; passing through.");
}
}
private unsafe void LogAccessViolationTrace(ulong exceptionAddress, EXCEPTION_RECORD* exceptionRecord)
{
ulong accessType = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
@@ -828,7 +895,7 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe static bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord)
private unsafe bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord, ulong rip, ulong rsp)
{
if (exceptionRecord->NumberParameters < 2)
{
@@ -845,6 +912,10 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (!IsGuestOwnedLazyCommitAddress(faultAddress, out var owner))
{
return false;
}
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
@@ -852,7 +923,21 @@ public sealed partial class DirectExecutionBackend
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query: fault=0x{faultAddress:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
}
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
{
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
}
return true;
}
bool committed = false;
ulong committedBase = 0;
@@ -860,14 +945,25 @@ public sealed partial class DirectExecutionBackend
if (mbi.State == 65536)
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
committedBase = windowBase;
committedSize = windowSize;
}
else
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
}
}
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
@@ -890,7 +986,10 @@ public sealed partial class DirectExecutionBackend
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
}
return true;
}
@@ -899,14 +998,25 @@ public sealed partial class DirectExecutionBackend
return false;
}
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
committedBase = commitWindowBase;
committedSize = commitWindowSize;
}
else
{
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
}
}
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
@@ -935,9 +1045,46 @@ public sealed partial class DirectExecutionBackend
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
}
return true;
static bool TryGetLazyCommitWindow(ulong fault, ulong regionBase, ulong regionSize, out ulong baseAddress, out ulong length)
{
baseAddress = 0;
length = 0;
if (regionSize == 0 || ulong.MaxValue - regionBase < regionSize)
{
return false;
}
ulong regionEnd = regionBase + regionSize;
ulong windowBase = fault & ~(LazyCommitWindowBytes - 1);
if (windowBase < regionBase)
{
windowBase = regionBase;
}
if (windowBase >= regionEnd)
{
return false;
}
ulong windowEnd = Math.Min(regionEnd, windowBase + LazyCommitWindowBytes);
ulong windowSize = windowEnd - windowBase;
windowSize &= 0xFFFFFFFFFFFFF000uL;
if (windowSize == 0)
{
return false;
}
baseAddress = windowBase;
length = windowSize;
return true;
}
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
{
if (length == 0)
@@ -964,6 +1111,49 @@ public sealed partial class DirectExecutionBackend
}
return TryCommitRange(commitAddress, commitSize, protection);
}
static bool IsAccessCompatible(ulong accessType, uint protection)
{
const uint pageNoAccess = 0x01;
const uint pageReadOnly = 0x02;
const uint pageReadWrite = 0x04;
const uint pageWriteCopy = 0x08;
const uint pageExecute = 0x10;
const uint pageExecuteRead = 0x20;
const uint pageExecuteReadWrite = 0x40;
const uint pageExecuteWriteCopy = 0x80;
const uint pageGuard = 0x100;
const uint accessMask = 0xFF;
if ((protection & pageGuard) != 0)
{
return false;
}
uint access = protection & accessMask;
if (access == pageNoAccess)
{
return false;
}
return accessType switch
{
0 => access is pageReadOnly or pageReadWrite or pageWriteCopy or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
1 => access is pageReadWrite or pageWriteCopy or pageExecuteReadWrite or pageExecuteWriteCopy,
8 => access is pageExecute or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
_ => false
};
}
}
private static bool ShouldTraceLazyCommit(int traceIndex)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
{
return true;
}
return traceIndex <= 16 || traceIndex % 256 == 0;
}
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
File diff suppressed because it is too large Load Diff
@@ -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
+12 -1
View File
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
{
private readonly ICpuMemory _inner;
@@ -39,4 +39,15 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
return result;
}
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
if (_inner is IGuestMemoryAllocator allocator)
{
return allocator.TryAllocateGuestMemory(size, alignment, out address);
}
address = 0;
return false;
}
}
+13 -1
View File
@@ -21,7 +21,10 @@ public sealed class SelfImage
IReadOnlyList<ulong>? initializerFunctions = null,
ulong initFunctionEntryPoint = 0,
ulong imageBase = 0,
ulong procParamAddress = 0)
ulong procParamAddress = 0,
string? title = null,
string? titleId = null,
string? version = null)
{
ArgumentNullException.ThrowIfNull(programHeaders);
ArgumentNullException.ThrowIfNull(mappedRegions);
@@ -38,6 +41,9 @@ public sealed class SelfImage
InitFunctionEntryPoint = initFunctionEntryPoint;
_imageBase = imageBase;
ProcParamAddress = procParamAddress;
Title = title;
TitleId = titleId;
Version = version;
}
public bool IsSelf { get; }
@@ -63,4 +69,10 @@ public sealed class SelfImage
public ulong EntryPoint => ElfHeader.EntryPoint + _imageBase;
public ulong ProcParamAddress { get; }
public string? Title { get; }
public string? TitleId { get; }
public string? Version { get; }
}
+59 -51
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;
@@ -144,10 +146,9 @@ public sealed class SelfLoader : ISelfLoader
throw new InvalidDataException("Input image is empty.");
}
if (readParamJson)
{
TryLoadParamJson(fs, mountRoot);
}
var applicationInfo = readParamJson
? TryLoadParamJson(fs, mountRoot)
: default;
if (clearVirtualMemory)
{
@@ -156,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);
@@ -256,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,
@@ -270,15 +271,20 @@ public sealed class SelfLoader : ISelfLoader
initializerFunctions,
initFunctionEntryPoint,
imageBase,
procParamAddress);
procParamAddress,
applicationInfo.Title,
applicationInfo.TitleId,
applicationInfo.Version);
}
private static void TryLoadParamJson(IFileSystem? fs, string? mountRoot)
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
IFileSystem? fs,
string? mountRoot)
{
if (fs == null)
{
Console.WriteLine("[LOADER] param.json not found (no filesystem provided).");
return;
return default;
}
string[] possiblePaths = string.IsNullOrEmpty(mountRoot)
@@ -298,12 +304,16 @@ public sealed class SelfLoader : ISelfLoader
if (foundPath == null)
{
Console.WriteLine("[LOADER] param.json not found (no root path / unknown layout).");
return;
return default;
}
var (title, titleId, ver) = Ps5ParamJsonReader.TryReadPs5Param(fs, foundPath);
var applicationInfo = Ps5ParamJsonReader.TryReadPs5Param(fs, foundPath);
Console.WriteLine($"[LOADER] Loading param.json at {foundPath}");
Console.WriteLine($"[LOADER] Title: {title ?? "(unknown)"}, TitleId: {titleId ?? "(unknown)"}, Version: {ver ?? "(unknown)"}");
Console.WriteLine(
$"[LOADER] Title: {applicationInfo.Title ?? "(unknown)"}, " +
$"TitleId: {applicationInfo.TitleId ?? "(unknown)"}, " +
$"Version: {applicationInfo.Version ?? "(unknown)"}");
return applicationInfo;
}
private static LoadContext ParseLayout(ReadOnlySpan<byte> imageData)
@@ -395,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}");
}
@@ -663,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}");
}
}
@@ -681,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>")}");
}
}
@@ -775,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;
}
@@ -792,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;
}
@@ -832,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;
}
@@ -844,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(
@@ -865,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(
@@ -885,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;
}
@@ -894,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;
}
@@ -942,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}");
}
}
@@ -1017,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}");
}
}
@@ -1401,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.");
@@ -1834,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;
}
@@ -1851,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;
}
@@ -1912,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;
}
@@ -1931,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;
}
+558 -96
View File
@@ -4,18 +4,28 @@
using System.Runtime.InteropServices;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
{
private readonly object _gate = new();
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
private readonly object _guestAllocationGate = new();
private readonly object _allocationSearchHintGate = new();
private readonly List<MemoryRegion> _regions = new();
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
private bool _disposed;
private const ulong PageSize = 0x1000;
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
private const ulong GuestAllocationArenaSize = 0x0100_0000;
private const ulong GuestAllocationArenaStartOffset = PageSize;
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
private const ulong FullCommitRegionLimit = 4UL << 30;
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
private const uint MEM_COMMIT = 0x1000;
@@ -29,6 +39,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_READONLY = 0x02;
private ulong _guestAllocationArenaBase;
private ulong _guestAllocationOffset;
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
@@ -44,7 +58,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[DllImport("kernel32.dll")]
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
private static extern void* GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
{
@@ -71,9 +89,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return false;
}
lock (_gate)
_gate.EnterWriteLock();
try
{
_regions.Add(new MemoryRegion
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -82,9 +101,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
Protection = protection
});
}
finally
{
_gate.ExitWriteLock();
}
var allocationKind = executable ? "executable memory" : "data memory";
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
return true;
}
@@ -98,7 +121,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var allocationType = MEM_COMMIT | MEM_RESERVE;
var reservedOnly = false;
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
var preferReserveOnly = !executable &&
alignedSize >= LargeDataReserveThreshold &&
alignedSize > FullCommitRegionLimit;
void* result = null;
if (preferReserveOnly)
@@ -127,7 +152,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
}
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
if (result == null)
@@ -181,12 +206,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
lazyPrimeState = committedBytes == primeBytes
? $"ok:{committedBytes:X}"
: $"partial:{committedBytes:X}/{primeBytes:X}";
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
}
else
{
lazyPrimeState = $"fail:{primeBytes:X}";
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
}
}
else
@@ -195,9 +220,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
}
}
lock (_gate)
_gate.EnterWriteLock();
try
{
_regions.Add(new MemoryRegion
InsertRegionSorted(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -206,25 +232,135 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
Protection = protection
});
}
finally
{
_gate.ExitWriteLock();
}
var allocationKind = reservedOnly
? "reserved data memory (lazy commit)"
: (executable ? "executable memory" : "data memory");
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
TraceVmem($"Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
return actualAddress;
}
public bool TryAllocateAtOrAbove(
ulong desiredAddress,
ulong size,
bool executable,
ulong alignment,
out ulong actualAddress)
{
actualAddress = 0;
if (size == 0)
{
return false;
}
var alignedSize = AlignUp(size, PageSize);
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
for (var attempt = 0; attempt < 0x10000; attempt++)
{
if (cursor == 0 || ulong.MaxValue - cursor < alignedSize)
{
return false;
}
if (TryGetOverlappingRegionEnd(cursor, alignedSize, out var overlapEnd))
{
cursor = AlignUp(overlapEnd, effectiveAlignment);
continue;
}
try
{
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
if (actualAddress == cursor)
{
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
return true;
}
actualAddress = 0;
}
catch
{
}
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
}
return false;
}
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
address = 0;
if (size == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0)
{
return false;
}
lock (_guestAllocationGate)
{
if (_guestAllocationArenaBase == 0)
{
try
{
_guestAllocationArenaBase = AllocateAt(
GuestAllocationArenaAddress,
GuestAllocationArenaSize,
executable: false,
allowAlternative: true);
_guestAllocationOffset = GuestAllocationArenaStartOffset;
}
catch (Exception)
{
return false;
}
}
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
{
return false;
}
address = _guestAllocationArenaBase + alignedOffset;
_guestAllocationOffset = alignedOffset + size;
return true;
}
}
public void Clear()
{
lock (_gate)
lock (_guestAllocationGate)
{
foreach (var region in _regions)
_gate.EnterWriteLock();
try
{
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
foreach (var region in _regions)
{
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
}
_regions.Clear();
_pageProtections.Clear();
lock (_allocationSearchHintGate)
{
_allocationSearchHints.Clear();
}
}
_regions.Clear();
_pageProtections.Clear();
finally
{
_gate.ExitWriteLock();
}
_guestAllocationArenaBase = 0;
_guestAllocationOffset = 0;
}
}
@@ -241,7 +377,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
var mapEnd = AlignUp(segmentEnd, PageSize);
var mapSize = checked(mapEnd - mapStart);
lock (_gate)
_gate.EnterWriteLock();
try
{
var existingRegion = FindRegion(mapStart, mapSize);
if (existingRegion == null)
@@ -272,7 +409,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
ApplySegmentProtection(mapStart, mapEnd, protection);
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
TraceVmem($"Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
}
finally
{
_gate.ExitWriteLock();
}
}
@@ -317,13 +458,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
if ((flags & ProgramHeaderFlags.Execute) != 0)
{
FlushInstructionCache(null, (void*)address, (nuint)size);
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
}
}
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
{
lock (_gate)
_gate.EnterReadLock();
try
{
var snapshot = new VirtualMemoryRegion[_regions.Count];
for (var i = 0; i < _regions.Count; i++)
@@ -338,96 +480,240 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
}
return snapshot;
}
finally
{
_gate.ExitReadLock();
}
}
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
lock (_gate)
var requiresExclusiveAccess = false;
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
var region = FindRegion(virtualAddress, (ulong)destination.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)destination.Length,
region,
out var offset))
{
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
var srcPtr = (void*)(region.VirtualAddress + offset);
if (destination.IsEmpty)
{
var srcPtr = (void*)(region.VirtualAddress + offset);
if (destination.IsEmpty)
{
return true;
}
return true;
}
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
}
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
requiresExclusiveAccess = true;
}
else
{
fixed (byte* destPtr = destination)
{
return false;
}
try
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
}
finally
{
RestorePageProtections(touchedPages);
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
return true;
}
}
}
finally
{
_gate.ExitReadLock();
}
if (!requiresExclusiveAccess)
{
return false;
}
_gate.EnterWriteLock();
try
{
return TryReadExclusive(virtualAddress, destination);
}
finally
{
_gate.ExitWriteLock();
}
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
lock (_gate)
var requiresExclusiveAccess = false;
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
var region = FindRegion(virtualAddress, (ulong)source.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)source.Length,
region,
out var offset))
{
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
var destPtr = (void*)(region.VirtualAddress + offset);
if (source.IsEmpty)
{
var destPtr = (void*)(region.VirtualAddress + offset);
if (source.IsEmpty)
{
return true;
}
return true;
}
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
requiresExclusiveAccess = true;
}
else
{
fixed (byte* srcPtr = source)
{
return false;
}
try
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
}
finally
{
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(null, destPtr, (nuint)source.Length);
}
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
return true;
}
}
}
finally
{
_gate.ExitReadLock();
}
if (!requiresExclusiveAccess)
{
return false;
}
_gate.EnterWriteLock();
try
{
return TryWriteExclusive(virtualAddress, source);
}
finally
{
_gate.ExitWriteLock();
}
}
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
{
var region = FindRegion(virtualAddress, (ulong)destination.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)destination.Length,
region,
out var offset))
{
var srcPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
return true;
}
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
{
return false;
}
try
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
}
finally
{
RestorePageProtections(touchedPages);
}
return true;
}
return false;
}
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
{
var region = FindRegion(virtualAddress, (ulong)source.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)source.Length,
region,
out var offset))
{
var destPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
return true;
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
}
try
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
}
finally
{
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
}
}
return true;
}
return false;
}
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
@@ -439,45 +725,146 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
public void* GetPointer(ulong virtualAddress)
{
lock (_gate)
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
{
if (virtualAddress >= region.VirtualAddress &&
virtualAddress < region.VirtualAddress + region.Size)
{
return (void*)virtualAddress;
}
}
return null;
return FindRegion(virtualAddress, 1) is not null
? (void*)virtualAddress
: null;
}
finally
{
_gate.ExitReadLock();
}
}
public bool IsAccessible(ulong virtualAddress, ulong size)
{
lock (_gate)
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
{
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
{
return true;
}
}
return false;
return FindRegion(virtualAddress, size) is not null;
}
finally
{
_gate.ExitReadLock();
}
}
private MemoryRegion? FindRegion(ulong address, ulong size)
{
foreach (var region in _regions)
var low = 0;
var high = _regions.Count - 1;
MemoryRegion? candidate = null;
while (low <= high)
{
if (TryResolveRegionOffset(address, size, region, out _))
var middle = low + ((high - low) >> 1);
var region = _regions[middle];
if (region.VirtualAddress <= address)
{
return region;
candidate = region;
low = middle + 1;
}
else
{
high = middle - 1;
}
}
return null;
return candidate is not null &&
TryResolveRegionOffset(address, size, candidate, out _)
? candidate
: null;
}
private void InsertRegionSorted(MemoryRegion region)
{
var low = 0;
var high = _regions.Count;
while (low < high)
{
var middle = low + ((high - low) >> 1);
if (_regions[middle].VirtualAddress < region.VirtualAddress)
{
low = middle + 1;
}
else
{
high = middle;
}
}
_regions.Insert(low, region);
}
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
{
overlapEnd = 0;
if (size == 0 || ulong.MaxValue - address < size - 1)
{
return false;
}
var end = address + size;
_gate.EnterReadLock();
try
{
foreach (var region in _regions)
{
var regionEnd = region.VirtualAddress + region.Size;
if (region.VirtualAddress >= end)
{
break;
}
if (regionEnd <= address)
{
continue;
}
if (address < regionEnd && region.VirtualAddress < end)
{
overlapEnd = Math.Max(overlapEnd, regionEnd);
}
}
}
finally
{
_gate.ExitReadLock();
}
return overlapEnd != 0;
}
private ulong GetAllocationSearchCursor(
ulong desiredAddress,
ulong requestedCursor,
ulong alignment,
bool executable)
{
lock (_allocationSearchHintGate)
{
var key = (desiredAddress, alignment, executable);
if (_allocationSearchHints.TryGetValue(key, out var hintedCursor) &&
hintedCursor > requestedCursor)
{
return AlignUp(hintedCursor, alignment);
}
}
return requestedCursor;
}
private void UpdateAllocationSearchCursor(
ulong desiredAddress,
ulong alignment,
bool executable,
ulong nextCursor)
{
lock (_allocationSearchHintGate)
{
_allocationSearchHints[(desiredAddress, alignment, executable)] = AlignUp(nextCursor, alignment);
}
}
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
@@ -507,6 +894,44 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
}
private bool CanReadWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
CanAccessWithoutProtectionChange(address, size, region, write: false);
private bool CanWriteWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
CanAccessWithoutProtectionChange(address, size, region, write: true);
private bool CanAccessWithoutProtectionChange(ulong address, ulong size, MemoryRegion region, bool write)
{
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
{
if (_pageProtections.TryGetValue(pageAddress, out var flags))
{
if (write ? (flags & ProgramHeaderFlags.Write) == 0 : (flags & ProgramHeaderFlags.Read) == 0)
{
return false;
}
}
else if (write ? !IsWritableProtection(region.Protection) : !IsReadableProtection(region.Protection))
{
return false;
}
}
return true;
}
private static bool IsReadableProtection(uint protection)
{
return protection is PAGE_READONLY or PAGE_READWRITE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE;
}
private static bool IsWritableProtection(uint protection)
{
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
}
private static uint GetCommitProtection(MemoryRegion region)
{
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
@@ -523,15 +948,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
var endPage = AlignUp(address + size, PageSize);
var commitProtection = GetCommitProtection(region);
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
var pageAddress = startPage;
while (pageAddress < endPage)
{
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{
return false;
}
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
? ulong.MaxValue
: info.BaseAddress + info.RegionSize;
var rangeEnd = Math.Min(endPage, queriedEnd);
if (rangeEnd <= pageAddress)
{
return false;
}
if (info.State == MEM_COMMIT)
{
pageAddress = rangeEnd;
continue;
}
@@ -540,10 +976,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return false;
}
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null)
var commitSize = rangeEnd - pageAddress;
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
{
return false;
}
pageAddress = rangeEnd;
}
return true;
@@ -596,6 +1035,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return checked((value + mask) & ~mask);
}
private static ulong ResolveLazyReservePrimeBytes()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_LAZY_RESERVE_PRIME_MB");
if (ulong.TryParse(configured, out var megabytes))
{
return megabytes == 0
? 0
: checked(Math.Min(megabytes, 4096UL) * 1024UL * 1024UL);
}
return DefaultLazyReservePrimeBytes;
}
private static void TraceVmem(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
{
return;
}
Log.Debug(message);
}
public void Dispose()
{
if (!_disposed)
+145 -40
View File
@@ -10,15 +10,20 @@ using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData;
using SharpEmu.Logging;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace SharpEmu.Core.Runtime;
public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
@@ -78,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();
@@ -125,18 +129,22 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
public OrbisGen2Result Run(string ebootPath)
{
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
var normalizedEbootPath = Path.GetFullPath(ebootPath);
using var app0Binding = BindApp0Root(normalizedEbootPath);
Log.Info($"Loading: {ebootPath}");
LastExecutionDiagnostics = null;
LastExecutionTrace = null;
LastSessionSummary = null;
LastBasicBlockTrace = null;
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(ebootPath);
var normalizedEbootPath = Path.GetFullPath(ebootPath);
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
@@ -159,7 +167,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName);
if (initializerResult is { } failedInitializerResult)
{
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -167,8 +175,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return failedInitializerResult;
}
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
Log.Info($"Dispatching, gen: {generation}");
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
var result = _cpuDispatcher.DispatchEntry(
image.EntryPoint,
@@ -178,8 +186,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName,
_cpuExecutionOptions);
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
Log.Info($"DispatchEntry returned: {result}");
Log.Info($"Dispatch result: {result}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -350,6 +358,101 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return result;
}
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
{
var executableName = Path.GetFileName(ebootPath);
if (string.IsNullOrWhiteSpace(executableName))
{
executableName = "eboot.bin";
}
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
var builder = new StringBuilder();
builder.AppendLine("App bundle info:");
builder.AppendLine($"- Display name: {displayName}");
builder.AppendLine($"- Version: {version}");
builder.AppendLine($"- Title ID: {titleId}");
builder.AppendLine($"- Content ID: {contentId}");
builder.AppendLine($"- Executable: {executableName}");
builder.Append("- Platform: PlayStation 5");
Log.Info(builder.ToString());
}
private static string? ResolveContentId(string? bundleRoot)
{
if (string.IsNullOrEmpty(bundleRoot))
{
return null;
}
foreach (var candidate in new[]
{
Path.Combine(bundleRoot, "sce_sys", "param.json"),
Path.Combine(bundleRoot, "param.json"),
})
{
if (!File.Exists(candidate))
{
continue;
}
try
{
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
{
return contentId.GetString();
}
}
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
{
}
break;
}
return null;
}
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
{
const string app0VariableName = "SHARPEMU_APP0_DIR";
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(app0VariableName)))
{
return null;
}
var app0Root = Path.GetDirectoryName(normalizedEbootPath);
if (string.IsNullOrWhiteSpace(app0Root))
{
return null;
}
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
return new App0BindingScope(app0VariableName);
}
private sealed class App0BindingScope(string variableName) : IDisposable
{
private readonly string _variableName = variableName;
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
Environment.SetEnvironmentVariable(_variableName, null);
_disposed = true;
}
}
private OrbisGen2Result? RunAllInitializers(
SelfImage mainImage,
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
@@ -395,8 +498,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
moduleName = $"module#{i}";
}
Console.Error.WriteLine(
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
Log.Info(
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
var result = _cpuDispatcher.DispatchModuleInitializer(
initEntryPoint,
@@ -407,8 +510,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
_cpuExecutionOptions);
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
Console.Error.WriteLine(
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
Log.Error(
$"Module start failed: {moduleName} -> {result}");
return result;
}
}
@@ -429,8 +532,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return null;
}
Console.Error.WriteLine(
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
Log.Info(
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
var result = RunInitializerList(
$"{label}:preinit",
@@ -469,8 +572,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
continue;
}
Console.Error.WriteLine(
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
Log.Debug(
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
var result = _cpuDispatcher.DispatchEntry(
initializerAddress,
@@ -504,6 +607,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
Path.Combine(ebootDirectory, "sce_module"),
Path.Combine(ebootDirectory, "sce_modules"),
Path.Combine(ebootDirectory, "Media", "Modules"),
}
.Distinct(StringComparer.OrdinalIgnoreCase)
.Where(Directory.Exists)
@@ -515,7 +619,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
}
var allModulePaths = moduleDirectories
.SelectMany(Directory.EnumerateFiles)
.SelectMany(directory => Directory
.EnumerateFiles(directory)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
.Where(path =>
{
var extension = Path.GetExtension(path);
@@ -523,7 +629,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
})
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
var modulePaths = allModulePaths
@@ -536,7 +641,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
.ToArray();
if (skippedModules.Length > 0)
{
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
}
if (modulePaths.Length == 0)
@@ -544,8 +649,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return loadedImages;
}
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
Log.Info($"Loading {modulePaths.Length} module(s)...");
var loadedModules = 0;
var failedModules = 0;
var mergedImportCount = 0;
@@ -580,18 +685,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
loadedModules++;
Console.Error.WriteLine(
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
Log.Info(
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
}
catch (Exception ex)
{
failedModules++;
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
}
}
Console.Error.WriteLine(
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
Log.Info(
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
return loadedImages;
}
@@ -611,8 +716,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (rebound != 0 || unresolved != 0)
{
Console.Error.WriteLine(
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
Log.Info(
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
}
}
@@ -644,8 +749,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
Console.Error.WriteLine(
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
Log.Warning(
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
}
unresolved++;
@@ -657,8 +762,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
Console.Error.WriteLine(
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
Log.Error(
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
unresolved++;
@@ -667,8 +772,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (logRebind)
{
Console.Error.WriteLine(
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
Log.Debug(
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
rebound++;
@@ -704,8 +809,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
Log.Warning(
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
}
continue;
@@ -814,8 +919,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
image.EntryPoint,
isMain,
isSystemModule);
Console.Error.WriteLine(
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
Log.Info(
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
}
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
+5 -1
View File
@@ -1,4 +1,4 @@
<!--
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageReference Include="Iced" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
+104 -2
View File
@@ -8,17 +8,119 @@
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )"
"SharpEmu.HLE": "[1.0.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
"Silk.NET.Windowing": "[2.23.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"Silk.NET.Core": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.EXT": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.KHR": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
}
}
}
+198
View File
@@ -0,0 +1,198 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SharpEmu.GUI.App"
RequestedThemeVariant="Dark">
<Application.Resources>
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
<LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
<GradientStop Offset="0" Color="#12151F" />
<GradientStop Offset="0.55" Color="#0D1017" />
<GradientStop Offset="1" Color="#0B0D14" />
</LinearGradientBrush>
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
<SolidColorBrush x:Key="ElevatedBrush" Color="#1B2230" />
<SolidColorBrush x:Key="TextBrush" Color="#E8ECF4" />
<SolidColorBrush x:Key="MutedBrush" Color="#8B94A7" />
<SolidColorBrush x:Key="FaintBrush" Color="#5A6478" />
<SolidColorBrush x:Key="AccentBrush" Color="#7C5CFC" />
<SolidColorBrush x:Key="AccentHoverBrush" Color="#8F73FF" />
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
</Application.Resources>
<Application.Styles>
<FluentTheme />
<Style Selector="Window">
<Setter Property="FontFamily" Value="Inter, Segoe UI, sans-serif" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="Border.card">
<Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="16" />
</Style>
<Style Selector="Border.pill">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="CornerRadius" Value="999" />
<Setter Property="Padding" Value="10,3" />
</Style>
<Style Selector="TextBlock.sectionTitle">
<Setter Property="FontSize" Value="11" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="LetterSpacing" Value="1.5" />
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
</Style>
<Style Selector="TextBlock.fieldLabel">
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
<Setter Property="Margin" Value="0,0,0,6" />
</Style>
<Style Selector="TextBox">
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.accent">
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="22,10" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource AccentHoverBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="22,10" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource DangerHoverBrush}" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.ghost">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="Padding" Value="12,7" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="ToggleButton.ghost">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
<Setter Property="Padding" Value="12,7" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="ToggleButton.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="ToggleButton.ghost:checked /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="ContextMenu">
<Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Padding" Value="6" />
</Style>
<Style Selector="ContextMenu MenuItem">
<Setter Property="Padding" Value="10,7" />
<Setter Property="CornerRadius" Value="7" />
</Style>
<Style Selector="ContextMenu Separator">
<Setter Property="Background" Value="{StaticResource CardBorderBrush}" />
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="8,4" />
</Style>
<Style Selector="ListBox.console">
<Setter Property="Background" Value="#0B0E14" />
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="ListBox.console ListBoxItem">
<Setter Property="Padding" Value="10,1" />
<Setter Property="MinHeight" Value="0" />
</Style>
<!-- Cover-art library grid -->
<Style Selector="ListBox.tileGrid ListBoxItem">
<Setter Property="Padding" Value="10" />
<Setter Property="Margin" Value="5" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="RenderTransform" Value="translateY(0px)" />
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
</Transitions>
</Setter>
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
<Setter Property="RenderTransform" Value="translateY(-3px)" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
</Style>
<Style Selector="Border.coverShadow">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="BoxShadow" Value="0 6 14 0 #55000000" />
</Style>
<Style Selector="Border.coverClip">
<Setter Property="CornerRadius" Value="10" />
<Setter Property="ClipToBounds" Value="True" />
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
</Style>
</Application.Styles>
</Application>
+26
View File
@@ -0,0 +1,26 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace SharpEmu.GUI;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
}
}
+120
View File
@@ -0,0 +1,120 @@
#nullable disable
using System.IO;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
/// <summary>
/// Stores the configuration data needed to decode or encode an ATRAC9 stream.
/// </summary>
public class Atrac9Config
{
/// <summary>
/// The 4-byte ATRAC9 configuration data.
/// </summary>
public byte[] ConfigData { get; }
/// <summary>
/// A 4-bit value specifying one of 16 sample rates.
/// </summary>
public int SampleRateIndex { get; }
/// <summary>
/// A 3-bit value specifying one of 6 substream channel mappings.
/// </summary>
public int ChannelConfigIndex { get; }
/// <summary>
/// An 11-bit value containing the average size of a single frame.
/// </summary>
public int FrameBytes { get; }
/// <summary>
/// A 2-bit value indicating how many frames are in each superframe.
/// </summary>
public int SuperframeIndex { get; }
/// <summary>
/// The channel mapping used by the ATRAC9 stream.
/// </summary>
public ChannelConfig ChannelConfig { get; }
/// <summary>
/// The total number of channels in the ATRAC9 stream.
/// </summary>
public int ChannelCount { get; }
/// <summary>
/// The sample rate of the ATRAC9 stream.
/// </summary>
public int SampleRate { get; }
/// <summary>
/// Indicates whether the ATRAC9 stream has a <see cref="SampleRateIndex"/> of 8 or above.
/// </summary>
public bool HighSampleRate { get; }
/// <summary>
/// The number of frames in each superframe.
/// </summary>
public int FramesPerSuperframe { get; }
/// <summary>
/// The number of samples in one frame as an exponent of 2.
/// <see cref="FrameSamples"/> = 2^<see cref="FrameSamplesPower"/>.
/// </summary>
public int FrameSamplesPower { get; }
/// <summary>
/// The number of samples in one frame.
/// </summary>
public int FrameSamples { get; }
/// <summary>
/// The number of bytes in one superframe.
/// </summary>
public int SuperframeBytes { get; }
/// <summary>
/// The number of samples in one superframe.
/// </summary>
public int SuperframeSamples { get; }
/// <summary>
/// Reads ATRAC9 configuration data and calculates the stream parameters from it.
/// </summary>
/// <param name="configData">The processed ATRAC9 configuration.</param>
public Atrac9Config(byte[] configData)
{
if (configData == null || configData.Length != 4)
{
throw new InvalidDataException("Config data must be 4 bytes long");
}
ReadConfigData(configData, out int a, out int b, out int c, out int d);
SampleRateIndex = a;
ChannelConfigIndex = b;
FrameBytes = c;
SuperframeIndex = d;
ConfigData = configData;
FramesPerSuperframe = 1 << SuperframeIndex;
SuperframeBytes = FrameBytes << SuperframeIndex;
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
ChannelCount = ChannelConfig.ChannelCount;
SampleRate = Tables.SampleRates[SampleRateIndex];
HighSampleRate = SampleRateIndex > 7;
FrameSamplesPower = Tables.SamplingRateIndexToFrameSamplesPower[SampleRateIndex];
FrameSamples = 1 << FrameSamplesPower;
SuperframeSamples = FrameSamples * FramesPerSuperframe;
}
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex)
{
var reader = new BitReader(configData);
int header = reader.ReadInt(8);
sampleRateIndex = reader.ReadInt(4);
channelConfigIndex = reader.ReadInt(3);
int validationBit = reader.ReadInt(1);
frameBytes = reader.ReadInt(11) + 1;
superframeIndex = reader.ReadInt(2);
if (header != 0xFE || validationBit != 0)
{
throw new InvalidDataException("ATRAC9 Config Data is invalid");
}
}
}
}
+128
View File
@@ -0,0 +1,128 @@
#nullable disable
using System;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
/// <summary>
/// Decodes an ATRAC9 stream into 16-bit PCM.
/// </summary>
public class Atrac9Decoder
{
/// <summary>
/// The config data for the current ATRAC9 stream.
/// </summary>
public Atrac9Config Config { get; private set; }
private Frame Frame { get; set; }
private BitReader Reader { get; set; }
private bool _initialized;
/// <summary>
/// Sets up the decoder to decode an ATRAC9 stream based on the information in <paramref name="configData"/>.
/// </summary>
/// <param name="configData">A 4-byte value containing information about the ATRAC9 stream.</param>
public void Initialize(byte[] configData)
{
Config = new Atrac9Config(configData);
Frame = new Frame(Config);
Reader = new BitReader(null);
_initialized = true;
}
/// <summary>
/// Decodes one superframe of ATRAC9 data.
/// </summary>
/// <param name="atrac9Data">The ATRAC9 data to decode. The array must be at least
/// <see cref="Config"/>.<see cref="Atrac9Config.SuperframeBytes"/> bytes long.</param>
/// <param name="pcmOut">A buffer that the decoded PCM data will be placed in.
/// The array must have dimensions of at least [<see cref="Config"/>.<see cref="Atrac9Config.ChannelCount"/>]
/// [<see cref="Config"/>.<see cref="Atrac9Config.SuperframeSamples"/>].</param>
public void Decode(byte[] atrac9Data, short[][] pcmOut)
{
if (!_initialized) throw new InvalidOperationException("Decoder must be initialized before decoding.");
ValidateDecodeBuffers(atrac9Data, pcmOut);
Reader.SetBuffer(atrac9Data);
DecodeSuperFrame(pcmOut);
}
private void ValidateDecodeBuffers(byte[] atrac9Buffer, short[][] pcmBuffer)
{
if (atrac9Buffer == null) throw new ArgumentNullException(nameof(atrac9Buffer));
if (pcmBuffer == null) throw new ArgumentNullException(nameof(pcmBuffer));
if (atrac9Buffer.Length < Config.SuperframeBytes)
{
throw new ArgumentException("ATRAC9 buffer is too small");
}
if (pcmBuffer.Length < Config.ChannelCount)
{
throw new ArgumentException("PCM buffer is too small");
}
for (int i = 0; i < Config.ChannelCount; i++)
{
if (pcmBuffer[i]?.Length < Config.SuperframeSamples)
{
throw new ArgumentException("PCM buffer is too small");
}
}
}
private void DecodeSuperFrame(short[][] pcmOut)
{
for (int i = 0; i < Config.FramesPerSuperframe; i++)
{
Frame.FrameIndex = i;
DecodeFrame(Reader, Frame);
PcmFloatToShort(pcmOut, i * Config.FrameSamples);
Reader.AlignPosition(8);
}
}
private void PcmFloatToShort(short[][] pcmOut, int start)
{
int endSample = start + Config.FrameSamples;
int channelNum = 0;
foreach (Block block in Frame.Blocks)
{
foreach (Channel channel in block.Channels)
{
double[] pcmSrc = channel.Pcm;
short[] pcmDest = pcmOut[channelNum++];
for (int d = 0, s = start; s < endSample; d++, s++)
{
double sample = pcmSrc[d];
// Not using Math.Round because it's ~20x slower on 64-bit
int roundedSample = (int)Math.Floor(sample + 0.5);
pcmDest[s] = Helpers.Clamp16(roundedSample);
}
}
}
}
private static void DecodeFrame(BitReader reader, Frame frame)
{
Unpack.UnpackFrame(reader, frame);
foreach (Block block in frame.Blocks)
{
Quantization.DequantizeSpectra(block);
Stereo.ApplyIntensityStereo(block);
Quantization.ScaleSpectrum(block);
BandExtension.ApplyBandExtension(block);
ImdctBlock(block);
}
}
private static void ImdctBlock(Block block)
{
foreach (Channel channel in block.Channels)
{
channel.Mdct.RunImdct(channel.Spectra, channel.Pcm);
}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
#nullable disable
namespace LibAtrac9
{
/// <summary>
/// An Xorshift RNG used by the ATRAC9 codec
/// </summary>
internal class Atrac9Rng
{
private ushort _stateA;
private ushort _stateB;
private ushort _stateC;
private ushort _stateD;
public Atrac9Rng(ushort seed)
{
int startValue = 0x4D93 * (seed ^ (seed >> 14));
_stateA = (ushort)(3 - startValue);
_stateB = (ushort)(2 - startValue);
_stateC = (ushort)(1 - startValue);
_stateD = (ushort)(0 - startValue);
}
public ushort Next()
{
ushort t = (ushort)(_stateD ^ (_stateD << 5));
_stateD = _stateC;
_stateC = _stateB;
_stateB = _stateA;
_stateA = (ushort)(t ^ _stateA ^ ((t ^ (_stateA >> 5)) >> 4));
return _stateA;
}
}
}
+373
View File
@@ -0,0 +1,373 @@
#nullable disable
using System;
namespace LibAtrac9
{
internal static class BandExtension
{
public static void ApplyBandExtension(Block block)
{
if (!block.BandExtensionEnabled || !block.HasExtensionData) return;
foreach (Channel channel in block.Channels)
{
ApplyBandExtensionChannel(channel);
}
}
private static void ApplyBandExtensionChannel(Channel channel)
{
int groupAUnit = channel.Block.QuantizationUnitCount;
int[] scaleFactors = channel.ScaleFactors;
double[] spectra = channel.Spectra;
double[] scales = channel.BexScales;
int[] values = channel.BexValues;
GetBexBandInfo(out int bandCount, out int groupBUnit, out int groupCUnit, groupAUnit);
int totalUnits = Math.Max(groupCUnit, 22);
int groupABin = Tables.QuantUnitToCoeffIndex[groupAUnit];
int groupBBin = Tables.QuantUnitToCoeffIndex[groupBUnit];
int groupCBin = Tables.QuantUnitToCoeffIndex[groupCUnit];
int totalBins = Tables.QuantUnitToCoeffIndex[totalUnits];
FillHighFrequencies(spectra, groupABin, groupBBin, groupCBin, totalBins);
switch (channel.BexMode)
{
case 0:
int bexQuantUnits = totalUnits - groupAUnit;
switch (bandCount)
{
case 3:
scales[0] = BexMode0Bands3[0][values[0]];
scales[1] = BexMode0Bands3[1][values[0]];
scales[2] = BexMode0Bands3[2][values[1]];
scales[3] = BexMode0Bands3[3][values[2]];
scales[4] = BexMode0Bands3[4][values[3]];
break;
case 4:
scales[0] = BexMode0Bands4[0][values[0]];
scales[1] = BexMode0Bands4[1][values[0]];
scales[2] = BexMode0Bands4[2][values[1]];
scales[3] = BexMode0Bands4[3][values[2]];
scales[4] = BexMode0Bands4[4][values[3]];
break;
case 5:
scales[0] = BexMode0Bands5[0][values[0]];
scales[1] = BexMode0Bands5[1][values[1]];
scales[2] = BexMode0Bands5[2][values[1]];
break;
}
scales[bexQuantUnits - 1] = Tables.SpectrumScale[scaleFactors[groupAUnit]];
AddNoiseToSpectrum(channel, Tables.QuantUnitToCoeffIndex[totalUnits - 1],
Tables.QuantUnitToCoeffCount[totalUnits - 1]);
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
break;
case 1:
for (int i = groupAUnit; i < totalUnits; i++)
{
scales[i - groupAUnit] = Tables.SpectrumScale[scaleFactors[i]];
}
AddNoiseToSpectrum(channel, groupABin, totalBins - groupABin);
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
break;
case 2:
double groupAScale2 = BexMode2Scale[values[0]];
double groupBScale2 = BexMode2Scale[values[1]];
for (int i = groupABin; i < groupBBin; i++)
{
spectra[i] *= groupAScale2;
}
for (int i = groupBBin; i < groupCBin; i++)
{
spectra[i] *= groupBScale2;
}
return;
case 3:
double rate = Math.Pow(2, BexMode3Rate[values[1]]);
double scale = BexMode3Initial[values[0]];
for (int i = groupABin; i < totalBins; i++)
{
scale *= rate;
spectra[i] *= scale;
}
return;
case 4:
double mult = BexMode4Multiplier[values[0]];
double groupAScale4 = 0.7079468 * mult;
double groupBScale4 = 0.5011902 * mult;
double groupCScale4 = 0.3548279 * mult;
for (int i = groupABin; i < groupBBin; i++)
{
spectra[i] *= groupAScale4;
}
for (int i = groupBBin; i < groupCBin; i++)
{
spectra[i] *= groupBScale4;
}
for (int i = groupCBin; i < totalBins; i++)
{
spectra[i] *= groupCScale4;
}
return;
}
}
private static void ScaleBexQuantUnits(double[] spectra, double[] scales, int startUnit, int totalUnits)
{
for (int i = startUnit; i < totalUnits; i++)
{
for (int k = Tables.QuantUnitToCoeffIndex[i]; k < Tables.QuantUnitToCoeffIndex[i + 1]; k++)
{
spectra[k] *= scales[i - startUnit];
}
}
}
private static void FillHighFrequencies(double[] spectra, int groupABin, int groupBBin, int groupCBin, int totalBins)
{
for (int i = 0; i < groupBBin - groupABin; i++)
{
spectra[groupABin + i] = spectra[groupABin - i - 1];
}
for (int i = 0; i < groupCBin - groupBBin; i++)
{
spectra[groupBBin + i] = spectra[groupBBin - i - 1];
}
for (int i = 0; i < totalBins - groupCBin; i++)
{
spectra[groupCBin + i] = spectra[groupCBin - i - 1];
}
}
private static void AddNoiseToSpectrum(Channel channel, int index, int count)
{
if (channel.Rng == null)
{
int[] sf = channel.ScaleFactors;
ushort seed = (ushort)(543 * (sf[8] + sf[12] + sf[15] + 1));
channel.Rng = new Atrac9Rng(seed);
}
for (int i = 0; i < count; i++)
{
channel.Spectra[i + index] = channel.Rng.Next() / 65535.0 * 2.0 - 1.0;
}
}
public static void GetBexBandInfo(out int bandCount, out int groupAUnit, out int groupBUnit, int quantUnits)
{
groupAUnit = BexGroupInfo[quantUnits - 13][0];
groupBUnit = BexGroupInfo[quantUnits - 13][1];
bandCount = BexGroupInfo[quantUnits - 13][2];
}
public static readonly byte[][] BexGroupInfo =
{
new byte[] {16, 21, 0},
new byte[] {18, 22, 1},
new byte[] {20, 22, 2},
new byte[] {21, 22, 3},
new byte[] {21, 22, 3},
new byte[] {23, 24, 4},
new byte[] {23, 24, 4},
new byte[] {24, 24, 5}
};
// [mode][bands]
public static readonly byte[][] BexEncodedValueCounts =
{
new byte[] {0, 0, 0, 4, 4, 2},
new byte[] {0, 0, 0, 0, 0, 0},
new byte[] {0, 0, 0, 2, 2, 1},
new byte[] {0, 0, 0, 2, 2, 2},
new byte[] {1, 1, 1, 0, 0, 0}
};
// [mode][bands][valueIndex]
public static readonly byte[][][] BexDataLengths =
{
new[] {
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {5, 4, 3, 3},
new byte[] {4, 4, 3, 4},
new byte[] {4, 5, 0, 0}
}, new[] {
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0}
}, new[] {
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {6, 6, 0, 0},
new byte[] {6, 6, 0, 0},
new byte[] {6, 0, 0, 0}
}, new[] {
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {4, 4, 0, 0},
new byte[] {4, 4, 0, 0},
new byte[] {4, 4, 0, 0}
}, new[] {
new byte[] {3, 0, 0, 0},
new byte[] {3, 0, 0, 0},
new byte[] {3, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0},
new byte[] {0, 0, 0, 0}
}
};
public static readonly double[][] BexMode0Bands3 =
{
new[] {
0.000000e+0, 1.988220e-1, 2.514343e-1, 2.960510e-1,
3.263550e-1, 3.771362e-1, 3.786926e-1, 4.540405e-1,
4.877625e-1, 5.262451e-1, 5.447083e-1, 5.737000e-1,
6.212158e-1, 6.222839e-1, 6.560974e-1, 6.896667e-1,
7.555542e-1, 7.677917e-1, 7.918091e-1, 7.971497e-1,
8.188171e-1, 8.446045e-1, 9.790649e-1, 9.822083e-1,
9.846191e-1, 9.859314e-1, 9.863586e-1, 9.863892e-1,
9.873352e-1, 9.881287e-1, 9.898682e-1, 9.913330e-1
}, new[] {
0.000000e+0, 9.982910e-1, 7.592773e-2, 7.179565e-1,
9.851379e-1, 5.340271e-1, 9.013672e-1, 6.349182e-1,
7.226257e-1, 1.948547e-1, 7.628174e-1, 9.873657e-1,
8.112183e-1, 2.715454e-1, 9.734192e-1, 1.443787e-1,
4.640198e-1, 3.249207e-1, 3.790894e-1, 8.276367e-2,
5.954590e-1, 2.864380e-1, 9.806824e-1, 7.929077e-1,
6.292114e-1, 4.887085e-1, 2.905273e-1, 1.301880e-1,
3.140869e-1, 5.482483e-1, 4.210815e-1, 1.182861e-1
}, new[] {
0.000000e+0, 3.155518e-2, 8.581543e-2, 1.364746e-1,
1.858826e-1, 2.368469e-1, 2.888184e-1, 3.432617e-1,
4.012451e-1, 4.623108e-1, 5.271301e-1, 5.954895e-1,
6.681213e-1, 7.448425e-1, 8.245239e-1, 9.097290e-1
}, new[] {
0.000000e+0, 4.418945e-2, 1.303711e-1, 2.273560e-1,
3.395996e-1, 4.735718e-1, 6.267090e-1, 8.003845e-1
}, new[] {
0.000000e+0, 2.804565e-2, 9.683228e-2, 1.849976e-1,
3.005981e-1, 4.470520e-1, 6.168518e-1, 8.007813e-1
}
};
public static readonly double[][] BexMode0Bands4 =
{
new[] {
0.000000e+0, 2.708740e-1, 3.479614e-1, 3.578186e-1,
5.083618e-1, 5.299072e-1, 5.819092e-1, 6.381836e-1,
7.276917e-1, 7.595520e-1, 7.878723e-1, 9.707336e-1,
9.713135e-1, 9.736023e-1, 9.759827e-1, 9.832458e-1
}, new[] {
0.000000e+0, 2.330627e-1, 5.891418e-1, 7.170410e-1,
2.036438e-1, 1.613464e-1, 6.668701e-1, 9.481201e-1,
9.769897e-1, 5.111694e-1, 3.522644e-1, 8.209534e-1,
2.933960e-1, 9.757690e-1, 5.289917e-1, 4.372253e-1
}, new[] {
0.000000e+0, 4.360962e-2, 1.056519e-1, 1.590576e-1,
2.078857e-1, 2.572937e-1, 3.082581e-1, 3.616028e-1,
4.191589e-1, 4.792175e-1, 5.438538e-1, 6.125183e-1,
6.841125e-1, 7.589417e-1, 8.365173e-1, 9.148254e-1
}, new[] {
0.000000e+0, 4.074097e-2, 1.164551e-1, 2.077026e-1,
3.184509e-1, 4.532166e-1, 6.124268e-1, 7.932129e-1
}, new[] {
0.000000e+0, 8.880615e-3, 2.932739e-2, 5.593872e-2,
8.825684e-2, 1.259155e-1, 1.721497e-1, 2.270813e-1,
2.901611e-1, 3.579712e-1, 4.334106e-1, 5.147095e-1,
6.023254e-1, 6.956177e-1, 7.952881e-1, 8.977356e-1
}
};
public static readonly double[][] BexMode0Bands5 =
{
new[] {
0.000000e+0, 7.379150e-2, 1.806335e-1, 2.687073e-1,
3.407898e-1, 4.047546e-1, 4.621887e-1, 5.168762e-1,
5.703125e-1, 6.237488e-1, 6.763611e-1, 7.288208e-1,
7.808533e-1, 8.337708e-1, 8.874512e-1, 9.418030e-1
}, new[] {
0.000000e+0, 7.980347e-2, 1.615295e-1, 1.665649e-1,
1.822205e-1, 2.185669e-1, 2.292175e-1, 2.456665e-1,
2.666321e-1, 3.306580e-1, 3.330688e-1, 3.765259e-1,
4.085083e-1, 4.400024e-1, 4.407654e-1, 4.817505e-1,
4.924011e-1, 5.320740e-1, 5.893860e-1, 6.131287e-1,
6.212463e-1, 6.278076e-1, 6.308899e-1, 7.660828e-1,
7.850647e-1, 7.910461e-1, 7.929382e-1, 8.038330e-1,
9.834900e-1, 9.846191e-1, 9.852295e-1, 9.862671e-1
}, new[] {
0.000000e+0, 6.084290e-1, 3.672791e-1, 3.151855e-1,
1.488953e-1, 2.571716e-1, 5.103455e-1, 3.311157e-1,
5.426025e-2, 4.254456e-1, 7.998352e-1, 7.873230e-1,
5.418701e-1, 2.925110e-1, 8.468628e-2, 1.410522e-1,
9.819641e-1, 9.609070e-1, 3.530884e-2, 9.729004e-2,
5.758362e-1, 9.941711e-1, 7.215576e-1, 7.183228e-1,
2.028809e-1, 9.588623e-2, 2.032166e-1, 1.338806e-1,
5.003357e-1, 1.874390e-1, 9.804993e-1, 1.107788e-1
}
};
public static readonly double[] BexMode2Scale =
{
4.272461e-4, 1.312256e-3, 2.441406e-3, 3.692627e-3,
4.913330e-3, 6.134033e-3, 7.507324e-3, 8.972168e-3,
1.049805e-2, 1.223755e-2, 1.406860e-2, 1.599121e-2,
1.800537e-2, 2.026367e-2, 2.264404e-2, 2.517700e-2,
2.792358e-2, 3.073120e-2, 3.344727e-2, 3.631592e-2,
3.952026e-2, 4.275513e-2, 4.608154e-2, 4.968262e-2,
5.355835e-2, 5.783081e-2, 6.195068e-2, 6.677246e-2,
7.196045e-2, 7.745361e-2, 8.319092e-2, 8.993530e-2,
9.759521e-2, 1.056213e-1, 1.138916e-1, 1.236267e-1,
1.348267e-1, 1.470337e-1, 1.603394e-1, 1.755676e-1,
1.905823e-1, 2.071228e-1, 2.245178e-1, 2.444153e-1,
2.658997e-1, 2.897644e-1, 3.146057e-1, 3.450012e-1,
3.766174e-1, 4.122620e-1, 4.505615e-1, 4.893799e-1,
5.305481e-1, 5.731201e-1, 6.157837e-1, 6.580811e-1,
6.985168e-1, 7.435303e-1, 7.865906e-1, 8.302612e-1,
8.718567e-1, 9.125671e-1, 9.575806e-1, 9.996643e-1
};
public static readonly double[] BexMode3Initial =
{
3.491211e-1, 5.371094e-1, 6.782227e-1, 7.910156e-1,
9.057617e-1, 1.024902e+0, 1.156250e+0, 1.290527e+0,
1.458984e+0, 1.664551e+0, 1.929688e+0, 2.278320e+0,
2.831543e+0, 3.659180e+0, 5.257813e+0, 8.373047e+0
};
public static readonly double[] BexMode3Rate =
{
-2.913818e-1, -2.541504e-1, -1.664429e-1, -1.476440e-1,
-1.342163e-1, -1.220703e-1, -1.117554e-1, -1.026611e-1,
-9.436035e-2, -8.483887e-2, -7.476807e-2, -6.304932e-2,
-4.492188e-2, -2.447510e-2, +1.831055e-4, +4.174805e-2
};
public static readonly double[] BexMode4Multiplier =
{
3.610229e-2, 1.260681e-1, 2.227478e-1, 3.338318e-1,
4.662170e-1, 6.221313e-1, 7.989197e-1, 9.939575e-1
};
}
}
+141
View File
@@ -0,0 +1,141 @@
#nullable disable
using System;
namespace LibAtrac9
{
internal static class BitAllocation
{
public static void CreateGradient(Block block)
{
int valueCount = block.GradientEndValue - block.GradientStartValue;
int unitCount = block.GradientEndUnit - block.GradientStartUnit;
for (int i = 0; i < block.GradientEndUnit; i++)
{
block.Gradient[i] = block.GradientStartValue;
}
for (int i = block.GradientEndUnit; i <= block.QuantizationUnitCount; i++)
{
block.Gradient[i] = block.GradientEndValue;
}
if (unitCount <= 0) return;
if (valueCount == 0) return;
byte[] curve = Tables.GradientCurves[unitCount - 1];
if (valueCount <= 0)
{
double scale = (-valueCount - 1) / 31.0;
int baseVal = block.GradientStartValue - 1;
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
{
block.Gradient[i] = baseVal - (int)(curve[i - block.GradientStartUnit] * scale);
}
}
else
{
double scale = (valueCount - 1) / 31.0;
int baseVal = block.GradientStartValue + 1;
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
{
block.Gradient[i] = baseVal + (int)(curve[i - block.GradientStartUnit] * scale);
}
}
}
public static void CalculateMask(Channel channel)
{
Array.Clear(channel.PrecisionMask, 0, channel.PrecisionMask.Length);
for (int i = 1; i < channel.Block.QuantizationUnitCount; i++)
{
int delta = channel.ScaleFactors[i] - channel.ScaleFactors[i - 1];
if (delta > 1)
{
channel.PrecisionMask[i] += Math.Min(delta - 1, 5);
}
else if (delta < -1)
{
channel.PrecisionMask[i - 1] += Math.Min(delta * -1 - 1, 5);
}
}
}
public static void CalculatePrecisions(Channel channel)
{
Block block = channel.Block;
if (block.GradientMode != 0)
{
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
channel.Precisions[i] = channel.ScaleFactors[i] + channel.PrecisionMask[i] - block.Gradient[i];
if (channel.Precisions[i] > 0)
{
switch (block.GradientMode)
{
case 1:
channel.Precisions[i] /= 2;
break;
case 2:
channel.Precisions[i] = 3 * channel.Precisions[i] / 8;
break;
case 3:
channel.Precisions[i] /= 4;
break;
}
}
}
}
else
{
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
channel.Precisions[i] = channel.ScaleFactors[i] - block.Gradient[i];
}
}
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
if (channel.Precisions[i] < 1)
{
channel.Precisions[i] = 1;
}
}
for (int i = 0; i < block.GradientBoundary; i++)
{
channel.Precisions[i]++;
}
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
channel.PrecisionsFine[i] = 0;
if (channel.Precisions[i] > 15)
{
channel.PrecisionsFine[i] = channel.Precisions[i] - 15;
channel.Precisions[i] = 15;
}
}
}
public static byte[][] GenerateGradientCurves()
{
byte[] main =
{
01, 01, 01, 01, 02, 02, 02, 02, 03, 03, 03, 04, 04, 05, 05, 06, 07, 08, 09, 10, 11, 12, 13, 15,
16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30
};
var curves = new byte[main.Length][];
for (int length = 1; length <= main.Length; length++)
{
curves[length - 1] = new byte[length];
for (int i = 0; i < length; i++)
{
curves[length - 1][i] = main[i * main.Length / length];
}
}
return curves;
}
}
}
+92
View File
@@ -0,0 +1,92 @@
#nullable disable
namespace LibAtrac9
{
internal class Block
{
public Atrac9Config Config { get; }
public BlockType BlockType { get; }
public int BlockIndex { get; }
public Frame Frame { get; }
public Channel[] Channels { get; }
public int ChannelCount { get; }
public bool FirstInSuperframe { get; set; }
public bool ReuseBandParams { get; set; }
public int BandCount { get; set; }
public int StereoBand { get; set; }
public int ExtensionBand { get; set; }
public int QuantizationUnitCount { get; set; }
public int StereoQuantizationUnit { get; set; }
public int ExtensionUnit { get; set; }
public int QuantizationUnitsPrev { get; set; }
public int[] Gradient { get; } = new int[31];
public int GradientMode { get; set; }
public int GradientStartUnit { get; set; }
public int GradientStartValue { get; set; }
public int GradientEndUnit { get; set; }
public int GradientEndValue { get; set; }
public int GradientBoundary { get; set; }
public int PrimaryChannelIndex { get; set; }
public int[] JointStereoSigns { get; } = new int[30];
public bool HasJointStereoSigns { get; set; }
public Channel PrimaryChannel => Channels[PrimaryChannelIndex == 0 ? 0 : 1];
public Channel SecondaryChannel => Channels[PrimaryChannelIndex == 0 ? 1 : 0];
public bool BandExtensionEnabled { get; set; }
public bool HasExtensionData { get; set; }
public int BexDataLength { get; set; }
public int BexMode { get; set; }
public Block(Frame parentFrame, int blockIndex)
{
Frame = parentFrame;
BlockIndex = blockIndex;
Config = parentFrame.Config;
BlockType = Config.ChannelConfig.BlockTypes[blockIndex];
ChannelCount = BlockTypeToChannelCount(BlockType);
Channels = new Channel[ChannelCount];
for (int i = 0; i < ChannelCount; i++)
{
Channels[i] = new Channel(this, i);
}
}
public static int BlockTypeToChannelCount(BlockType blockType)
{
switch (blockType)
{
case BlockType.Mono:
return 1;
case BlockType.Stereo:
return 2;
case BlockType.LFE:
return 1;
default:
return 0;
}
}
}
/// <summary>
/// An ATRAC9 block (substream) type
/// </summary>
public enum BlockType
{
/// <summary>
/// Mono ATRAC9 block
/// </summary>
Mono = 0,
/// <summary>
/// Stereo ATRAC9 block
/// </summary>
Stereo = 1,
/// <summary>
/// Low-frequency effects ATRAC9 block
/// </summary>
LFE = 2
}
}
+49
View File
@@ -0,0 +1,49 @@
#nullable disable
using LibAtrac9.Utilities;
namespace LibAtrac9
{
internal class Channel
{
public Atrac9Config Config { get; }
public int ChannelIndex { get; }
public bool IsPrimary => Block.PrimaryChannelIndex == ChannelIndex;
public Block Block { get; }
public Mdct Mdct { get; }
public double[] Pcm { get; } = new double[256];
public double[] Spectra { get; } = new double[256];
public int CodedQuantUnits { get; set; }
public int ScaleFactorCodingMode { get; set; }
public int[] ScaleFactors { get; } = new int[31];
public int[] ScaleFactorsPrev { get; } = new int[31];
public int[] Precisions { get; } = new int[30];
public int[] PrecisionsFine { get; } = new int[30];
public int[] PrecisionMask { get; } = new int[30];
public int[] SpectraValuesBuffer { get; } = new int[16];
public int[] CodebookSet { get; } = new int[30];
public int[] QuantizedSpectra { get; } = new int[256];
public int[] QuantizedSpectraFine { get; } = new int[256];
public int BexMode { get; set; }
public int BexValueCount { get; set; }
public int[] BexValues { get; } = new int[4];
public double[] BexScales { get; } = new double[6];
public Atrac9Rng Rng { get; set; }
public Channel(Block parentBlock, int channelIndex)
{
Block = parentBlock;
ChannelIndex = channelIndex;
Config = parentBlock.Config;
Mdct = new Mdct(Config.FrameSamplesPower, Tables.ImdctWindow[Config.FrameSamplesPower - 6]);
}
public void UpdateCodedUnits() =>
CodedQuantUnits = IsPrimary ? Block.QuantizationUnitCount : Block.StereoQuantizationUnit;
}
}
+34
View File
@@ -0,0 +1,34 @@
#nullable disable
namespace LibAtrac9
{
/// <summary>
/// Describes the channel mapping for an ATRAC9 stream
/// </summary>
public class ChannelConfig
{
internal ChannelConfig(params BlockType[] blockTypes)
{
BlockCount = blockTypes.Length;
BlockTypes = blockTypes;
foreach (BlockType type in blockTypes)
{
ChannelCount += Block.BlockTypeToChannelCount(type);
}
}
/// <summary>
/// The number of blocks or substreams in the ATRAC9 stream
/// </summary>
public int BlockCount { get; }
/// <summary>
/// The type of each block or substream in the ATRAC9 stream
/// </summary>
public BlockType[] BlockTypes { get; }
/// <summary>
/// The number of channels in the ATRAC9 stream
/// </summary>
public int ChannelCount { get; }
}
}
+21
View File
@@ -0,0 +1,21 @@
#nullable disable
namespace LibAtrac9
{
internal class Frame
{
public Atrac9Config Config { get; }
public int FrameIndex { get; set; }
public Block[] Blocks { get; }
public Frame(Atrac9Config config)
{
Config = config;
Blocks = new Block[config.ChannelConfig.BlockCount];
for (int i = 0; i < config.ChannelConfig.BlockCount; i++)
{
Blocks[i] = new Block(this, i);
}
}
}
}
@@ -0,0 +1,63 @@
#nullable disable
using System;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
internal class HuffmanCodebook
{
public HuffmanCodebook(short[] codes, byte[] bits, byte valueCountPower)
{
Codes = codes;
Bits = bits;
if (Codes == null || Bits == null) return;
ValueCount = 1 << valueCountPower;
ValueCountPower = valueCountPower;
ValueBits = Helpers.Log2(codes.Length) >> valueCountPower;
ValueMax = 1 << ValueBits;
int max = 0;
foreach (byte bitSize in bits)
{
max = Math.Max(max, bitSize);
}
MaxBitSize = max;
Lookup = CreateLookupTable();
}
private byte[] CreateLookupTable()
{
if (Codes == null || Bits == null) return null;
int tableSize = 1 << MaxBitSize;
var dest = new byte[tableSize];
for (int i = 0; i < Bits.Length; i++)
{
if (Bits[i] == 0) continue;
int unusedBits = MaxBitSize - Bits[i];
int start = Codes[i] << unusedBits;
int length = 1 << unusedBits;
int end = start + length;
for (int j = start; j < end; j++)
{
dest[j] = (byte)i;
}
}
return dest;
}
public short[] Codes { get; }
public byte[] Bits { get; }
public byte[] Lookup { get; }
public int ValueCount { get; }
public int ValueCountPower { get; }
public int ValueBits { get; }
public int ValueMax { get; }
public int MaxBitSize { get; }
}
}
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Alex Barney
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+58
View File
@@ -0,0 +1,58 @@
#nullable disable
using System;
namespace LibAtrac9
{
internal static class Quantization
{
public static void DequantizeSpectra(Block block)
{
foreach (Channel channel in block.Channels)
{
Array.Clear(channel.Spectra, 0, channel.Spectra.Length);
for (int i = 0; i < channel.CodedQuantUnits; i++)
{
DequantizeQuantUnit(channel, i);
}
}
}
private static void DequantizeQuantUnit(Channel channel, int band)
{
int subBandIndex = Tables.QuantUnitToCoeffIndex[band];
int subBandCount = Tables.QuantUnitToCoeffCount[band];
double stepSize = Tables.QuantizerStepSize[channel.Precisions[band]];
double stepSizeFine = Tables.QuantizerFineStepSize[channel.PrecisionsFine[band]];
for (int sb = 0; sb < subBandCount; sb++)
{
double coarse = channel.QuantizedSpectra[subBandIndex + sb] * stepSize;
double fine = channel.QuantizedSpectraFine[subBandIndex + sb] * stepSizeFine;
channel.Spectra[subBandIndex + sb] = coarse + fine;
}
}
public static void ScaleSpectrum(Block block)
{
foreach (Channel channel in block.Channels)
{
ScaleSpectrum(channel);
}
}
private static void ScaleSpectrum(Channel channel)
{
int quantUnitCount = channel.Block.QuantizationUnitCount;
double[] spectra = channel.Spectra;
for (int i = 0; i < quantUnitCount; i++)
{
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
{
spectra[sb] *= Tables.SpectrumScale[channel.ScaleFactors[i]];
}
}
}
}
}
+172
View File
@@ -0,0 +1,172 @@
#nullable disable
using System;
using System.IO;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
internal static class ScaleFactors
{
public static void Read(BitReader reader, Channel channel)
{
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
channel.ScaleFactorCodingMode = reader.ReadInt(2);
if (channel.ChannelIndex == 0)
{
switch (channel.ScaleFactorCodingMode)
{
case 0:
ReadVlcDeltaOffset(reader, channel);
break;
case 1:
ReadClcOffset(reader, channel);
break;
case 2:
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
break;
case 3:
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
break;
}
}
else
{
switch (channel.ScaleFactorCodingMode)
{
case 0:
ReadVlcDeltaOffset(reader, channel);
break;
case 1:
ReadVlcDistanceToBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
break;
case 2:
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
break;
case 3:
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
break;
}
}
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
{
if (channel.ScaleFactors[i] < 0 || channel.ScaleFactors[i] > 31)
{
throw new InvalidDataException("Scale factor values are out of range.");
}
}
Array.Copy(channel.ScaleFactors, channel.ScaleFactorsPrev, channel.ScaleFactors.Length);
}
private static void ReadClcOffset(BitReader reader, Channel channel)
{
const int maxBits = 5;
int[] sf = channel.ScaleFactors;
int bitLength = reader.ReadInt(2) + 2;
int baseValue = bitLength < maxBits ? reader.ReadInt(maxBits) : 0;
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
{
sf[i] = reader.ReadInt(bitLength) + baseValue;
}
}
private static void ReadVlcDeltaOffset(BitReader reader, Channel channel)
{
int weightIndex = reader.ReadInt(3);
byte[] weights = ScaleFactorWeights[weightIndex];
int[] sf = channel.ScaleFactors;
int baseValue = reader.ReadInt(5);
int bitLength = reader.ReadInt(2) + 3;
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
sf[0] = reader.ReadInt(bitLength);
for (int i = 1; i < channel.Block.ExtensionUnit; i++)
{
int delta = Unpack.ReadHuffmanValue(codebook, reader);
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
}
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
{
sf[i] += baseValue - weights[i];
}
}
private static void ReadVlcDistanceToBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
{
int[] sf = channel.ScaleFactors;
int bitLength = reader.ReadInt(2) + 2;
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsSigned[bitLength];
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
for (int i = 0; i < unitCount; i++)
{
int distance = Unpack.ReadHuffmanValue(codebook, reader, true);
sf[i] = (baseline[i] + distance) & 31;
}
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
{
sf[i] = reader.ReadInt(5);
}
}
private static void ReadVlcDeltaOffsetWithBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
{
int[] sf = channel.ScaleFactors;
int baseValue = reader.ReadOffsetBinary(5, BitReader.OffsetBias.Negative);
int bitLength = reader.ReadInt(2) + 1;
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
sf[0] = reader.ReadInt(bitLength);
for (int i = 1; i < unitCount; i++)
{
int delta = Unpack.ReadHuffmanValue(codebook, reader);
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
}
for (int i = 0; i < unitCount; i++)
{
sf[i] += baseValue + baseline[i];
}
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
{
sf[i] = reader.ReadInt(5);
}
}
public static readonly byte[][] ScaleFactorWeights =
{
new byte[] {
0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 10, 12, 12, 12
}, new byte[] {
3, 2, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 7, 10, 10, 10
}, new byte[] {
0, 2, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 9, 12, 12, 12
}, new byte[] {
0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 8, 8, 10, 11, 11, 12, 13, 13, 13, 13
}, new byte[] {
0, 2, 2, 3, 3, 4, 4, 5, 4, 5, 5, 5, 5, 6, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 11, 12, 12, 13, 13, 14, 14, 14
}, new byte[] {
1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 6, 7, 7, 9, 11, 11, 11
}, new byte[] {
0, 5, 8, 10, 11, 11, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12,
12, 13, 15, 15, 15
}, new byte[] {
0, 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13,
15, 15, 15
}
};
}
}
+34
View File
@@ -0,0 +1,34 @@
#nullable disable
namespace LibAtrac9
{
internal static class Stereo
{
public static void ApplyIntensityStereo(Block block)
{
if (block.BlockType != BlockType.Stereo) return;
int totalUnits = block.QuantizationUnitCount;
int stereoUnits = block.StereoQuantizationUnit;
if (stereoUnits >= totalUnits) return;
Channel source = block.PrimaryChannel;
Channel dest = block.SecondaryChannel;
for (int i = stereoUnits; i < totalUnits; i++)
{
int sign = block.JointStereoSigns[i];
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
{
if (sign > 0)
{
dest.Spectra[sb] = -source.Spectra[sb];
}
else
{
dest.Spectra[sb] = source.Spectra[sb];
}
}
}
}
}
}
+116
View File
@@ -0,0 +1,116 @@
#nullable disable
using System;
using static LibAtrac9.HuffmanCodebooks;
namespace LibAtrac9
{
internal static class Tables
{
public static int MaxHuffPrecision(bool highSampleRate) => highSampleRate ? 1 : 7;
public static int MinBandCount(bool highSampleRate) => highSampleRate ? 1 : 3;
public static int MaxExtensionBand(bool highSampleRate) => highSampleRate ? 16 : 18;
public static readonly int[] SampleRates =
{
11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000,
44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000
};
public static readonly byte[] SamplingRateIndexToFrameSamplesPower = { 6, 6, 7, 7, 7, 8, 8, 8, 6, 6, 7, 7, 7, 8, 8, 8 };
// From sampling rate index
public static readonly byte[] MaxBandCount = { 8, 8, 12, 12, 12, 18, 18, 18, 8, 8, 12, 12, 12, 16, 16, 16 };
public static readonly byte[] BandToQuantUnitCount = { 0, 4, 8, 10, 12, 13, 14, 15, 16, 18, 20, 21, 22, 23, 24, 25, 26, 28, 30 };
public static readonly byte[] QuantUnitToCoeffCount =
{
02, 02, 02, 02, 02, 02, 02, 02, 04, 04, 04, 04, 08, 08, 08,
08, 08, 08, 08, 08, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16
};
public static readonly short[] QuantUnitToCoeffIndex =
{
0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56,
64, 72, 80, 88, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256
};
public static readonly byte[] QuantUnitToCodebookIndex =
{
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3
};
public static readonly ChannelConfig[] ChannelConfig =
{
new ChannelConfig(BlockType.Mono),
new ChannelConfig(BlockType.Mono, BlockType.Mono),
new ChannelConfig(BlockType.Stereo),
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo),
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo, BlockType.Stereo),
new ChannelConfig(BlockType.Stereo, BlockType.Stereo)
};
public static readonly HuffmanCodebook[] HuffmanScaleFactorsUnsigned =
GenerateHuffmanCodebooks(HuffmanScaleFactorsACodes, HuffmanScaleFactorsABits, HuffmanScaleFactorsGroupSizes);
public static readonly HuffmanCodebook[] HuffmanScaleFactorsSigned =
GenerateHuffmanCodebooks(HuffmanScaleFactorsBCodes, HuffmanScaleFactorsBBits, HuffmanScaleFactorsGroupSizes);
public static readonly HuffmanCodebook[][][] HuffmanSpectrum =
{
GenerateHuffmanCodebooks(HuffmanSpectrumACodes, HuffmanSpectrumABits, HuffmanSpectrumAGroupSizes),
GenerateHuffmanCodebooks(HuffmanSpectrumBCodes, HuffmanSpectrumBBits, HuffmanSpectrumBGroupSizes)
};
public static readonly double[][] ImdctWindow = { GenerateImdctWindow(6), GenerateImdctWindow(7), GenerateImdctWindow(8) };
public static readonly double[] SpectrumScale = Generate(32, SpectrumScaleFunction);
public static readonly double[] QuantizerStepSize = Generate(16, QuantizerStepSizeFunction);
public static readonly double[] QuantizerFineStepSize = Generate(16, QuantizerFineStepSizeFunction);
public static readonly byte[][] GradientCurves = BitAllocation.GenerateGradientCurves();
private static double QuantizerStepSizeFunction(int x) => 2.0 / ((1 << (x + 1)) - 1);
private static double QuantizerFineStepSizeFunction(int x) => QuantizerStepSizeFunction(x) / ushort.MaxValue;
private static double SpectrumScaleFunction(int x) => Math.Pow(2, x - 15);
private static double[] GenerateImdctWindow(int frameSizePower)
{
int frameSize = 1 << frameSizePower;
var output = new double[frameSize];
double[] a1 = GenerateMdctWindow(frameSizePower);
for (int i = 0; i < frameSize; i++)
{
output[i] = a1[i] / (a1[frameSize - 1 - i] * a1[frameSize - 1 - i] + a1[i] * a1[i]);
}
return output;
}
private static double[] GenerateMdctWindow(int frameSizePower)
{
int frameSize = 1 << frameSizePower;
var output = new double[frameSize];
for (int i = 0; i < frameSize; i++)
{
output[i] = (Math.Sin(((i + 0.5) / frameSize - 0.5) * Math.PI) + 1.0) * 0.5;
}
return output;
}
private static T[] Generate<T>(int count, Func<int, T> elementGenerator)
{
var table = new T[count];
for (int i = 0; i < count; i++)
{
table[i] = elementGenerator(i);
}
return table;
}
}
}
+426
View File
@@ -0,0 +1,426 @@
#nullable disable
using System;
using System.IO;
using LibAtrac9.Utilities;
namespace LibAtrac9
{
internal static class Unpack
{
public static void UnpackFrame(BitReader reader, Frame frame)
{
foreach (Block block in frame.Blocks)
{
UnpackBlock(reader, block);
}
}
private static void UnpackBlock(BitReader reader, Block block)
{
ReadBlockHeader(reader, block);
if (block.BlockType == BlockType.LFE)
{
UnpackLfeBlock(reader, block);
}
else
{
UnpackStandardBlock(reader, block);
}
reader.AlignPosition(8);
}
private static void ReadBlockHeader(BitReader reader, Block block)
{
bool firstInSuperframe = block.Frame.FrameIndex == 0;
block.FirstInSuperframe = !reader.ReadBool();
block.ReuseBandParams = reader.ReadBool();
if (block.FirstInSuperframe != firstInSuperframe)
{
throw new InvalidDataException();
}
if (firstInSuperframe && block.ReuseBandParams && block.BlockType != BlockType.LFE)
{
throw new InvalidDataException();
}
}
private static void UnpackStandardBlock(BitReader reader, Block block)
{
Channel[] channels = block.Channels;
if (!block.ReuseBandParams)
{
ReadBandParams(reader, block);
}
ReadGradientParams(reader, block);
BitAllocation.CreateGradient(block);
ReadStereoParams(reader, block);
ReadExtensionParams(reader, block);
foreach (Channel channel in channels)
{
channel.UpdateCodedUnits();
ScaleFactors.Read(reader, channel);
BitAllocation.CalculateMask(channel);
BitAllocation.CalculatePrecisions(channel);
CalculateSpectrumCodebookIndex(channel);
ReadSpectra(reader, channel);
ReadSpectraFine(reader, channel);
}
block.QuantizationUnitsPrev = block.BandExtensionEnabled ? block.ExtensionUnit : block.QuantizationUnitCount;
}
private static void ReadBandParams(BitReader reader, Block block)
{
int minBandCount = Tables.MinBandCount(block.Config.HighSampleRate);
int maxExtensionBand = Tables.MaxExtensionBand(block.Config.HighSampleRate);
block.BandCount = reader.ReadInt(4);
block.BandCount += minBandCount;
block.QuantizationUnitCount = Tables.BandToQuantUnitCount[block.BandCount];
if (block.BandCount < minBandCount || block.BandCount >
Tables.MaxBandCount[block.Config.SampleRateIndex])
{
return;
}
if (block.BlockType == BlockType.Stereo)
{
block.StereoBand = reader.ReadInt(4);
block.StereoBand += minBandCount;
block.StereoQuantizationUnit = Tables.BandToQuantUnitCount[block.StereoBand];
}
else
{
block.StereoBand = block.BandCount;
}
block.BandExtensionEnabled = reader.ReadBool();
if (block.BandExtensionEnabled)
{
block.ExtensionBand = reader.ReadInt(4);
block.ExtensionBand += minBandCount;
if (block.ExtensionBand < block.BandCount || block.ExtensionBand > maxExtensionBand)
{
throw new InvalidDataException();
}
block.ExtensionUnit = Tables.BandToQuantUnitCount[block.ExtensionBand];
}
else
{
block.ExtensionBand = block.BandCount;
block.ExtensionUnit = block.QuantizationUnitCount;
}
}
private static void ReadGradientParams(BitReader reader, Block block)
{
block.GradientMode = reader.ReadInt(2);
if (block.GradientMode > 0)
{
block.GradientEndUnit = 31;
block.GradientEndValue = 31;
block.GradientStartUnit = reader.ReadInt(5);
block.GradientStartValue = reader.ReadInt(5);
}
else
{
block.GradientStartUnit = reader.ReadInt(6);
block.GradientEndUnit = reader.ReadInt(6) + 1;
block.GradientStartValue = reader.ReadInt(5);
block.GradientEndValue = reader.ReadInt(5);
}
block.GradientBoundary = reader.ReadInt(4);
if (block.GradientBoundary > block.QuantizationUnitCount)
{
throw new InvalidDataException();
}
if (block.GradientStartUnit < 1 || block.GradientStartUnit >= 48)
{
throw new InvalidDataException();
}
if (block.GradientEndUnit < 1 || block.GradientEndUnit >= 48)
{
throw new InvalidDataException();
}
if (block.GradientStartUnit > block.GradientEndUnit)
{
throw new InvalidDataException();
}
if (block.GradientStartValue < 0 || block.GradientStartValue >= 32)
{
throw new InvalidDataException();
}
if (block.GradientEndValue < 0 || block.GradientEndValue >= 32)
{
throw new InvalidDataException();
}
}
private static void ReadStereoParams(BitReader reader, Block block)
{
if (block.BlockType != BlockType.Stereo) return;
block.PrimaryChannelIndex = reader.ReadInt(1);
block.HasJointStereoSigns = reader.ReadBool();
if (block.HasJointStereoSigns)
{
for (int i = block.StereoQuantizationUnit; i < block.QuantizationUnitCount; i++)
{
block.JointStereoSigns[i] = reader.ReadInt(1);
}
}
else
{
Array.Clear(block.JointStereoSigns, 0, block.JointStereoSigns.Length);
}
}
private static void ReadExtensionParams(BitReader reader, Block block)
{
// ReSharper disable once RedundantAssignment
int bexBand = 0;
if (block.BandExtensionEnabled)
{
BandExtension.GetBexBandInfo(out bexBand, out _, out _, block.QuantizationUnitCount);
if (block.BlockType == BlockType.Stereo)
{
ReadHeader(block.Channels[1]);
}
else
{
reader.Position += 1;
}
}
block.HasExtensionData = reader.ReadBool();
if (!block.HasExtensionData) return;
if (!block.BandExtensionEnabled)
{
block.BexMode = reader.ReadInt(2);
block.BexDataLength = reader.ReadInt(5);
reader.Position += block.BexDataLength;
return;
}
ReadHeader(block.Channels[0]);
block.BexDataLength = reader.ReadInt(5);
if (block.BexDataLength <= 0) return;
int bexDataEnd = reader.Position + block.BexDataLength;
ReadData(block.Channels[0]);
if (block.BlockType == BlockType.Stereo)
{
ReadData(block.Channels[1]);
}
// Make sure we didn't read too many bits
if (reader.Position > bexDataEnd)
{
throw new InvalidDataException();
}
void ReadHeader(Channel channel)
{
int bexMode = reader.ReadInt(2);
channel.BexMode = bexBand > 2 ? bexMode : 4;
channel.BexValueCount = BandExtension.BexEncodedValueCounts[channel.BexMode][bexBand];
}
void ReadData(Channel channel)
{
for (int i = 0; i < channel.BexValueCount; i++)
{
int dataLength = BandExtension.BexDataLengths[channel.BexMode][bexBand][i];
channel.BexValues[i] = reader.ReadInt(dataLength);
}
}
}
private static void CalculateSpectrumCodebookIndex(Channel channel)
{
Array.Clear(channel.CodebookSet, 0, channel.CodebookSet.Length);
int quantUnits = channel.CodedQuantUnits;
int[] sf = channel.ScaleFactors;
if (quantUnits <= 1) return;
if (channel.Config.HighSampleRate) return;
// Temporarily setting this value allows for simpler code by
// making the last value a non-special case.
int originalScaleTmp = sf[quantUnits];
sf[quantUnits] = sf[quantUnits - 1];
int avg = 0;
if (quantUnits > 12)
{
for (int i = 0; i < 12; i++)
{
avg += sf[i];
}
avg = (avg + 6) / 12;
}
for (int i = 8; i < quantUnits; i++)
{
int prevSf = sf[i - 1];
int nextSf = sf[i + 1];
int minSf = Math.Min(prevSf, nextSf);
if (sf[i] - minSf >= 3 || sf[i] - prevSf + sf[i] - nextSf >= 3)
{
channel.CodebookSet[i] = 1;
}
}
for (int i = 12; i < quantUnits; i++)
{
if (channel.CodebookSet[i] == 0)
{
int minSf = Math.Min(sf[i - 1], sf[i + 1]);
if (sf[i] - minSf >= 2 && sf[i] >= avg - (Tables.QuantUnitToCoeffCount[i] == 16 ? 1 : 0))
{
channel.CodebookSet[i] = 1;
}
}
}
sf[quantUnits] = originalScaleTmp;
}
private static void ReadSpectra(BitReader reader, Channel channel)
{
int[] values = channel.SpectraValuesBuffer;
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
int maxHuffPrecision = Tables.MaxHuffPrecision(channel.Config.HighSampleRate);
for (int i = 0; i < channel.CodedQuantUnits; i++)
{
int subbandCount = Tables.QuantUnitToCoeffCount[i];
int precision = channel.Precisions[i] + 1;
if (precision <= maxHuffPrecision)
{
HuffmanCodebook huff = Tables.HuffmanSpectrum[channel.CodebookSet[i]][precision][Tables.QuantUnitToCodebookIndex[i]];
int groupCount = subbandCount >> huff.ValueCountPower;
for (int j = 0; j < groupCount; j++)
{
values[j] = ReadHuffmanValue(huff, reader);
}
DecodeHuffmanValues(channel.QuantizedSpectra, Tables.QuantUnitToCoeffIndex[i], subbandCount, huff, values);
}
else
{
int subbandIndex = Tables.QuantUnitToCoeffIndex[i];
for (int j = subbandIndex; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
{
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
}
}
}
}
private static void ReadSpectraFine(BitReader reader, Channel channel)
{
Array.Clear(channel.QuantizedSpectraFine, 0, channel.QuantizedSpectraFine.Length);
for (int i = 0; i < channel.CodedQuantUnits; i++)
{
if (channel.PrecisionsFine[i] > 0)
{
int overflowBits = channel.PrecisionsFine[i] + 1;
int startSubband = Tables.QuantUnitToCoeffIndex[i];
int endSubband = Tables.QuantUnitToCoeffIndex[i + 1];
for (int j = startSubband; j < endSubband; j++)
{
channel.QuantizedSpectraFine[j] = reader.ReadSignedInt(overflowBits);
}
}
}
}
private static void DecodeHuffmanValues(int[] spectrum, int index, int bandCount, HuffmanCodebook huff, int[] values)
{
int valueCount = bandCount >> huff.ValueCountPower;
int mask = (1 << huff.ValueBits) - 1;
for (int i = 0; i < valueCount; i++)
{
int value = values[i];
for (int j = 0; j < huff.ValueCount; j++)
{
spectrum[index++] = Bit.SignExtend32(value & mask, huff.ValueBits);
value >>= huff.ValueBits;
}
}
}
public static int ReadHuffmanValue(HuffmanCodebook huff, BitReader reader, bool signed = false)
{
int code = reader.PeekInt(huff.MaxBitSize);
byte value = huff.Lookup[code];
int bits = huff.Bits[value];
reader.Position += bits;
return signed ? Bit.SignExtend32(value, huff.ValueBits) : value;
}
private static void UnpackLfeBlock(BitReader reader, Block block)
{
Channel channel = block.Channels[0];
block.QuantizationUnitCount = 2;
DecodeLfeScaleFactors(reader, channel);
CalculateLfePrecision(channel);
channel.CodedQuantUnits = block.QuantizationUnitCount;
ReadLfeSpectra(reader, channel);
}
private static void DecodeLfeScaleFactors(BitReader reader, Channel channel)
{
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
for (int i = 0; i < channel.Block.QuantizationUnitCount; i++)
{
channel.ScaleFactors[i] = reader.ReadInt(5);
}
}
private static void CalculateLfePrecision(Channel channel)
{
Block block = channel.Block;
int precision = block.ReuseBandParams ? 8 : 4;
for (int i = 0; i < block.QuantizationUnitCount; i++)
{
channel.Precisions[i] = precision;
channel.PrecisionsFine[i] = 0;
}
}
private static void ReadLfeSpectra(BitReader reader, Channel channel)
{
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
for (int i = 0; i < channel.CodedQuantUnits; i++)
{
if (channel.Precisions[i] <= 0) continue;
int precision = channel.Precisions[i] + 1;
for (int j = Tables.QuantUnitToCoeffIndex[i]; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
{
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
}
}
}
}
}
+23
View File
@@ -0,0 +1,23 @@
#nullable disable
namespace LibAtrac9.Utilities
{
internal static class Bit
{
private static uint BitReverse32(uint value)
{
value = ((value & 0xaaaaaaaa) >> 1) | ((value & 0x55555555) << 1);
value = ((value & 0xcccccccc) >> 2) | ((value & 0x33333333) << 2);
value = ((value & 0xf0f0f0f0) >> 4) | ((value & 0x0f0f0f0f) << 4);
value = ((value & 0xff00ff00) >> 8) | ((value & 0x00ff00ff) << 8);
return (value >> 16) | (value << 16);
}
private static uint BitReverse32(uint value, int bitCount) => BitReverse32(value) >> (32 - bitCount);
public static int BitReverse32(int value, int bitCount) => (int) BitReverse32((uint) value, bitCount);
public static int SignExtend32(int value, int bits)
{
int shift = 8 * sizeof(int) - bits;
return (value << shift) >> shift;
}
}
}
@@ -0,0 +1,133 @@
#nullable disable
using System;
using System.Diagnostics;
namespace LibAtrac9.Utilities
{
internal class BitReader
{
private byte[] Buffer { get; set; }
private int LengthBits { get; set; }
public int Position { get; set; }
private int Remaining => LengthBits - Position;
public BitReader(byte[] buffer) => SetBuffer(buffer);
public void SetBuffer(byte[] buffer)
{
Buffer = buffer;
LengthBits = Buffer?.Length * 8 ?? 0;
Position = 0;
}
public int ReadInt(int bitCount)
{
int value = PeekInt(bitCount);
Position += bitCount;
return value;
}
public int ReadSignedInt(int bitCount)
{
int value = PeekInt(bitCount);
Position += bitCount;
return Bit.SignExtend32(value, bitCount);
}
public bool ReadBool() => ReadInt(1) == 1;
public int ReadOffsetBinary(int bitCount, OffsetBias bias)
{
int offset = (1 << (bitCount - 1)) - (int)bias;
int value = PeekInt(bitCount) - offset;
Position += bitCount;
return value;
}
public void AlignPosition(int multiple)
{
Position = Helpers.GetNextMultiple(Position, multiple);
}
public int PeekInt(int bitCount)
{
Debug.Assert(bitCount >= 0 && bitCount <= 32);
if (bitCount > Remaining)
{
if (Position >= LengthBits) return 0;
int extraBits = bitCount - Remaining;
return PeekIntFallback(Remaining) << extraBits;
}
int byteIndex = Position / 8;
int bitIndex = Position % 8;
if (bitCount <= 9 && Remaining >= 16)
{
int value = Buffer[byteIndex] << 8 | Buffer[byteIndex + 1];
value &= 0xFFFF >> bitIndex;
value >>= 16 - bitCount - bitIndex;
return value;
}
if (bitCount <= 17 && Remaining >= 24)
{
int value = Buffer[byteIndex] << 16 | Buffer[byteIndex + 1] << 8 | Buffer[byteIndex + 2];
value &= 0xFFFFFF >> bitIndex;
value >>= 24 - bitCount - bitIndex;
return value;
}
if (bitCount <= 25 && Remaining >= 32)
{
int value = Buffer[byteIndex] << 24 | Buffer[byteIndex + 1] << 16 | Buffer[byteIndex + 2] << 8 | Buffer[byteIndex + 3];
value &= (int)(0xFFFFFFFF >> bitIndex);
value >>= 32 - bitCount - bitIndex;
return value;
}
return PeekIntFallback(bitCount);
}
private int PeekIntFallback(int bitCount)
{
int value = 0;
int byteIndex = Position / 8;
int bitIndex = Position % 8;
while (bitCount > 0)
{
if (bitIndex >= 8)
{
bitIndex = 0;
byteIndex++;
}
int bitsToRead = Math.Min(bitCount, 8 - bitIndex);
int mask = 0xFF >> bitIndex;
int currentByte = (mask & Buffer[byteIndex]) >> (8 - bitIndex - bitsToRead);
value = (value << bitsToRead) | currentByte;
bitIndex += bitsToRead;
bitCount -= bitsToRead;
}
return value;
}
/// <summary>
/// Specifies the bias of an offset binary value. A positive bias can represent one more
/// positive value than negative value, and a negative bias can represent one more
/// negative value than positive value.
/// </summary>
/// <remarks>Example:
/// A 4-bit offset binary value with a positive bias can store
/// the values 8 through -7 inclusive.
/// A 4-bit offset binary value with a positive bias can store
/// the values 7 through -8 inclusive.</remarks>
public enum OffsetBias
{
Negative = 0
}
}
}
@@ -0,0 +1,51 @@
#nullable disable
using System.Runtime.CompilerServices;
namespace LibAtrac9.Utilities
{
internal static class Helpers
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static short Clamp16(int value)
{
if (value > short.MaxValue)
return short.MaxValue;
if (value < short.MinValue)
return short.MinValue;
return (short)value;
}
public static int GetNextMultiple(int value, int multiple)
{
if (multiple <= 0)
return value;
if (value % multiple == 0)
return value;
return value + multiple - value % multiple;
}
/// <summary>
/// Returns the floor of the base 2 logarithm of a specified number.
/// </summary>
/// <param name="value">The number whose logarithm is to be found.</param>
/// <returns>The floor of the base 2 logarithm of <paramref name="value"/>.</returns>
public static int Log2(int value)
{
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return MultiplyDeBruijnBitPosition[(uint)(value * 0x07C4ACDDU) >> 27];
}
private static readonly int[] MultiplyDeBruijnBitPosition =
{
0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
};
}
}
+178
View File
@@ -0,0 +1,178 @@
#nullable disable
using System;
using System.Collections.Generic;
namespace LibAtrac9.Utilities
{
internal class Mdct
{
private int MdctBits { get; }
private int MdctSize { get; }
private double Scale { get; }
private static readonly object TableLock = new object();
private static int _tableBits = -1;
private static readonly List<double[]> SinTables = new List<double[]>();
private static readonly List<double[]> CosTables = new List<double[]>();
private static readonly List<int[]> ShuffleTables = new List<int[]>();
private readonly double[] _imdctPrevious;
private readonly double[] _imdctWindow;
private readonly double[] _scratchMdct;
private readonly double[] _scratchDct;
public Mdct(int mdctBits, double[] window, double scale = 1)
{
SetTables(mdctBits);
MdctBits = mdctBits;
MdctSize = 1 << mdctBits;
Scale = scale;
if (window.Length < MdctSize)
{
throw new ArgumentException("Window must be as long as the MDCT size.", nameof(window));
}
_imdctPrevious = new double[MdctSize];
_scratchMdct = new double[MdctSize];
_scratchDct = new double[MdctSize];
_imdctWindow = window;
}
private static void SetTables(int maxBits)
{
lock (TableLock)
{
if (maxBits > _tableBits)
{
for (int i = _tableBits + 1; i <= maxBits; i++)
{
GenerateTrigTables(i, out double[] sin, out double[] cos);
SinTables.Add(sin);
CosTables.Add(cos);
ShuffleTables.Add(GenerateShuffleTable(i));
}
_tableBits = maxBits;
}
}
}
public void RunImdct(double[] input, double[] output)
{
if (input.Length < MdctSize)
{
throw new ArgumentException("Input must be as long as the MDCT size.", nameof(input));
}
if (output.Length < MdctSize)
{
throw new ArgumentException("Output must be as long as the MDCT size.", nameof(output));
}
int size = MdctSize;
int half = size / 2;
double[] dctOut = _scratchMdct;
Dct4(input, dctOut);
for (int i = 0; i < half; i++)
{
output[i] = _imdctWindow[i] * dctOut[i + half] + _imdctPrevious[i];
output[i + half] = _imdctWindow[i + half] * -dctOut[size - 1 - i] - _imdctPrevious[i + half];
_imdctPrevious[i] = _imdctWindow[size - 1 - i] * -dctOut[half - i - 1];
_imdctPrevious[i + half] = _imdctWindow[half - i - 1] * dctOut[i];
}
}
/// <summary>
/// Does a Type-4 DCT.
/// </summary>
/// <param name="input">The input array containing the time or frequency-domain samples</param>
/// <param name="output">The output array that will contain the transformed time or frequency-domain samples</param>
private void Dct4(double[] input, double[] output)
{
int[] shuffleTable = ShuffleTables[MdctBits];
double[] sinTable = SinTables[MdctBits];
double[] cosTable = CosTables[MdctBits];
double[] dctTemp = _scratchDct;
int size = MdctSize;
int lastIndex = size - 1;
int halfSize = size / 2;
for (int i = 0; i < halfSize; i++)
{
int i2 = i * 2;
double a = input[i2];
double b = input[lastIndex - i2];
double sin = sinTable[i];
double cos = cosTable[i];
dctTemp[i2] = a * cos + b * sin;
dctTemp[i2 + 1] = a * sin - b * cos;
}
int stageCount = MdctBits - 1;
for (int stage = 0; stage < stageCount; stage++)
{
int blockCount = 1 << stage;
int blockSizeBits = stageCount - stage;
int blockHalfSizeBits = blockSizeBits - 1;
int blockSize = 1 << blockSizeBits;
int blockHalfSize = 1 << blockHalfSizeBits;
sinTable = SinTables[blockHalfSizeBits];
cosTable = CosTables[blockHalfSizeBits];
for (int block = 0; block < blockCount; block++)
{
for (int i = 0; i < blockHalfSize; i++)
{
int frontPos = (block * blockSize + i) * 2;
int backPos = frontPos + blockSize;
double a = dctTemp[frontPos] - dctTemp[backPos];
double b = dctTemp[frontPos + 1] - dctTemp[backPos + 1];
double sin = sinTable[i];
double cos = cosTable[i];
dctTemp[frontPos] += dctTemp[backPos];
dctTemp[frontPos + 1] += dctTemp[backPos + 1];
dctTemp[backPos] = a * cos + b * sin;
dctTemp[backPos + 1] = a * sin - b * cos;
}
}
}
for (int i = 0; i < MdctSize; i++)
{
output[i] = dctTemp[shuffleTable[i]] * Scale;
}
}
internal static void GenerateTrigTables(int sizeBits, out double[] sin, out double[] cos)
{
int size = 1 << sizeBits;
sin = new double[size];
cos = new double[size];
for (int i = 0; i < size; i++)
{
double value = Math.PI * (4 * i + 1) / (4 * size);
sin[i] = Math.Sin(value);
cos[i] = Math.Cos(value);
}
}
internal static int[] GenerateShuffleTable(int sizeBits)
{
int size = 1 << sizeBits;
var table = new int[size];
for (int i = 0; i < size; i++)
{
table[i] = Bit.BitReverse32(i ^ (i / 2), sizeBits);
}
return table;
}
}
}
+150
View File
@@ -0,0 +1,150 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using System.Collections.Specialized;
namespace SharpEmu.GUI;
public sealed class ConsoleWindow : Window
{
private readonly AvaloniaList<LogLine> _sourceLines;
private readonly AvaloniaList<LogLine> _visibleLines = new();
private readonly ListBox _list;
private readonly TextBox _searchBox;
private readonly CheckBox _autoScrollCheck;
public ConsoleWindow(
AvaloniaList<LogLine> lines,
Action clear,
bool autoScroll)
{
_sourceLines = lines;
Title = "SharpEmu Console";
Width = 980;
Height = 620;
MinWidth = 520;
MinHeight = 320;
Background = new SolidColorBrush(Color.Parse("#0D1017"));
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
_searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) };
_autoScrollCheck = new CheckBox
{
Content = "Auto-scroll",
IsChecked = autoScroll,
FontSize = 12,
Margin = new Thickness(0, 0, 12, 0),
VerticalAlignment = VerticalAlignment.Center,
};
var copyButton = new Button
{
Classes = { "ghost" },
Content = "Copy",
Padding = new Thickness(10, 4),
Margin = new Thickness(0, 0, 8, 0),
};
var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) };
copyButton.Click += async (_, _) => await CopyAsync();
clearButton.Click += (_, _) => clear();
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
_list = new ListBox
{
Classes = { "console" },
ItemsSource = _visibleLines,
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
{
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
return text;
}),
};
Content = new Grid
{
Margin = new Thickness(12),
RowDefinitions = new RowDefinitions("Auto,*"),
Children =
{
new Grid
{
Margin = new Thickness(0, 0, 0, 8),
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
Children =
{
new TextBlock
{
Classes = { "sectionTitle" },
Text = "CONSOLE",
VerticalAlignment = VerticalAlignment.Center,
},
_searchBox.WithGridColumn(1),
_autoScrollCheck.WithGridColumn(2),
copyButton.WithGridColumn(3),
clearButton.WithGridColumn(4),
},
},
_list.WithGridRow(1),
},
};
lines.CollectionChanged += OnLinesChanged;
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
RefreshVisibleLines();
}
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshVisibleLines();
if (_autoScrollCheck.IsChecked == true)
{
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
}
}
private void RefreshVisibleLines()
{
var query = _searchBox.Text ?? string.Empty;
_visibleLines.Clear();
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
? _sourceLines
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
}
private async Task CopyAsync()
{
if (_visibleLines.Count == 0 || Clipboard is null)
{
return;
}
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
}
}
file static class GridExtensions
{
public static T WithGridColumn<T>(this T control, int column) where T : Control
{
Grid.SetColumn(control, column);
return control;
}
public static T WithGridRow<T>(this T control, int row) where T : Control
{
Grid.SetRow(control, row);
return control;
}
}
+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.
}
}
}
+637
View File
@@ -0,0 +1,637 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.GUI;
/// <summary>
/// Launches the SharpEmu CLI as a child process with the same CET/CFG mitigation
/// opt-outs the CLI would apply to its own relaunched child, while capturing
/// stdout/stderr through pipes. The CLI's internal relaunch is suppressed via
/// SHARPEMU_DISABLE_MITIGATION_RELAUNCH so output is not lost to a detached
/// console. A kill-on-close job object ties the emulator's lifetime to the GUI.
/// </summary>
internal sealed class EmulatorProcess : IDisposable
{
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
private const uint CREATE_NO_WINDOW = 0x08000000;
private const int STARTF_USESTDHANDLES = 0x00000100;
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
private const uint INFINITE = 0xFFFFFFFF;
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 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 readonly object _sync = new();
private nint _processHandle;
private nint _jobHandle;
private Process? _fallbackProcess;
private bool _running;
private bool _disposed;
public event Action<string, bool>? OutputReceived;
public event Action<int>? Exited;
public bool IsRunning
{
get
{
lock (_sync)
{
return _running;
}
}
}
public void Start(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
{
lock (_sync)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_running)
{
throw new InvalidOperationException("The emulator process is already running.");
}
if (OperatingSystem.IsWindows())
{
StartWindows(exePath, arguments, workingDirectory);
}
else
{
StartFallback(exePath, arguments, workingDirectory);
}
_running = true;
}
}
public void Stop()
{
lock (_sync)
{
if (!_running)
{
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);
}
try
{
_fallbackProcess?.Kill(entireProcessTree: true);
}
catch (InvalidOperationException)
{
// Already exited.
}
}
}
public void Dispose()
{
lock (_sync)
{
if (_disposed)
{
return;
}
_disposed = true;
}
Stop();
}
private void StartWindows(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
{
// The CLI would otherwise relaunch itself into a mitigated child whose
// console output cannot flow through our pipes.
Environment.SetEnvironmentVariable("SHARPEMU_DISABLE_MITIGATION_RELAUNCH", "1");
var securityAttributes = new SECURITY_ATTRIBUTES
{
nLength = Marshal.SizeOf<SECURITY_ATTRIBUTES>(),
bInheritHandle = 1,
};
if (!CreatePipe(out var stdoutRead, out var stdoutWrite, ref securityAttributes, 0) ||
!CreatePipe(out var stderrRead, out var stderrWrite, ref securityAttributes, 0))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create output pipes.");
}
_ = SetHandleInformation(stdoutRead, HANDLE_FLAG_INHERIT, 0);
_ = SetHandleInformation(stderrRead, HANDLE_FLAG_INHERIT, 0);
var startupInfoEx = new STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
startupInfoEx.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
startupInfoEx.StartupInfo.hStdOutput = stdoutWrite;
startupInfoEx.StartupInfo.hStdError = stderrWrite;
nint attributeList = 0;
nint mitigationPolicies = 0;
try
{
nuint attributeListSize = 0;
_ = InitializeProcThreadAttributeList(0, 1, 0, ref attributeListSize);
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to initialize the process attribute list.");
}
startupInfoEx.lpAttributeList = attributeList;
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;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
Marshal.WriteInt64(nint.Add(mitigationPolicies, sizeof(long)), unchecked((long)policy2));
if (!UpdateProcThreadAttribute(
attributeList,
0,
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
mitigationPolicies,
(nuint)(sizeof(ulong) * 2),
0,
0))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to apply the mitigation policy.");
}
var currentDirectory = workingDirectory ?? Environment.CurrentDirectory;
var created = CreateProcessW(
exePath,
new StringBuilder(BuildCommandLine(exePath, arguments)),
0,
0,
true,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
0,
currentDirectory,
ref startupInfoEx,
out var processInfo);
if (!created)
{
var error = Marshal.GetLastWin32Error();
throw new Win32Exception(error, $"Failed to start '{exePath}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
}
CloseHandle(processInfo.hThread);
_processHandle = processInfo.hProcess;
_jobHandle = CreateJobObjectW(0, null);
if (_jobHandle != 0 &&
(!TryEnableKillOnJobClose(_jobHandle) || !AssignProcessToJobObject(_jobHandle, processInfo.hProcess)))
{
CloseHandle(_jobHandle);
_jobHandle = 0;
}
StartReaderThread(stdoutRead, isError: false);
StartReaderThread(stderrRead, isError: true);
StartExitWatcherThread();
}
catch
{
CloseHandle(stdoutRead);
CloseHandle(stderrRead);
throw;
}
finally
{
// The child owns duplicated pipe write ends; closing ours lets the
// readers observe EOF when the child exits.
CloseHandle(stdoutWrite);
CloseHandle(stderrWrite);
if (attributeList != 0)
{
DeleteProcThreadAttributeList(attributeList);
Marshal.FreeHGlobal(attributeList);
}
if (mitigationPolicies != 0)
{
Marshal.FreeHGlobal(mitigationPolicies);
}
}
}
private void StartFallback(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
{
var startInfo = new ProcessStartInfo
{
FileName = exePath,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
foreach (var argument in arguments)
{
startInfo.ArgumentList.Add(argument);
}
var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
process.OutputDataReceived += (_, e) =>
{
if (e.Data is not null)
{
OutputReceived?.Invoke(e.Data, false);
}
};
process.ErrorDataReceived += (_, e) =>
{
if (e.Data is not null)
{
OutputReceived?.Invoke(e.Data, true);
}
};
process.Exited += (_, _) =>
{
int exitCode;
try
{
exitCode = process.ExitCode;
}
catch (InvalidOperationException)
{
exitCode = -1;
}
OnExited(exitCode);
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
_fallbackProcess = process;
}
private void StartReaderThread(nint readHandle, bool isError)
{
var thread = new Thread(() =>
{
using var stream = new FileStream(new SafeFileHandle(readHandle, ownsHandle: true), FileAccess.Read);
using var reader = new StreamReader(stream, Encoding.UTF8);
try
{
while (reader.ReadLine() is { } line)
{
OutputReceived?.Invoke(line, isError);
}
}
catch (IOException)
{
// Pipe broken on process teardown.
}
})
{
IsBackground = true,
Name = isError ? "SharpEmu stderr reader" : "SharpEmu stdout reader",
};
thread.Start();
}
private void StartExitWatcherThread()
{
var processHandle = _processHandle;
var thread = new Thread(() =>
{
_ = WaitForSingleObject(processHandle, INFINITE);
var exitCode = GetExitCodeProcess(processHandle, out var rawExitCode)
? unchecked((int)rawExitCode)
: -1;
OnExited(exitCode);
})
{
IsBackground = true,
Name = "SharpEmu exit watcher",
};
thread.Start();
}
private void OnExited(int exitCode)
{
lock (_sync)
{
if (!_running)
{
return;
}
_running = false;
if (_processHandle != 0)
{
CloseHandle(_processHandle);
_processHandle = 0;
}
if (_jobHandle != 0)
{
CloseHandle(_jobHandle);
_jobHandle = 0;
}
_fallbackProcess?.Dispose();
_fallbackProcess = null;
}
Exited?.Invoke(exitCode);
}
private static bool TryEnableKillOnJobClose(nint jobHandle)
{
var extendedLimitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
},
};
var size = Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
var memory = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(extendedLimitInfo, memory, false);
return SetInformationJobObject(
jobHandle,
JobObjectExtendedLimitInformation,
memory,
unchecked((uint)size));
}
finally
{
Marshal.FreeHGlobal(memory);
}
}
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
{
var builder = new StringBuilder();
builder.Append(QuoteArgument(processPath));
for (var i = 0; i < args.Count; i++)
{
builder.Append(' ');
builder.Append(QuoteArgument(args[i]));
}
return builder.ToString();
}
private static string QuoteArgument(string argument)
{
if (argument.Length == 0)
{
return "\"\"";
}
var needsQuotes = false;
foreach (var c in argument)
{
if (char.IsWhiteSpace(c) || c == '"')
{
needsQuotes = true;
break;
}
}
if (!needsQuotes)
{
return argument;
}
var builder = new StringBuilder(argument.Length + 2);
builder.Append('"');
var backslashCount = 0;
foreach (var c in argument)
{
if (c == '\\')
{
backslashCount++;
continue;
}
if (c == '"')
{
builder.Append('\\', (backslashCount * 2) + 1);
builder.Append('"');
backslashCount = 0;
continue;
}
if (backslashCount > 0)
{
builder.Append('\\', backslashCount);
backslashCount = 0;
}
builder.Append(c);
}
if (backslashCount > 0)
{
builder.Append('\\', backslashCount * 2);
}
builder.Append('"');
return builder.ToString();
}
[StructLayout(LayoutKind.Sequential)]
private struct SECURITY_ATTRIBUTES
{
public int nLength;
public nint lpSecurityDescriptor;
public int bInheritHandle;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public nint lpReserved;
public nint lpDesktop;
public nint lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public nint lpReserved2;
public nint hStdInput;
public nint hStdOutput;
public nint hStdError;
}
[StructLayout(LayoutKind.Sequential)]
private struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public nint lpAttributeList;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public nint hProcess;
public nint hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public uint LimitFlags;
public nuint MinimumWorkingSetSize;
public nuint MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public nint Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
private struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public nuint ProcessMemoryLimit;
public nuint JobMemoryLimit;
public nuint PeakProcessMemoryUsed;
public nuint PeakJobMemoryUsed;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreatePipe(
out nint hReadPipe,
out nint hWritePipe,
ref SECURITY_ATTRIBUTES lpPipeAttributes,
uint nSize);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetHandleInformation(nint hObject, uint dwMask, uint dwFlags);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool InitializeProcThreadAttributeList(
nint lpAttributeList,
int dwAttributeCount,
int dwFlags,
ref nuint lpSize);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UpdateProcThreadAttribute(
nint lpAttributeList,
uint dwFlags,
nint attribute,
nint lpValue,
nuint cbSize,
nint lpPreviousValue,
nint lpReturnSize);
[DllImport("kernel32.dll")]
private static extern void DeleteProcThreadAttributeList(nint lpAttributeList);
[DllImport("kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern nint CreateJobObjectW(nint lpJobAttributes, string? lpName);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetInformationJobObject(
nint hJob,
int jobObjectInfoClass,
nint lpJobObjectInfo,
uint cbJobObjectInfoLength);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AssignProcessToJobObject(nint hJob, nint hProcess);
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessW(
string applicationName,
StringBuilder commandLine,
nint processAttributes,
nint threadAttributes,
[MarshalAs(UnmanagedType.Bool)] bool inheritHandles,
uint creationFlags,
nint environment,
string currentDirectory,
ref STARTUPINFOEX startupInfo,
out PROCESS_INFORMATION processInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetExitCodeProcess(nint process, out uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[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);
}
+156
View File
@@ -0,0 +1,156 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.ComponentModel;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Imaging;
namespace SharpEmu.GUI;
public sealed class GameEntry : INotifyPropertyChanged
{
// Placeholder gradients for games without cover art, picked
// deterministically from the game name so a game keeps its color.
private static readonly (Color Start, Color End)[] PlaceholderPalette =
{
(Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")),
(Color.Parse("#1F6E8C"), Color.Parse("#173B45")),
(Color.Parse("#7A4069"), Color.Parse("#3B1C32")),
(Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")),
(Color.Parse("#8C5425"), Color.Parse("#4A2B12")),
(Color.Parse("#4F6D9E"), Color.Parse("#263349")),
(Color.Parse("#8A4B4B"), Color.Parse("#3F2222")),
(Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")),
};
private Bitmap? _cover;
private IBrush? _placeholderBrush;
private long _sizeBytes;
public GameEntry(
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
{
Name = name;
TitleId = titleId;
Path = path;
_sizeBytes = sizeBytes;
CoverPath = coverPath;
BackgroundPath = backgroundPath;
Initials = ComputeInitials(name);
}
public event PropertyChangedEventHandler? PropertyChanged;
public string Name { get; }
public string? TitleId { get; }
public string Path { get; }
/// <summary>
/// Total size of the game. Initially the eboot's own size from the scan;
/// replaced with the full install folder size once computed in the
/// background.
/// </summary>
public long SizeBytes
{
get => _sizeBytes;
set
{
if (_sizeBytes == value)
{
return;
}
_sizeBytes = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
}
}
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
public string? CoverPath { get; }
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
public string? BackgroundPath { get; }
/// <summary>
/// Decoded key art used as the window backdrop while this game is
/// selected. Loaded on demand and cached; not exposed via binding.
/// </summary>
public Bitmap? Background { get; set; }
public string Initials { get; }
// Built lazily: brushes are AvaloniaObjects that must be created on the
// UI thread, while GameEntry itself is constructed on the scan thread.
public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name);
/// <summary>Decoded cover art; loaded asynchronously after the library scan.</summary>
public Bitmap? Cover
{
get => _cover;
set
{
if (ReferenceEquals(_cover, value))
{
return;
}
_cover = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover)));
}
}
public bool HasCover => _cover is not null;
public string Detail => TitleId is not null
? $"{TitleId} • {FormatSize(SizeBytes)}"
: FormatSize(SizeBytes);
private static string ComputeInitials(string name)
{
var initials = name
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(word => char.IsLetterOrDigit(word[0]))
.Select(word => char.ToUpperInvariant(word[0]))
.Take(2)
.ToArray();
return initials.Length > 0 ? new string(initials) : "?";
}
private static IBrush BuildPlaceholderBrush(string name)
{
var hash = 0;
foreach (var ch in name)
{
hash = unchecked(hash * 31 + ch);
}
var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)];
return new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops =
{
new GradientStop(start, 0),
new GradientStop(end, 1),
},
};
}
private static string FormatSize(long bytes)
{
return bytes switch
{
>= 1L << 30 => $"{bytes / (double)(1L << 30):0.0} GiB",
>= 1L << 20 => $"{bytes / (double)(1L << 20):0.0} MiB",
>= 1L << 10 => $"{bytes / (double)(1L << 10):0.0} KiB",
_ => $"{bytes} B",
};
}
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
namespace SharpEmu.GUI;
/// <summary>
/// Entry point for the desktop frontend, hosted by the SharpEmu executable
/// when it is started without command-line arguments.
/// </summary>
public static class GuiLauncher
{
public static int Run()
{
try
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(Array.Empty<string>());
return 0;
}
catch (Exception ex)
{
WriteCrashLog(ex);
throw;
}
}
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
private static void WriteCrashLog(Exception ex)
{
try
{
File.AppendAllText(
Path.Combine(AppContext.BaseDirectory, "gui-crash.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex}{Environment.NewLine}{Environment.NewLine}");
}
catch (Exception)
{
// Crash logging is best-effort.
}
}
}
+94
View File
@@ -0,0 +1,94 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.GUI;
public sealed class GuiSettings
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = true,
};
public List<string> GameFolders { get; set; } = new();
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
public List<string> ExcludedGames { get; set; } = new();
public string LogLevel { get; set; } = "Info";
public int ImportTraceLimit { get; set; }
public bool StrictDynlibResolution { get; set; }
/// <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>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");
public static GuiSettings Load()
{
try
{
if (File.Exists(SettingsPath))
{
var json = File.ReadAllText(SettingsPath);
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
}
}
catch (Exception)
{
// Corrupt or unreadable settings fall back to defaults.
}
return new GuiSettings();
}
public void Save()
{
try
{
var directory = Path.GetDirectoryName(SettingsPath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(this, SerializerOptions));
}
catch (Exception)
{
// Settings persistence is best-effort.
}
}
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia.Media;
namespace SharpEmu.GUI;
public sealed record LogLine(string Text, IBrush Brush);
+291
View File
@@ -0,0 +1,291 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SharpEmu.GUI.MainWindow"
Title="SharpEmu"
Width="1280" Height="820"
MinWidth="980" MinHeight="640"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgBrush}"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="PreferSystemChrome"
ExtendClientAreaTitleBarHeightHint="44"
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
KeyDown="OnKeyDown">
<Grid RowDefinitions="44,*,32">
<!-- Selected-game backdrop: key art behind the main content, dimmed by
a scrim so tiles and text stay readable. Fades on selection. -->
<Panel Grid.Row="1" ClipToBounds="True">
<Image x:Name="BackdropImage" Stretch="UniformToFill" Opacity="0"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Image.Transitions>
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.35" />
</Transitions>
</Image.Transitions>
</Image>
<Border>
<Border.Background>
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
<GradientStop Offset="0" Color="#730D1017" />
<GradientStop Offset="0.55" Color="#BF0D1017" />
<GradientStop Offset="1" Color="#EB0D1017" />
</LinearGradientBrush>
</Border.Background>
</Border>
</Panel>
<!-- Title bar -->
<Grid x:Name="TitleBar" Grid.Row="0" 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" />
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
<Border Classes="pill">
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
</Grid>
<!-- Main content -->
<Grid Grid.Row="1" Margin="18,14,18,14" 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" />
</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>
</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,Auto,Auto" Margin="16,12,16,8">
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." 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="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"
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
<!-- Launch bar -->
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Selected game cover thumbnail -->
<Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
VerticalAlignment="Center">
<Panel x:Name="SelectedCoverPanel">
<Border Background="{Binding PlaceholderBrush, FallbackValue={x:Null}}"
IsVisible="{Binding !HasCover, FallbackValue=False}">
<TextBlock Text="{Binding Initials}" FontSize="20" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill"
IsVisible="{Binding HasCover, FallbackValue=False}" />
</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" />
<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">
<Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
VerticalAlignment="Center" />
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
VerticalAlignment="Center" />
</StackPanel>
</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 Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log file path" />
<Button x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
VerticalAlignment="Center" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Override log file" />
<ToggleSwitch x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off" />
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Title music" />
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
</StackPanel>
<StackPanel>
<TextBlock Classes="fieldLabel" Text="Discord presence" />
<ToggleSwitch x:Name="DiscordToggle" OnContent="On" OffContent="Off" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
</Grid>
<!-- Status bar -->
<Grid Grid.Row="2" 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" />
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
</Grid>
</Grid>
</Window>
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<!-- Class library: the GUI is hosted by SharpEmu.CLI, which opens it when
the executable is started without arguments. -->
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version>
</PropertyGroup>
<!-- Dependency-free; provides the BuildInfo provenance shown in the
title bar. -->
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" />
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Fonts.Inter" />
<PackageReference Include="Avalonia.Themes.Fluent" />
<PackageReference Include="Tmds.DBus.Protocol" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
</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
SharpEmu.Libs into the launcher. -->
<ItemGroup>
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
</ItemGroup>
</Project>
+301
View File
@@ -0,0 +1,301 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Text;
using LibAtrac9;
namespace SharpEmu.GUI;
/// <summary>
/// Loops a game's sce_sys/snd0.at9 preview music while the game is selected
/// in the library, like the console home screen. The ATRAC9 stream is decoded
/// to WAV on a background task (vendored LibAtrac9); playback uses winmm's
/// PlaySound, so this is Windows-only and a no-op elsewhere.
/// </summary>
internal sealed class SndPreviewPlayer
{
private const uint SND_ASYNC = 0x0001;
private const uint SND_NODEFAULT = 0x0002;
private const uint SND_MEMORY = 0x0004;
private const uint SND_LOOP = 0x0008;
private readonly object _sync = new();
private int _generation;
private GCHandle _pinnedWav;
private bool _playing;
private bool _paused;
private string? _cachedPath;
private byte[]? _cachedWav;
/// <summary>Starts looping the given snd0.at9 after a short debounce.</summary>
public void Play(string at9Path)
{
if (!OperatingSystem.IsWindows())
{
return;
}
int generation;
lock (_sync)
{
generation = ++_generation;
}
_ = Task.Run(async () =>
{
// Debounce so skimming through the library does not decode (or
// start) a preview per tile.
await Task.Delay(300).ConfigureAwait(false);
byte[]? wav;
lock (_sync)
{
if (generation != _generation)
{
return;
}
wav = string.Equals(_cachedPath, at9Path, StringComparison.OrdinalIgnoreCase)
? _cachedWav
: null;
}
if (wav is null)
{
try
{
wav = DecodeAt9ToWav(File.ReadAllBytes(at9Path));
}
catch (Exception)
{
return; // corrupt or unsupported preview: stay silent
}
}
lock (_sync)
{
if (generation != _generation)
{
return;
}
_cachedPath = at9Path;
_cachedWav = wav;
StopLocked();
// The WAV image must stay pinned while winmm plays from it.
_pinnedWav = GCHandle.Alloc(wav, GCHandleType.Pinned);
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
if (!_playing)
{
_pinnedWav.Free();
}
}
});
}
public void Stop()
{
if (!OperatingSystem.IsWindows())
{
return;
}
lock (_sync)
{
_generation++;
StopLocked();
}
}
/// <summary>
/// Silences playback but keeps the decoded track ready, so
/// <see cref="Resume"/> can restart it (winmm cannot truly pause).
/// </summary>
public void Pause()
{
if (!OperatingSystem.IsWindows())
{
return;
}
lock (_sync)
{
if (!_playing)
{
return;
}
_ = PlaySound(0, 0, 0);
_playing = false;
_paused = true;
}
}
/// <summary>Restarts the track silenced by <see cref="Pause"/>.</summary>
public void Resume()
{
if (!OperatingSystem.IsWindows())
{
return;
}
lock (_sync)
{
if (!_paused || !_pinnedWav.IsAllocated)
{
return;
}
_paused = false;
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
}
}
private void StopLocked()
{
_ = PlaySound(0, 0, 0);
_playing = false;
_paused = false;
if (_pinnedWav.IsAllocated)
{
_pinnedWav.Free();
}
}
private static readonly Guid Atrac9SubFormat = new("47E142D2-36BA-4D8D-88FC-61654F8C836C");
/// <summary>
/// Decodes a Sony AT9 (RIFF-wrapped ATRAC9) file to a PCM16 WAV image.
/// Layout per Sony's container: an extensible fmt chunk whose extension
/// carries the 4-byte codec config, a fact chunk with the sample count
/// and encoder delay, and superframes in the data chunk.
/// </summary>
private static byte[] DecodeAt9ToWav(byte[] file)
{
using var reader = new BinaryReader(new MemoryStream(file), Encoding.ASCII);
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF")
{
throw new InvalidDataException("Not a RIFF file.");
}
reader.BaseStream.Position += 4; // riff size
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE")
{
throw new InvalidDataException("Not a WAVE file.");
}
byte[]? configData = null;
var sampleCount = 0;
var encoderDelay = 0;
var dataOffset = -1;
var dataSize = 0;
while (reader.BaseStream.Position + 8 <= reader.BaseStream.Length)
{
var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4));
var chunkSize = reader.ReadInt32();
var chunkStart = reader.BaseStream.Position;
switch (chunkId)
{
case "fmt ":
var formatTag = reader.ReadUInt16();
reader.BaseStream.Position = chunkStart + 24; // to SubFormat GUID
var subFormat = new Guid(reader.ReadBytes(16));
if (formatTag != 0xFFFE || subFormat != Atrac9SubFormat)
{
throw new InvalidDataException("Not an ATRAC9 stream.");
}
reader.BaseStream.Position += 4; // version info
configData = reader.ReadBytes(4);
break;
case "fact":
sampleCount = reader.ReadInt32();
reader.BaseStream.Position += 4; // input overlap delay
encoderDelay = reader.ReadInt32();
break;
case "data":
dataOffset = (int)chunkStart;
dataSize = chunkSize;
break;
}
reader.BaseStream.Position = chunkStart + chunkSize + (chunkSize & 1);
}
if (configData is null || sampleCount <= 0 || dataOffset < 0)
{
throw new InvalidDataException("Missing fmt, fact, or data chunk.");
}
var decoder = new Atrac9Decoder();
decoder.Initialize(configData);
var config = decoder.Config;
var superframeCount = (sampleCount + encoderDelay + config.SuperframeSamples - 1) / config.SuperframeSamples;
superframeCount = Math.Min(superframeCount, dataSize / config.SuperframeBytes);
var channels = config.ChannelCount;
var pcmBuffer = new short[channels][];
for (var i = 0; i < channels; i++)
{
pcmBuffer[i] = new short[config.SuperframeSamples];
}
var wav = new byte[44 + (sampleCount * channels * 2)];
WriteWavHeader(wav, channels, config.SampleRate, sampleCount);
var superframe = new byte[config.SuperframeBytes];
var decodedIndex = 0L; // per-channel, includes the encoder delay
var written = 0;
for (var f = 0; f < superframeCount && written < sampleCount; f++)
{
Buffer.BlockCopy(file, dataOffset + (f * config.SuperframeBytes), superframe, 0, config.SuperframeBytes);
decoder.Decode(superframe, pcmBuffer);
for (var s = 0; s < config.SuperframeSamples && written < sampleCount; s++)
{
if (decodedIndex++ < encoderDelay)
{
continue;
}
var sampleOffset = 44 + ((long)written * channels * 2);
for (var ch = 0; ch < channels; ch++)
{
BinaryPrimitives.WriteInt16LittleEndian(
wav.AsSpan((int)(sampleOffset + (ch * 2))),
pcmBuffer[ch][s]);
}
written++;
}
}
return wav;
}
private static void WriteWavHeader(byte[] wav, int channels, int sampleRate, int sampleCount)
{
var span = wav.AsSpan();
Encoding.ASCII.GetBytes("RIFF").CopyTo(span);
BinaryPrimitives.WriteInt32LittleEndian(span[4..], wav.Length - 8);
Encoding.ASCII.GetBytes("WAVE").CopyTo(span[8..]);
Encoding.ASCII.GetBytes("fmt ").CopyTo(span[12..]);
BinaryPrimitives.WriteInt32LittleEndian(span[16..], 16);
BinaryPrimitives.WriteInt16LittleEndian(span[20..], 1); // PCM
BinaryPrimitives.WriteInt16LittleEndian(span[22..], (short)channels);
BinaryPrimitives.WriteInt32LittleEndian(span[24..], sampleRate);
BinaryPrimitives.WriteInt32LittleEndian(span[28..], sampleRate * channels * 2);
BinaryPrimitives.WriteInt16LittleEndian(span[32..], (short)(channels * 2));
BinaryPrimitives.WriteInt16LittleEndian(span[34..], 16); // bits per sample
Encoding.ASCII.GetBytes("data").CopyTo(span[36..]);
BinaryPrimitives.WriteInt32LittleEndian(span[40..], sampleCount * channels * 2);
}
[DllImport("winmm.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool PlaySound(nint sound, nint module, uint flags);
}
+189
View File
@@ -0,0 +1,189 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Avalonia": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Tmds.DBus.Protocol": {
"type": "Direct",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "11.3.2",
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
},
"Avalonia.FreeDesktop": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
"dependencies": {
"Avalonia": "11.3.18",
"Tmds.DBus.Protocol": "0.21.3"
}
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
},
"Avalonia.Skia": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
"dependencies": {
"Avalonia": "11.3.18",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
}
},
"Avalonia.Win32": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
}
},
"Avalonia.X11": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.FreeDesktop": "11.3.18",
"Avalonia.Skia": "11.3.18"
}
},
"HarfBuzzSharp": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"sharpemu.logging": {
"type": "Project"
}
}
}
}
+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);
}
}
+135 -9
View File
@@ -2,25 +2,20 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.HLE;
public sealed class CpuContext
public sealed class CpuContext(ICpuMemory memory, Generation generation)
{
private readonly ulong[] _registers = new ulong[16];
private readonly ulong[] _xmmRegisters = new ulong[32];
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
private bool _raxWritten;
public CpuContext(ICpuMemory memory, Generation generation)
{
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
TargetGeneration = generation;
}
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
public ICpuMemory Memory { get; }
public Generation TargetGeneration { get; }
public Generation TargetGeneration { get; } = generation;
public ulong Rip { get; set; }
@@ -130,6 +125,91 @@ public sealed class CpuContext
SetYmmUpper(registerIndex, highLow, highHigh);
}
public bool TryReadByte(ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
public bool TryReadUInt16(ulong address, out ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
if (!Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
return true;
}
public bool TryWriteUInt16(ulong address, ushort value)
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
return Memory.TryWrite(address, buffer);
}
public bool TryReadInt32(ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
public bool TryWriteInt32(ulong address, int value, bool checkNil = false)
{
if (checkNil && address == 0)
{
return false;
}
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return Memory.TryWrite(address, bytes);
}
public bool TryReadUInt32(ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
public bool TryWriteUInt32(ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return Memory.TryWrite(address, buffer);
}
public bool TryWriteInt64(ulong address, long value)
{
Span<byte> buffer = stackalloc byte[sizeof(long)];
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
return Memory.TryWrite(address, buffer);
}
public bool TryReadUInt64(ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
@@ -150,6 +230,33 @@ public sealed class CpuContext
return Memory.TryWrite(address, buffer);
}
public bool TryReadNullTerminatedUtf8(ulong address, int capacity, out string value)
{
value = string.Empty;
if (address == 0 || capacity <= 0)
{
return false;
}
var bytes = new byte[capacity];
for (var index = 0; index < bytes.Length; index++)
{
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
{
return false;
}
if (bytes[index] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
public bool PushUInt64(ulong value)
{
var rsp = this[CpuRegister.Rsp];
@@ -169,4 +276,23 @@ public sealed class CpuContext
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
return true;
}
public int SetReturn(int result, Type? cast = null)
{
var value = cast switch
{
null => (ulong)result,
_ when cast == typeof(long) => (ulong)(long)result,
_ => throw new NotSupportedException(),
};
this[CpuRegister.Rax] = unchecked(value);
return result;
}
public int SetReturn(OrbisGen2Result result)
{
this[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
}
+456
View File
@@ -0,0 +1,456 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.HLE;
public readonly record struct GuestThreadStartRequest(
ulong ThreadHandle,
ulong EntryPoint,
ulong Argument,
ulong AttributeAddress,
string Name,
int Priority,
ulong AffinityMask);
public readonly record struct GuestThreadSnapshot(
ulong ThreadHandle,
string Name,
string State,
long ImportCount,
string? LastImportNid,
ulong LastReturnRip,
string? BlockReason);
public interface IGuestThreadScheduler
{
bool SupportsGuestContextTransfer { get; }
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
bool TryJoinThread(
CpuContext callerContext,
ulong threadHandle,
out ulong returnValue,
out string? error);
void Pump(CpuContext callerContext, string reason);
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
bool TryCallGuestFunction(
CpuContext callerContext,
ulong entryPoint,
ulong arg0,
ulong arg1,
ulong stackAddress,
ulong stackSize,
string reason,
out string? error);
bool TryCallGuestContinuation(
CpuContext callerContext,
GuestCpuContinuation continuation,
string reason,
out string? error);
}
public readonly record struct GuestImportCallFrame(
bool IsValid,
ulong ReturnRip,
ulong ResumeRsp,
ulong ReturnSlotAddress);
public readonly record struct GuestCpuContinuation(
ulong Rip,
ulong Rsp,
ulong ReturnSlotAddress,
ulong Rflags,
ulong FsBase,
ulong GsBase,
ulong Rax,
ulong Rcx,
ulong Rdx,
ulong Rbx,
ulong Rbp,
ulong Rsi,
ulong Rdi,
ulong R8,
ulong R9,
ulong R12,
ulong R13,
ulong R14,
ulong R15);
public static class GuestThreadExecution
{
[ThreadStatic]
private static ulong _currentGuestThreadHandle;
[ThreadStatic]
private static ulong _currentFiberAddress;
[ThreadStatic]
private static string? _pendingBlockReason;
[ThreadStatic]
private static bool _pendingBlockContinuationValid;
[ThreadStatic]
private static GuestCpuContinuation _pendingBlockContinuation;
[ThreadStatic]
private static string? _pendingBlockWakeKey;
[ThreadStatic]
private static Func<int>? _pendingBlockResumeHandler;
[ThreadStatic]
private static Func<bool>? _pendingBlockWakeHandler;
[ThreadStatic]
private static long _pendingBlockDeadlineTimestamp;
[ThreadStatic]
private static bool _pendingEntryExit;
[ThreadStatic]
private static ulong _pendingEntryExitValue;
[ThreadStatic]
private static string? _pendingEntryExitReason;
[ThreadStatic]
private static bool _pendingContextTransfer;
[ThreadStatic]
private static GuestCpuContinuation _pendingContextTransferTarget;
[ThreadStatic]
private static bool _hasCurrentImportCallFrame;
[ThreadStatic]
private static ulong _currentImportReturnRip;
[ThreadStatic]
private static ulong _currentImportResumeRsp;
[ThreadStatic]
private static ulong _currentImportReturnSlotAddress;
public static IGuestThreadScheduler? Scheduler { get; set; }
public static bool IsGuestThread => _currentGuestThreadHandle != 0;
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
public static ulong CurrentFiberAddress => _currentFiberAddress;
public static ulong EnterGuestThread(ulong threadHandle)
{
var previous = _currentGuestThreadHandle;
_currentGuestThreadHandle = threadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
_hasCurrentImportCallFrame = false;
_currentImportReturnRip = 0;
_currentImportResumeRsp = 0;
_currentImportReturnSlotAddress = 0;
return previous;
}
public static void RestoreGuestThread(ulong previousThreadHandle)
{
_currentGuestThreadHandle = previousThreadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
_hasCurrentImportCallFrame = false;
_currentImportReturnRip = 0;
_currentImportResumeRsp = 0;
_currentImportReturnSlotAddress = 0;
}
public static ulong EnterFiber(ulong fiberAddress)
{
var previous = _currentFiberAddress;
_currentFiberAddress = fiberAddress;
return previous;
}
public static void RestoreFiber(ulong previousFiberAddress)
{
_currentFiberAddress = previousFiberAddress;
}
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
public static bool RequestCurrentThreadBlock(
CpuContext? context,
string reason,
string? wakeKey = null,
Func<int>? resumeHandler = null,
Func<bool>? wakeHandler = null,
long blockDeadlineTimestamp = 0)
{
if (!IsGuestThread)
{
return false;
}
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
_pendingBlockResumeHandler = resumeHandler;
_pendingBlockWakeHandler = wakeHandler;
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
{
_pendingBlockContinuation = continuation;
_pendingBlockContinuationValid = true;
}
else
{
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
}
return true;
}
public static bool TryConsumeCurrentThreadBlock(out string reason)
{
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out _,
out _,
out _,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out resumeHandler,
out wakeHandler,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler,
out long blockDeadlineTimestamp)
{
reason = _pendingBlockReason ?? string.Empty;
if (string.IsNullOrEmpty(reason))
{
continuation = default;
hasContinuation = false;
wakeKey = string.Empty;
resumeHandler = null;
wakeHandler = null;
blockDeadlineTimestamp = 0;
return false;
}
continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason;
resumeHandler = _pendingBlockResumeHandler;
wakeHandler = _pendingBlockWakeHandler;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null;
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
return true;
}
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
{
if (timeout <= TimeSpan.Zero)
{
return Stopwatch.GetTimestamp();
}
var ticks = timeout.TotalSeconds >= long.MaxValue / (double)Stopwatch.Frequency
? long.MaxValue
: (long)Math.Ceiling(timeout.TotalSeconds * Stopwatch.Frequency);
var now = Stopwatch.GetTimestamp();
if (long.MaxValue - now <= ticks)
{
return long.MaxValue;
}
return now + Math.Max(1, ticks);
}
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
{
if (!TryGetCurrentImportCallFrame(out var frame) ||
frame.ReturnRip < 65536 ||
frame.ResumeRsp == 0 ||
frame.ReturnSlotAddress == 0)
{
continuation = default;
return false;
}
continuation = new GuestCpuContinuation(
frame.ReturnRip,
frame.ResumeRsp,
frame.ReturnSlotAddress,
context.Rflags,
context.FsBase,
context.GsBase,
0,
context[CpuRegister.Rcx],
context[CpuRegister.Rdx],
context[CpuRegister.Rbx],
context[CpuRegister.Rbp],
context[CpuRegister.Rsi],
context[CpuRegister.Rdi],
context[CpuRegister.R8],
context[CpuRegister.R9],
context[CpuRegister.R12],
context[CpuRegister.R13],
context[CpuRegister.R14],
context[CpuRegister.R15]);
return true;
}
public static void RequestCurrentEntryExit(string reason, int status)
{
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
}
public static void RequestCurrentEntryExit(string reason, ulong value)
{
_pendingEntryExit = true;
_pendingEntryExitValue = value;
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
}
public static bool TryConsumeCurrentEntryExit(out ulong value, out string reason)
{
value = _pendingEntryExitValue;
reason = _pendingEntryExitReason ?? string.Empty;
if (!_pendingEntryExit)
{
return false;
}
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
return true;
}
public static void RequestCurrentContextTransfer(GuestCpuContinuation target)
{
_pendingContextTransferTarget = target;
_pendingContextTransfer = true;
}
public static bool TryConsumeCurrentContextTransfer(out GuestCpuContinuation target)
{
target = _pendingContextTransferTarget;
if (!_pendingContextTransfer)
{
return false;
}
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
return true;
}
public static GuestImportCallFrame EnterImportCallFrame(
ulong returnRip,
ulong resumeRsp,
ulong returnSlotAddress)
{
var previous = new GuestImportCallFrame(
_hasCurrentImportCallFrame,
_currentImportReturnRip,
_currentImportResumeRsp,
_currentImportReturnSlotAddress);
_hasCurrentImportCallFrame = true;
_currentImportReturnRip = returnRip;
_currentImportResumeRsp = resumeRsp;
_currentImportReturnSlotAddress = returnSlotAddress;
return previous;
}
public static void RestoreImportCallFrame(GuestImportCallFrame previous)
{
_hasCurrentImportCallFrame = previous.IsValid;
_currentImportReturnRip = previous.ReturnRip;
_currentImportResumeRsp = previous.ResumeRsp;
_currentImportReturnSlotAddress = previous.ReturnSlotAddress;
}
public static bool TryGetCurrentImportCallFrame(out GuestImportCallFrame frame)
{
if (!_hasCurrentImportCallFrame)
{
frame = default;
return false;
}
frame = new GuestImportCallFrame(
true,
_currentImportReturnRip,
_currentImportResumeRsp,
_currentImportReturnSlotAddress);
return true;
}
}
@@ -0,0 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public interface IGuestMemoryAllocator
{
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
}
+9 -2
View File
@@ -3,11 +3,14 @@
using System.Collections.Concurrent;
using System.Reflection;
using SharpEmu.Logging;
namespace SharpEmu.HLE;
public sealed class ModuleManager : IModuleManager
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
@@ -47,6 +50,7 @@ public sealed class ModuleManager : IModuleManager
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue;
}
@@ -105,6 +109,7 @@ public sealed class ModuleManager : IModuleManager
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
{
Log.Warning($"NID '{nid}' not found in dispatch table.");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false;
@@ -112,11 +117,13 @@ public sealed class ModuleManager : IModuleManager
if ((export.Target & context.TargetGeneration) == 0)
{
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
return false;
}
context.ClearRaxWriteFlag();
int ret = ((SysAbiFunction)function).Invoke(context);
+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>
+8
View File
@@ -8,6 +8,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<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"
}
}
}
}
File diff suppressed because it is too large Load Diff
+313
View File
@@ -0,0 +1,313 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
internal enum Gen5ShaderEncoding
{
Sop1,
Sop2,
Sopc,
Sopp,
Sopk,
Smrd,
Smem,
Mubuf,
Mtbuf,
Vop1,
Vop2,
Vopc,
Vop3,
Vintrp,
Ds,
Flat,
Vop3p,
Mimg,
Exp,
}
internal enum Gen5OperandKind
{
ScalarRegister,
VectorRegister,
EncodedConstant,
LiteralConstant,
}
internal enum Gen5ShaderResourceKind
{
ReadOnlyTexture,
ReadWriteTexture,
Sampler,
ConstantBuffer,
}
internal enum Gen5PixelOutputKind
{
Float,
Uint,
Sint,
}
internal enum Gen5SpirvStage
{
Vertex,
Pixel,
Compute,
}
internal sealed record Gen5SpirvShader(
byte[] Spirv,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
IReadOnlyList<Gen5ImageBinding> ImageBindings,
uint AttributeCount,
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
internal readonly record struct Gen5ShaderResourceMapping(
Gen5ShaderResourceKind Kind,
uint Slot,
uint OffsetDwords,
bool SizeFlag);
internal sealed record Gen5ShaderMetadata(
uint ExtendedUserDataSizeDwords,
uint ShaderResourceTableSizeDwords,
IReadOnlyDictionary<uint, uint> DirectResources,
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
internal readonly record struct Gen5ComputeSystemRegisters(
uint? WorkGroupXRegister,
uint? WorkGroupYRegister,
uint? WorkGroupZRegister,
uint? ThreadGroupSizeRegister)
{
public bool TryGetExpression(uint scalarRegister, out string expression)
{
if (WorkGroupXRegister == scalarRegister)
{
expression = "gl_WorkGroupID.x";
return true;
}
if (WorkGroupYRegister == scalarRegister)
{
expression = "gl_WorkGroupID.y";
return true;
}
if (WorkGroupZRegister == scalarRegister)
{
expression = "gl_WorkGroupID.z";
return true;
}
if (ThreadGroupSizeRegister == scalarRegister)
{
expression = "(gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z)";
return true;
}
expression = string.Empty;
return false;
}
public void ClearStaticValues(Span<uint> scalarRegisters)
{
ClearStaticValue(scalarRegisters, WorkGroupXRegister);
ClearStaticValue(scalarRegisters, WorkGroupYRegister);
ClearStaticValue(scalarRegisters, WorkGroupZRegister);
ClearStaticValue(scalarRegisters, ThreadGroupSizeRegister);
}
private static void ClearStaticValue(Span<uint> scalarRegisters, uint? scalarRegister)
{
if (scalarRegister is { } register && register < scalarRegisters.Length)
{
scalarRegisters[(int)register] = 0;
}
}
}
internal sealed record Gen5ShaderState(
Gen5ShaderProgram Program,
IReadOnlyList<uint> UserData,
Gen5ShaderMetadata? Metadata,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
uint UserDataScalarRegisterBase = 0);
internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
{
public static Gen5Operand Scalar(uint index) =>
new(Gen5OperandKind.ScalarRegister, index);
public static Gen5Operand Vector(uint index) =>
new(Gen5OperandKind.VectorRegister, index);
public static Gen5Operand Source(uint encoded, uint? literal = null)
{
if (encoded >= 256)
{
return Vector(encoded - 256);
}
if (encoded is 249 or 255 && literal.HasValue)
{
return new(Gen5OperandKind.LiteralConstant, literal.Value);
}
if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
{
return Scalar(encoded);
}
return new(Gen5OperandKind.EncodedConstant, encoded);
}
public override string ToString() => Kind switch
{
Gen5OperandKind.ScalarRegister => $"s{Value}",
Gen5OperandKind.VectorRegister => $"v{Value}",
Gen5OperandKind.LiteralConstant => $"0x{Value:X8}",
_ => $"src[{Value}]",
};
}
internal abstract record Gen5InstructionControl;
internal sealed record Gen5ImageControl(
uint Dmask,
uint VectorAddress,
IReadOnlyList<uint> AddressRegisters,
uint VectorData,
uint ScalarResource,
uint ScalarSampler,
uint Dimension,
bool IsArray,
bool Glc,
bool Slc) : Gen5InstructionControl
{
public uint GetAddressRegister(int component) =>
component < AddressRegisters.Count
? AddressRegisters[component]
: VectorAddress + (uint)component;
}
internal sealed record Gen5GlobalMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
uint ScalarAddress,
int OffsetBytes,
bool Glc,
bool Slc) : Gen5InstructionControl;
internal sealed record Gen5BufferMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
uint ScalarResource,
int OffsetBytes,
bool IndexEnabled,
bool OffsetEnabled,
bool Glc,
bool Slc) : Gen5InstructionControl;
internal sealed record Gen5ExportControl(
uint Target,
uint EnableMask,
bool Compressed,
bool Done,
bool ValidMask) : Gen5InstructionControl;
internal sealed record Gen5InterpolationControl(
uint Attribute,
uint Channel) : Gen5InstructionControl;
internal sealed record Gen5Vop3Control(
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp,
uint? ScalarDestination) : Gen5InstructionControl;
internal sealed record Gen5SdwaControl(
uint DestinationSelect,
uint Source0Select,
uint Source1Select,
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp) : Gen5InstructionControl;
internal sealed record Gen5DppControl(
uint Control,
bool FetchInactive,
bool BoundControl,
uint AbsoluteMask,
uint NegateMask,
uint BankMask,
uint RowMask) : Gen5InstructionControl;
internal sealed record Gen5ScalarMemoryControl(
uint DestinationCount,
int ImmediateOffsetBytes,
uint? DynamicOffsetRegister) : Gen5InstructionControl;
internal sealed record Gen5DataShareControl(
uint Offset0,
uint Offset1,
bool Gds) : Gen5InstructionControl;
internal sealed record Gen5ImageBinding(
uint Pc,
string Opcode,
Gen5ImageControl Control,
IReadOnlyList<uint> ResourceDescriptor,
IReadOnlyList<uint> SamplerDescriptor,
uint? MipLevel);
internal sealed record Gen5GlobalMemoryBinding(
uint ScalarAddress,
ulong BaseAddress,
IReadOnlyList<uint> InstructionPcs,
byte[] Data);
internal sealed record Gen5VertexInputBinding(
uint Pc,
uint Location,
uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress,
uint Stride,
uint OffsetBytes,
byte[] Data);
internal sealed record Gen5ShaderEvaluation(
IReadOnlyList<uint> InitialScalarRegisters,
IReadOnlyList<uint> ScalarRegisters,
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
IReadOnlyList<Gen5ImageBinding> ImageBindings,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
internal sealed record Gen5ShaderInstruction(
uint Pc,
Gen5ShaderEncoding Encoding,
string Opcode,
IReadOnlyList<uint> Words,
IReadOnlyList<Gen5Operand> Sources,
IReadOnlyList<Gen5Operand> Destinations,
Gen5InstructionControl? Control);
internal sealed record Gen5ShaderProgram(
ulong Address,
IReadOnlyList<Gen5ShaderInstruction> Instructions)
{
public IEnumerable<Gen5ImageControl> ImageResources =>
Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5ImageControl>();
}
@@ -0,0 +1,124 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Agc;
internal static class Gen5ShaderMetadataReader
{
private const ulong ShaderUserDataOffset = 0x08;
private const int ResourceClassCount = 4;
private const int MaxMetadataEntries = 4096;
public static bool TryRead(
CpuContext ctx,
ulong shaderHeaderAddress,
out Gen5ShaderMetadata metadata)
{
metadata = default!;
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
userDataAddress == 0 ||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
{
return false;
}
var resourceOffsets = new ulong[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
if (!ctx.TryReadUInt64(
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
out resourceOffsets[resourceClass]))
{
return false;
}
}
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
directResourceCount > MaxMetadataEntries)
{
return false;
}
var resourceCounts = new ushort[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
if (!ctx.TryReadUInt16(
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
out resourceCounts[resourceClass]) ||
resourceCounts[resourceClass] > MaxMetadataEntries)
{
return false;
}
}
var directResources = new Dictionary<uint, uint>();
if (directResourceCount != 0)
{
if (directResourceOffsetsAddress == 0)
{
return false;
}
for (uint type = 0; type < directResourceCount; type++)
{
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
{
return false;
}
if (offset != ushort.MaxValue)
{
directResources[type] = offset;
}
}
}
var resources = new List<Gen5ShaderResourceMapping>();
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
var count = resourceCounts[resourceClass];
if (count == 0)
{
continue;
}
if (resourceOffsets[resourceClass] == 0)
{
return false;
}
for (uint slot = 0; slot < count; slot++)
{
if (!ctx.TryReadUInt16(
resourceOffsets[resourceClass] + slot * sizeof(ushort),
out var sharp))
{
return false;
}
var offset = (uint)(sharp & 0x7FFF);
if (offset == 0x7FFF)
{
continue;
}
resources.Add(new Gen5ShaderResourceMapping(
(Gen5ShaderResourceKind)resourceClass,
slot,
offset,
(sharp & 0x8000) != 0));
}
}
metadata = new Gen5ShaderMetadata(
extendedUserDataSize,
shaderResourceTableSize,
directResources,
resources);
return true;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+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,
};
}
}
+167
View File
@@ -0,0 +1,167 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
internal static class SpirvFixedShaders
{
public static byte[] CreateFullscreenVertex(uint attributeCount)
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
var voidType = module.TypeVoid();
var boolType = module.TypeBool();
var uintType = module.TypeInt(32, signed: false);
var floatType = module.TypeFloat(32);
var vec4Type = module.TypeVector(floatType, 4);
var inputUintPointer = module.TypePointer(SpirvStorageClass.Input, uintType);
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
var vertexIndex = module.AddGlobalVariable(inputUintPointer, SpirvStorageClass.Input);
module.AddName(vertexIndex, "vertexIndex");
module.AddDecoration(
vertexIndex,
SpirvDecoration.BuiltIn,
(uint)SpirvBuiltIn.VertexIndex);
var position = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(position, "position");
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
var attributes = new uint[attributeCount];
for (uint index = 0; index < attributeCount; index++)
{
attributes[index] =
module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(attributes[index], $"attr{index}");
module.AddDecoration(attributes[index], SpirvDecoration.Location, index);
module.AddDecoration(attributes[index], SpirvDecoration.NoPerspective);
}
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddName(main, "main");
module.AddLabel();
var indexValue = module.AddInstruction(SpirvOp.Load, uintType, vertexIndex);
var one = module.Constant(uintType, 1);
var two = module.Constant(uintType, 2);
var shifted = module.AddInstruction(SpirvOp.ShiftLeftLogical, uintType, indexValue, one);
var xBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, shifted, two);
var yBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, indexValue, two);
var x = module.AddInstruction(SpirvOp.ConvertUToF, floatType, xBits);
var y = module.AddInstruction(SpirvOp.ConvertUToF, floatType, yBits);
var zero = module.ConstantFloat(floatType, 0f);
var oneFloat = module.ConstantFloat(floatType, 1f);
var twoFloat = module.ConstantFloat(floatType, 2f);
var xPosition = module.AddInstruction(SpirvOp.FMul, floatType, x, twoFloat);
xPosition = module.AddInstruction(SpirvOp.FSub, floatType, xPosition, oneFloat);
var yPosition = module.AddInstruction(SpirvOp.FMul, floatType, y, twoFloat);
yPosition = module.AddInstruction(SpirvOp.FSub, floatType, yPosition, oneFloat);
var positionValue = module.AddInstruction(
SpirvOp.CompositeConstruct,
vec4Type,
xPosition,
yPosition,
zero,
oneFloat);
module.AddStatement(SpirvOp.Store, position, positionValue);
var attributeValue = module.AddInstruction(
SpirvOp.CompositeConstruct,
vec4Type,
x,
y,
zero,
oneFloat);
foreach (var attribute in attributes)
{
module.AddStatement(SpirvOp.Store, attribute, attributeValue);
}
module.AddStatement(SpirvOp.Return);
module.EndFunction();
var interfaces = new uint[2 + attributes.Length];
interfaces[0] = vertexIndex;
interfaces[1] = position;
attributes.CopyTo(interfaces, 2);
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", interfaces);
_ = boolType;
return module.Build();
}
public static byte[] CreateCopyFragment()
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
var voidType = module.TypeVoid();
var floatType = module.TypeFloat(32);
var vec2Type = module.TypeVector(floatType, 2);
var vec4Type = module.TypeVector(floatType, 4);
var inputVec4Pointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
var imageType = module.TypeImage(
floatType,
SpirvImageDim.Dim2D,
depth: false,
arrayed: false,
multisampled: false,
sampled: 1,
SpirvImageFormat.Unknown);
var sampledImageType = module.TypeSampledImage(imageType);
var sampledImagePointer =
module.TypePointer(SpirvStorageClass.UniformConstant, sampledImageType);
var attribute = module.AddGlobalVariable(inputVec4Pointer, SpirvStorageClass.Input);
module.AddName(attribute, "attr0");
module.AddDecoration(attribute, SpirvDecoration.Location, 0);
var texture = module.AddGlobalVariable(
sampledImagePointer,
SpirvStorageClass.UniformConstant);
module.AddName(texture, "tex0");
module.AddDecoration(texture, SpirvDecoration.DescriptorSet, 0);
module.AddDecoration(texture, SpirvDecoration.Binding, 1);
var output = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(output, "outColor");
module.AddDecoration(output, SpirvDecoration.Location, 0);
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddName(main, "main");
module.AddLabel();
var attributeValue = module.AddInstruction(SpirvOp.Load, vec4Type, attribute);
var coordinates = module.AddInstruction(
SpirvOp.VectorShuffle,
vec2Type,
attributeValue,
attributeValue,
0,
1);
var sampledImage = module.AddInstruction(SpirvOp.Load, sampledImageType, texture);
var lod = module.ConstantFloat(floatType, 0f);
var color = module.AddInstruction(
SpirvOp.ImageSampleExplicitLod,
vec4Type,
sampledImage,
coordinates,
2,
lod);
module.AddStatement(SpirvOp.Store, output, color);
module.AddStatement(SpirvOp.Return);
module.EndFunction();
module.AddEntryPoint(
SpirvExecutionModel.Fragment,
main,
"main",
[attribute, texture, output]);
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
return module.Build();
}
}
+885
View File
@@ -0,0 +1,885 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.Libs.Agc;
internal enum SpirvOp : ushort
{
Nop = 0,
Name = 5,
Extension = 10,
ExtInstImport = 11,
ExtInst = 12,
MemoryModel = 14,
EntryPoint = 15,
ExecutionMode = 16,
Capability = 17,
TypeVoid = 19,
TypeBool = 20,
TypeInt = 21,
TypeFloat = 22,
TypeVector = 23,
TypeImage = 25,
TypeSampler = 26,
TypeSampledImage = 27,
TypeArray = 28,
TypeRuntimeArray = 29,
TypeStruct = 30,
TypePointer = 32,
TypeFunction = 33,
ConstantTrue = 41,
ConstantFalse = 42,
Constant = 43,
ConstantComposite = 44,
ConstantNull = 46,
Function = 54,
FunctionParameter = 55,
FunctionEnd = 56,
FunctionCall = 57,
Variable = 59,
Load = 61,
Store = 62,
AccessChain = 65,
ArrayLength = 68,
Decorate = 71,
VectorExtractDynamic = 77,
VectorInsertDynamic = 78,
VectorShuffle = 79,
CompositeConstruct = 80,
CompositeExtract = 81,
CompositeInsert = 82,
CopyObject = 83,
SampledImage = 86,
ImageSampleImplicitLod = 87,
ImageSampleExplicitLod = 88,
ImageSampleDrefImplicitLod = 89,
ImageSampleDrefExplicitLod = 90,
ImageFetch = 95,
ImageGather = 96,
ImageDrefGather = 97,
ImageRead = 98,
ImageWrite = 99,
Image = 100,
ImageQuerySizeLod = 103,
ImageQuerySize = 104,
ImageQueryLod = 105,
ImageQueryLevels = 106,
ImageQuerySamples = 107,
ConvertFToU = 109,
ConvertFToS = 110,
ConvertSToF = 111,
ConvertUToF = 112,
UConvert = 113,
SConvert = 114,
FConvert = 115,
Bitcast = 124,
SNegate = 126,
FNegate = 127,
IAdd = 128,
FAdd = 129,
ISub = 130,
FSub = 131,
IMul = 132,
FMul = 133,
UDiv = 134,
SDiv = 135,
FDiv = 136,
UMod = 137,
SRem = 138,
SMod = 139,
FRem = 140,
FMod = 141,
IAddCarry = 149,
ISubBorrow = 150,
UMulExtended = 151,
SMulExtended = 152,
Any = 154,
All = 155,
IsNan = 156,
IsInf = 157,
LogicalEqual = 164,
LogicalNotEqual = 165,
LogicalOr = 166,
LogicalAnd = 167,
LogicalNot = 168,
Select = 169,
IEqual = 170,
INotEqual = 171,
UGreaterThan = 172,
SGreaterThan = 173,
UGreaterThanEqual = 174,
SGreaterThanEqual = 175,
ULessThan = 176,
SLessThan = 177,
ULessThanEqual = 178,
SLessThanEqual = 179,
FOrdEqual = 180,
FUnordEqual = 181,
FOrdNotEqual = 182,
FUnordNotEqual = 183,
FOrdLessThan = 184,
FUnordLessThan = 185,
FOrdGreaterThan = 186,
FUnordGreaterThan = 187,
FOrdLessThanEqual = 188,
FUnordLessThanEqual = 189,
FOrdGreaterThanEqual = 190,
FUnordGreaterThanEqual = 191,
ShiftRightLogical = 194,
ShiftRightArithmetic = 195,
ShiftLeftLogical = 196,
BitwiseOr = 197,
BitwiseXor = 198,
BitwiseAnd = 199,
Not = 200,
BitFieldInsert = 201,
BitFieldSExtract = 202,
BitFieldUExtract = 203,
BitReverse = 204,
BitCount = 205,
ControlBarrier = 224,
MemoryBarrier = 225,
AtomicIAdd = 234,
Phi = 245,
LoopMerge = 246,
SelectionMerge = 247,
Label = 248,
Branch = 249,
BranchConditional = 250,
Switch = 251,
Kill = 252,
Return = 253,
ReturnValue = 254,
Unreachable = 255,
GroupNonUniformElect = 333,
GroupNonUniformAll = 334,
GroupNonUniformAny = 335,
GroupNonUniformAllEqual = 336,
GroupNonUniformBroadcast = 337,
GroupNonUniformBroadcastFirst = 338,
GroupNonUniformBallot = 339,
GroupNonUniformShuffle = 345,
GroupNonUniformShuffleXor = 346,
GroupNonUniformShuffleUp = 347,
GroupNonUniformShuffleDown = 348,
}
internal enum SpirvCapability : uint
{
Shader = 1,
Float16 = 9,
Float64 = 10,
Int64 = 11,
Int16 = 22,
ImageGatherExtended = 25,
StorageImageExtendedFormats = 49,
ImageQuery = 50,
StorageImageReadWithoutFormat = 55,
StorageImageWriteWithoutFormat = 56,
GroupNonUniform = 61,
GroupNonUniformVote = 62,
GroupNonUniformBallot = 64,
GroupNonUniformShuffle = 65,
RuntimeDescriptorArray = 5302,
}
internal enum SpirvStorageClass : uint
{
UniformConstant = 0,
Input = 1,
Uniform = 2,
Output = 3,
Workgroup = 4,
Private = 6,
Function = 7,
PushConstant = 9,
Image = 11,
StorageBuffer = 12,
}
internal enum SpirvExecutionModel : uint
{
Vertex = 0,
Fragment = 4,
GLCompute = 5,
}
internal enum SpirvExecutionMode : uint
{
OriginUpperLeft = 7,
DepthReplacing = 12,
LocalSize = 17,
}
internal enum SpirvDecoration : uint
{
Block = 2,
ArrayStride = 6,
BuiltIn = 11,
NoPerspective = 13,
Flat = 14,
Location = 30,
Binding = 33,
DescriptorSet = 34,
Offset = 35,
}
internal enum SpirvBuiltIn : uint
{
Position = 0,
VertexIndex = 42,
InstanceIndex = 43,
FragCoord = 15,
FrontFacing = 17,
WorkgroupId = 26,
LocalInvocationId = 27,
GlobalInvocationId = 28,
LocalInvocationIndex = 29,
SubgroupLocalInvocationId = 41,
}
internal enum SpirvImageDim : uint
{
Dim1D = 0,
Dim2D = 1,
Dim3D = 2,
Cube = 3,
Buffer = 5,
}
internal enum SpirvImageFormat : uint
{
Unknown = 0,
Rgba32f = 1,
Rgba16f = 2,
R32f = 3,
Rgba8 = 4,
Rgba8Snorm = 5,
Rg32f = 6,
Rg16f = 7,
R11fG11fB10f = 8,
R16f = 9,
Rgba16 = 10,
Rgb10A2 = 11,
Rg16 = 12,
Rg8 = 13,
R16 = 14,
R8 = 15,
Rgba16Snorm = 16,
Rg16Snorm = 17,
Rg8Snorm = 18,
R16Snorm = 19,
R8Snorm = 20,
Rgba32i = 21,
Rgba16i = 22,
Rgba8i = 23,
R32i = 24,
Rg32i = 25,
Rg16i = 26,
Rg8i = 27,
R16i = 28,
R8i = 29,
Rgba32ui = 30,
Rgba16ui = 31,
Rgba8ui = 32,
R32ui = 33,
Rgb10A2ui = 34,
Rg32ui = 35,
Rg16ui = 36,
Rg8ui = 37,
R16ui = 38,
R8ui = 39,
}
internal sealed class SpirvModuleBuilder
{
private const uint Magic = 0x07230203;
private const uint Version15 = 0x00010500;
private const uint Generator = 0x53504500; // "SPE"
private readonly List<uint> _capabilities = [];
private readonly List<uint> _extensions = [];
private readonly List<uint> _imports = [];
private readonly List<uint> _memoryModel = [];
private readonly List<uint> _entryPoints = [];
private readonly List<uint> _executionModes = [];
private readonly List<uint> _debug = [];
private readonly List<uint> _annotations = [];
private readonly List<uint> _typesConstantsGlobals = [];
private readonly List<uint> _functions = [];
private readonly Dictionary<(uint Width, bool Signed), uint> _integerTypes = [];
private readonly Dictionary<uint, uint> _floatTypes = [];
private readonly Dictionary<(uint Component, uint Count), uint> _vectorTypes = [];
private readonly Dictionary<
(
uint SampledType,
SpirvImageDim Dimension,
bool Depth,
bool Arrayed,
bool Multisampled,
uint Sampled,
SpirvImageFormat Format
),
uint> _imageTypes = [];
private readonly Dictionary<uint, uint> _sampledImageTypes = [];
private readonly Dictionary<(SpirvStorageClass Storage, uint Type), uint> _pointerTypes = [];
private readonly Dictionary<(uint Element, uint Count), uint> _arrayTypes = [];
private readonly Dictionary<uint, uint> _runtimeArrayTypes = [];
private readonly Dictionary<string, uint> _functionTypes = [];
private readonly Dictionary<(uint Type, ulong Value), uint> _constants = [];
private readonly HashSet<SpirvCapability> _declaredCapabilities = [];
private readonly Dictionary<string, uint> _extInstImports = [];
private uint _nextId = 1;
private uint? _voidType;
private uint? _boolType;
public uint AllocateId() => _nextId++;
public void AddCapability(SpirvCapability capability)
{
if (_declaredCapabilities.Add(capability))
{
Emit(_capabilities, SpirvOp.Capability, (uint)capability);
}
}
public void AddExtension(string extension) =>
EmitWithString(_extensions, SpirvOp.Extension, [], extension);
public uint ImportExtInst(string name)
{
if (_extInstImports.TryGetValue(name, out var existing))
{
return existing;
}
var id = AllocateId();
EmitWithString(_imports, SpirvOp.ExtInstImport, [id], name);
_extInstImports.Add(name, id);
return id;
}
public void SetLogicalGlsl450MemoryModel() =>
Emit(_memoryModel, SpirvOp.MemoryModel, 0, 1);
public void AddEntryPoint(
SpirvExecutionModel model,
uint function,
string name,
IReadOnlyList<uint> interfaces)
{
var prefix = new uint[2 + interfaces.Count];
prefix[0] = (uint)model;
prefix[1] = function;
for (var index = 0; index < interfaces.Count; index++)
{
prefix[index + 2] = interfaces[index];
}
EmitWithString(_entryPoints, SpirvOp.EntryPoint, prefix, name, stringBeforeTailCount: 2);
}
public void AddExecutionMode(uint function, SpirvExecutionMode mode, params uint[] operands)
{
var values = new uint[2 + operands.Length];
values[0] = function;
values[1] = (uint)mode;
operands.CopyTo(values, 2);
Emit(_executionModes, SpirvOp.ExecutionMode, values);
}
public void AddName(uint target, string name) =>
EmitWithString(_debug, SpirvOp.Name, [target], name);
public void AddDecoration(uint target, SpirvDecoration decoration, params uint[] operands)
{
var values = new uint[2 + operands.Length];
values[0] = target;
values[1] = (uint)decoration;
operands.CopyTo(values, 2);
Emit(_annotations, SpirvOp.Decorate, values);
}
public void AddMemberDecoration(
uint target,
uint member,
SpirvDecoration decoration,
params uint[] operands)
{
var values = new uint[3 + operands.Length];
values[0] = target;
values[1] = member;
values[2] = (uint)decoration;
operands.CopyTo(values, 3);
EmitRaw(_annotations, 72, values);
}
public uint TypeVoid()
{
if (_voidType is { } existing)
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeVoid, id);
_voidType = id;
return id;
}
public uint TypeBool()
{
if (_boolType is { } existing)
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeBool, id);
_boolType = id;
return id;
}
public uint TypeInt(uint width, bool signed)
{
var key = (width, signed);
if (_integerTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeInt, id, width, signed ? 1u : 0u);
_integerTypes.Add(key, id);
return id;
}
public uint TypeFloat(uint width)
{
if (_floatTypes.TryGetValue(width, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeFloat, id, width);
_floatTypes.Add(width, id);
return id;
}
public uint TypeVector(uint componentType, uint count)
{
var key = (componentType, count);
if (_vectorTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeVector, id, componentType, count);
_vectorTypes.Add(key, id);
return id;
}
public uint TypeImage(
uint sampledType,
SpirvImageDim dimension,
bool depth,
bool arrayed,
bool multisampled,
uint sampled,
SpirvImageFormat format)
{
var key = (
sampledType,
dimension,
depth,
arrayed,
multisampled,
sampled,
format);
if (_imageTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
SpirvOp.TypeImage,
id,
sampledType,
(uint)dimension,
depth ? 1u : 0u,
arrayed ? 1u : 0u,
multisampled ? 1u : 0u,
sampled,
(uint)format);
_imageTypes.Add(key, id);
return id;
}
public uint TypeSampledImage(uint imageType)
{
if (_sampledImageTypes.TryGetValue(imageType, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeSampledImage, id, imageType);
_sampledImageTypes.Add(imageType, id);
return id;
}
public uint TypeArray(uint elementType, uint count)
{
var key = (elementType, count);
if (_arrayTypes.TryGetValue(key, out var existing))
{
return existing;
}
var length = Constant(TypeInt(32, false), count);
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeArray, id, elementType, length);
_arrayTypes.Add(key, id);
return id;
}
public uint TypeRuntimeArray(uint elementType)
{
if (_runtimeArrayTypes.TryGetValue(elementType, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeRuntimeArray, id, elementType);
_runtimeArrayTypes.Add(elementType, id);
return id;
}
public uint TypeStruct(params uint[] memberTypes)
{
var id = AllocateId();
var operands = new uint[memberTypes.Length + 1];
operands[0] = id;
memberTypes.CopyTo(operands, 1);
Emit(_typesConstantsGlobals, SpirvOp.TypeStruct, operands);
return id;
}
public uint TypePointer(SpirvStorageClass storageClass, uint type)
{
var key = (storageClass, type);
if (_pointerTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypePointer, id, (uint)storageClass, type);
_pointerTypes.Add(key, id);
return id;
}
public uint TypeFunction(uint returnType, params uint[] parameterTypes)
{
var key = returnType + ":" + string.Join(',', parameterTypes);
if (_functionTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
var operands = new uint[parameterTypes.Length + 2];
operands[0] = id;
operands[1] = returnType;
parameterTypes.CopyTo(operands, 2);
Emit(_typesConstantsGlobals, SpirvOp.TypeFunction, operands);
_functionTypes.Add(key, id);
return id;
}
public uint ConstantBool(bool value)
{
var type = TypeBool();
var key = (type, value ? 1UL : 0UL);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
value ? SpirvOp.ConstantTrue : SpirvOp.ConstantFalse,
type,
id);
_constants.Add(key, id);
return id;
}
public uint Constant(uint type, uint value)
{
var key = (type, (ulong)value);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.Constant, type, id, value);
_constants.Add(key, id);
return id;
}
public uint Constant64(uint type, ulong value)
{
var key = (type, value);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
SpirvOp.Constant,
type,
id,
(uint)value,
(uint)(value >> 32));
_constants.Add(key, id);
return id;
}
public uint ConstantFloat(uint type, float value) =>
Constant(type, BitConverter.SingleToUInt32Bits(value));
public uint ConstantComposite(uint type, params uint[] constituents)
{
var id = AllocateId();
var operands = new uint[constituents.Length + 2];
operands[0] = type;
operands[1] = id;
constituents.CopyTo(operands, 2);
Emit(_typesConstantsGlobals, SpirvOp.ConstantComposite, operands);
return id;
}
public uint ConstantNull(uint type)
{
var key = (type, ulong.MaxValue);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.ConstantNull, type, id);
_constants.Add(key, id);
return id;
}
public uint AddGlobalVariable(
uint pointerType,
SpirvStorageClass storageClass,
uint? initializer = null)
{
var id = AllocateId();
if (initializer.HasValue)
{
Emit(
_typesConstantsGlobals,
SpirvOp.Variable,
pointerType,
id,
(uint)storageClass,
initializer.Value);
}
else
{
Emit(_typesConstantsGlobals, SpirvOp.Variable, pointerType, id, (uint)storageClass);
}
return id;
}
public uint BeginFunction(uint returnType, uint functionType)
{
var id = AllocateId();
Emit(_functions, SpirvOp.Function, returnType, id, 0, functionType);
return id;
}
public uint AddFunctionParameter(uint type) =>
EmitResult(_functions, SpirvOp.FunctionParameter, type);
public uint AddLabel(uint? id = null)
{
var result = id ?? AllocateId();
Emit(_functions, SpirvOp.Label, result);
return result;
}
public uint AddFunctionVariable(uint pointerType, uint? initializer = null)
{
var id = AllocateId();
if (initializer.HasValue)
{
Emit(
_functions,
SpirvOp.Variable,
pointerType,
id,
(uint)SpirvStorageClass.Function,
initializer.Value);
}
else
{
Emit(
_functions,
SpirvOp.Variable,
pointerType,
id,
(uint)SpirvStorageClass.Function);
}
return id;
}
public uint AddInstruction(SpirvOp opcode, uint resultType, params uint[] operands) =>
EmitResult(_functions, opcode, resultType, operands);
public void AddStatement(SpirvOp opcode, params uint[] operands) =>
Emit(_functions, opcode, operands);
public void EndFunction() => Emit(_functions, SpirvOp.FunctionEnd);
public byte[] Build()
{
if (_memoryModel.Count == 0)
{
SetLogicalGlsl450MemoryModel();
}
var wordCount =
5 +
_capabilities.Count +
_extensions.Count +
_imports.Count +
_memoryModel.Count +
_entryPoints.Count +
_executionModes.Count +
_debug.Count +
_annotations.Count +
_typesConstantsGlobals.Count +
_functions.Count;
var words = new uint[wordCount];
var offset = 0;
WriteWord(Magic);
WriteWord(Version15);
WriteWord(Generator);
WriteWord(_nextId);
WriteWord(0);
WriteSection(_capabilities);
WriteSection(_extensions);
WriteSection(_imports);
WriteSection(_memoryModel);
WriteSection(_entryPoints);
WriteSection(_executionModes);
WriteSection(_debug);
WriteSection(_annotations);
WriteSection(_typesConstantsGlobals);
WriteSection(_functions);
var bytes = new byte[wordCount * sizeof(uint)];
Buffer.BlockCopy(words, 0, bytes, 0, bytes.Length);
return bytes;
void WriteWord(uint value)
{
words[offset++] = value;
}
void WriteSection(List<uint> section)
{
foreach (var value in section)
{
WriteWord(value);
}
}
}
private uint EmitResult(
List<uint> section,
SpirvOp opcode,
uint resultType,
params uint[] operands)
{
var result = AllocateId();
var values = new uint[operands.Length + 2];
values[0] = resultType;
values[1] = result;
operands.CopyTo(values, 2);
Emit(section, opcode, values);
return result;
}
private static void Emit(List<uint> section, SpirvOp opcode, params uint[] operands) =>
EmitRaw(section, (ushort)opcode, operands);
private static void EmitRaw(List<uint> section, ushort opcode, params uint[] operands)
{
section.Add(((uint)(operands.Length + 1) << 16) | opcode);
section.AddRange(operands);
}
private static void EmitWithString(
List<uint> section,
SpirvOp opcode,
IReadOnlyList<uint> prefix,
string value,
int stringBeforeTailCount = -1)
{
var encoded = EncodeString(value);
if (stringBeforeTailCount < 0)
{
var operands = new uint[prefix.Count + encoded.Length];
for (var index = 0; index < prefix.Count; index++)
{
operands[index] = prefix[index];
}
encoded.CopyTo(operands, prefix.Count);
Emit(section, opcode, operands);
return;
}
var result = new uint[prefix.Count + encoded.Length];
for (var index = 0; index < stringBeforeTailCount; index++)
{
result[index] = prefix[index];
}
encoded.CopyTo(result, stringBeforeTailCount);
for (var index = stringBeforeTailCount; index < prefix.Count; index++)
{
result[index + encoded.Length] = prefix[index];
}
Emit(section, opcode, result);
}
private static uint[] EncodeString(string value)
{
var byteCount = Encoding.UTF8.GetByteCount(value) + 1;
var words = new uint[(byteCount + 3) / 4];
Encoding.UTF8.GetBytes(value, System.Runtime.InteropServices.MemoryMarshal.AsBytes(words.AsSpan()));
return words;
}
}
+954
View File
@@ -0,0 +1,954 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Ampr;
public static class AmprExports
{
private const int CommandBufferHeaderSize = 0x28;
private const ulong CommandBufferSelfOffset = 0x00;
private const ulong CommandBufferDataOffset = 0x08;
private const ulong CommandBufferSizeOffset = 0x10;
private const ulong CommandBufferAux0Offset = 0x18;
private const ulong CommandBufferAux1Offset = 0x20;
private const ulong ReadFileRecordSize = 0x30;
private const ulong KernelEventQueueRecordSize = 0x30;
private const ulong WriteAddressRecordSize = 0x20;
private const uint ReadFileRecordType = 1;
private const uint KernelEventQueueRecordType = 2;
private const uint WriteAddressRecordType = 3;
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
private static readonly ConcurrentDictionary<string, Lazy<CachedHostFile>> _hostFileCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly bool _traceAmpr =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
private static readonly bool _traceAmprReads =
_traceAmpr ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
private sealed class CommandBufferState
{
public ulong Buffer;
public ulong Size;
public ulong WriteOffset;
}
private sealed class CachedHostFile
{
public CachedHostFile(string path)
{
Stream = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete,
bufferSize: 1024 * 1024,
FileOptions.RandomAccess);
}
public object Gate { get; } = new();
public FileStream Stream { get; }
}
[SysAbiExport(
Nid = "8aI7R7WaOlc",
ExportName = "sceAmprCommandBufferConstructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferConstructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var buffer = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0: 0, aux1: 0, clear: true))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "ctor", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = commandBuffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "a8uLzYY--tM",
ExportName = "sceAmprAprCommandBufferConstructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferConstructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var aux0 = ctx[CpuRegister.Rsi];
var aux1 = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "apr_ctor", commandBuffer, aux0, aux1);
ctx[CpuRegister.Rax] = commandBuffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Qs1xtplKo0U",
ExportName = "sceAmprAprCommandBufferDestructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferDestructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
auxiliaryPointers.Clear();
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "apr_dtor", commandBuffer, 0, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GuchCTefuZw",
ExportName = "sceAmprCommandBufferDestructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferDestructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_commandBuffers.TryRemove(commandBuffer, out _);
TraceAmpr(ctx, "dtor", commandBuffer, 0, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "N-FSPA4S3nI",
ExportName = "sceAmprCommandBufferSetBuffer",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferSetBuffer(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var buffer = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "set_buffer", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "baQO9ez2gL4",
ExportName = "sceAmprCommandBufferReset",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferReset(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
if (!ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, bufferPointers))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var buffer = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers);
var size = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers[sizeof(ulong)..]);
if (
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "reset", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ULvXMDz56po",
ExportName = "sceAmprCommandBufferClearBuffer",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferClearBuffer(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out var size, out _) ||
!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_commandBuffers.TryRemove(commandBuffer, out _);
TraceAmpr(ctx, "clear_buffer", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = buffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "mQ16-QdKv7k",
ExportName = "sceAmprAprCommandBufferReadFile",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferReadFile(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var fileId = unchecked((uint)ctx[CpuRegister.Rcx]);
var destination = ctx[CpuRegister.R8];
var size = ctx[CpuRegister.R9];
if (commandBuffer == 0 || (destination == 0 && size != 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out var fileOffset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
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))
{
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;
}
// 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;
}
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_OK);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vWU-odnS+fU",
ExportName = "sceAmprMeasureCommandSizeReadFile",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeReadFile(CpuContext ctx)
{
TraceAmpr(ctx, "measure_read_file", 0, ReadFileRecordSize, 0);
ctx[CpuRegister.Rax] = ReadFileRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sSAUCCU1dv4",
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWriteKernelEventQueue0400(CpuContext ctx)
{
TraceAmpr(ctx, "measure_write_equeue", 0, KernelEventQueueRecordSize, 0);
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "C+IEj+BsAFM",
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWriteAddressOnCompletion(CpuContext ctx)
{
TraceAmpr(ctx, "measure_write_address_complete", 0, WriteAddressRecordSize, 0);
ctx[CpuRegister.Rax] = WriteAddressRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "tZDDEo2tE5k",
ExportName = "sceAmprCommandBufferGetSize",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferGetSize(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out var size, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "get_size", commandBuffer, size, 0);
ctx[CpuRegister.Rax] = size;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GnxKOHEawhk",
ExportName = "sceAmprCommandBufferGetCurrentOffset",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferGetCurrentOffset(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferOffset(ctx, commandBuffer, out var offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "get_offset", commandBuffer, offset, 0);
ctx[CpuRegister.Rax] = offset;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "H896Pt-yB4I",
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWriteKernelEventQueue0400(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var equeue = ctx[CpuRegister.Rsi];
var ident = ctx[CpuRegister.Rdx];
var completionToken = ctx[CpuRegister.Rcx];
var userData = ctx[CpuRegister.R8];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!AppendKernelEventQueueRecord(
ctx,
commandBuffer,
equeue,
ident,
completionToken,
userData))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_equeue", commandBuffer, ident, completionToken);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sJXyWHjP-F8",
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWriteAddressOnCompletion(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var address = ctx[CpuRegister.Rsi];
var value = ctx[CpuRegister.Rdx];
if (commandBuffer == 0 || address == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!AppendWriteAddressRecord(ctx, commandBuffer, address, value))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_address_complete", commandBuffer, address, value);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
{
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out _, out var state) || state is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong writeOffset;
lock (state)
{
writeOffset = state.WriteOffset;
}
var offset = 0UL;
while (offset < writeOffset)
{
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
switch (recordType)
{
case ReadFileRecordType:
offset += ReadFileRecordSize;
break;
case KernelEventQueueRecordType:
if (!CompleteKernelEventQueueRecord(ctx, buffer + offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
offset += KernelEventQueueRecordSize;
break;
case WriteAddressRecordType:
if (!CompleteWriteAddressRecord(ctx, buffer + offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
offset += WriteAddressRecordSize;
break;
default:
TraceAmpr(ctx, "complete_unknown", commandBuffer, recordType, offset);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
TraceAmpr(ctx, "complete", commandBuffer, buffer, writeOffset);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool InitializeCommandBuffer(
CpuContext ctx,
ulong commandBuffer,
ulong buffer,
ulong size,
ulong aux0,
ulong aux1,
bool clear)
{
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
if (clear)
{
header.Clear();
}
else
{
if (!ctx.Memory.TryRead(commandBuffer, header))
{
return false;
}
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
}
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux0Offset..], aux0);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux1Offset..], aux1);
if (!ctx.Memory.TryWrite(commandBuffer, header))
{
return false;
}
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset: 0);
return true;
}
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
{
return WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
}
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size, ulong writeOffset)
{
if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer, size))
{
return false;
}
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset);
return true;
}
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
{
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 3];
BinaryPrimitives.WriteUInt64LittleEndian(pointers, commandBuffer);
BinaryPrimitives.WriteUInt64LittleEndian(pointers[sizeof(ulong)..], buffer);
BinaryPrimitives.WriteUInt64LittleEndian(pointers[(sizeof(ulong) * 2)..], size);
return ctx.Memory.TryWrite(commandBuffer + CommandBufferSelfOffset, pointers);
}
private static void UpdateCommandBufferState(
ulong commandBuffer,
ulong buffer,
ulong size,
ulong writeOffset)
{
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state)
{
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = writeOffset;
}
}
private static bool TryGetCommandBufferState(
CpuContext ctx,
ulong commandBuffer,
out ulong buffer,
out ulong size,
out CommandBufferState? state)
{
if (_commandBuffers.TryGetValue(commandBuffer, out state))
{
lock (state)
{
buffer = state.Buffer;
size = state.Size;
}
return true;
}
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
{
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state)
{
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = 0;
}
return true;
}
buffer = 0;
size = 0;
state = null;
return false;
}
private static bool TryGetCommandBufferOffset(CpuContext ctx, ulong commandBuffer, out ulong offset)
{
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
offset = 0;
return false;
}
lock (state)
{
offset = state.WriteOffset;
}
return true;
}
private static int TryReadFileToGuestMemory(
CpuContext ctx,
string hostPath,
ulong fileOffset,
ulong destination,
ulong size,
out ulong bytesRead)
{
bytesRead = 0;
if (size == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (fileOffset > long.MaxValue)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
const int ChunkSize = 1024 * 1024;
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ChunkSize, size));
try
{
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
{
return openResult;
}
long fileLength;
lock (cachedFile.Gate)
{
fileLength = cachedFile.Stream.Length;
}
if (fileOffset >= (ulong)fileLength)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
while (bytesRead < size)
{
if (bytesRead > ulong.MaxValue - fileOffset)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var absoluteOffset = fileOffset + bytesRead;
if (absoluteOffset > long.MaxValue)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
int read;
lock (cachedFile.Gate)
{
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
read = cachedFile.Stream.Read(buffer, 0, request);
}
if (read <= 0)
{
break;
}
if (!ctx.Memory.TryWrite(destination + bytesRead, buffer.AsSpan(0, read)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
bytesRead += (ulong)read;
}
}
catch (UnauthorizedAccessException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryGetCachedHostFile(string hostPath, out CachedHostFile file, out int result)
{
file = null!;
result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
string cachePath;
try
{
cachePath = Path.GetFullPath(hostPath);
}
catch
{
cachePath = hostPath;
}
var lazy = _hostFileCache.GetOrAdd(
cachePath,
static path => new Lazy<CachedHostFile>(() => new CachedHostFile(path), isThreadSafe: true));
try
{
file = lazy.Value;
return true;
}
catch (UnauthorizedAccessException)
{
_hostFileCache.TryRemove(cachePath, out _);
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
return false;
}
catch (IOException)
{
_hostFileCache.TryRemove(cachePath, out _);
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false;
}
}
private static bool AppendReadFileRecord(
CpuContext ctx,
ulong commandBuffer,
uint fileId,
ulong destination,
ulong size,
ulong fileOffset,
ulong bytesRead)
{
Span<byte> record = stackalloc byte[(int)ReadFileRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], ReadFileRecordType);
BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], fileId);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], destination);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], size);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], fileOffset);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], bytesRead);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendKernelEventQueueRecord(
CpuContext ctx,
ulong commandBuffer,
ulong equeue,
ulong ident,
ulong completionToken,
ulong userData)
{
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
BinaryPrimitives.WriteInt16LittleEndian(record[0x04..], KernelEventQueueCompatExports.KernelEventFilterAmpr);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], completionToken);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendWriteAddressRecord(CpuContext ctx, ulong commandBuffer, ulong address, ulong value)
{
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], WriteAddressRecordType);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], address);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], value);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendCommandBufferRecord(CpuContext ctx, ulong commandBuffer, ReadOnlySpan<byte> record)
{
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
return false;
}
var recordSize = (ulong)record.Length;
lock (state)
{
if (state.Buffer == 0 ||
state.WriteOffset > state.Size ||
recordSize > state.Size - state.WriteOffset)
{
return false;
}
if (!ctx.Memory.TryWrite(state.Buffer + state.WriteOffset, record))
{
return false;
}
state.WriteOffset += recordSize;
}
return true;
}
private static bool CompleteKernelEventQueueRecord(CpuContext ctx, ulong recordAddress)
{
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
if (!ctx.Memory.TryRead(recordAddress, record))
{
return false;
}
var filter = unchecked((short)BinaryPrimitives.ReadUInt32LittleEndian(record[0x04..]));
var equeue = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
var ident = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
var userData = BinaryPrimitives.ReadUInt64LittleEndian(record[0x18..]);
var data = BinaryPrimitives.ReadUInt64LittleEndian(record[0x20..]);
var extra = BinaryPrimitives.ReadUInt64LittleEndian(record[0x28..]);
var queuedEvent = new KernelEventQueueCompatExports.KernelQueuedEvent(
ident,
filter,
0x20,
unchecked((uint)extra),
data,
userData);
_ = KernelEventQueueCompatExports.EnqueueEvent(equeue, queuedEvent);
TraceAmpr(ctx, "complete_equeue", equeue, ident, data);
return true;
}
private static bool CompleteWriteAddressRecord(CpuContext ctx, ulong recordAddress)
{
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
if (!ctx.Memory.TryRead(recordAddress, record))
{
return false;
}
var address = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
var value = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
if (!ctx.TryWriteUInt64(address, value))
{
return false;
}
TraceAmpr(ctx, "complete_write_address", address, value, 0);
return true;
}
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
{
if (!_traceAmpr)
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] ampr.{operation}: cmd=0x{commandBuffer:X16} arg0=0x{arg0:X16} arg1=0x{arg1:X16} ret=0x{returnRip:X16}");
}
private static void TraceAmprRead(
CpuContext ctx,
ulong commandBuffer,
uint fileId,
ulong destination,
ulong size,
ulong fileOffset,
ulong bytesRead,
string? hostPath,
int result)
{
if (!_traceAmprReads)
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] ampr.read_file: cmd=0x{commandBuffer:X16} id=0x{fileId:X8} dst=0x{destination:X16} size=0x{size:X16} offset=0x{fileOffset:X16} read=0x{bytesRead:X16} result=0x{result:X8} path='{hostPath ?? string.Empty}' ret=0x{returnRip:X16}");
}
}
@@ -0,0 +1,40 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Ampr;
internal static class AmprFileRegistry
{
private static readonly ConcurrentDictionary<uint, string> _hostPathsById = new();
public static uint Register(string guestPath, string hostPath)
{
var id = ComputeFileId(guestPath);
_hostPathsById[id] = hostPath;
return id;
}
public static bool TryGetHostPath(uint id, out string hostPath)
{
return _hostPathsById.TryGetValue(id, out hostPath!);
}
internal static uint ComputeFileId(string guestPath)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
const uint offsetBasis = 2166136261;
const uint prime = 16777619;
var hash = offsetBasis;
foreach (var b in bytes)
{
hash ^= b;
hash *= prime;
}
return hash;
}
}
@@ -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;
}
}
@@ -2,11 +2,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Text.Json;
namespace SharpEmu.Libs.AppContent;
public static class AppContentExports
{
private const ulong BootParamAttrOffset = 4;
private const string Temp0MountPoint = "/temp0";
private const uint AppParamSkuFlag = 0;
private const int AppParamSkuFlagFull = 3;
[SysAbiExport(
Nid = "R9lA82OraNs",
ExportName = "sceAppContentInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentInitialize(CpuContext ctx)
{
var initParamAddress = ctx[CpuRegister.Rdi];
var bootParamAddress = ctx[CpuRegister.Rsi];
if (initParamAddress == 0 || bootParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> attrBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(attrBytes, 0);
if (!ctx.Memory.TryWrite(bootParamAddress + BootParamAttrOffset, attrBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "xnd8BJzAxmk",
ExportName = "sceAppContentGetAddcontInfoList",
@@ -14,7 +47,159 @@ public static class AppContentExports
LibraryName = "libSceAppContent")]
public static int AppContentGetAddcontInfoList(CpuContext ctx)
{
_ = ctx;
var hitCountAddress = ctx[CpuRegister.Rcx];
if (hitCountAddress != 0)
{
Span<byte> hitCountBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(hitCountBytes, 0);
if (!ctx.Memory.TryWrite(hitCountAddress, hitCountBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "99b82IKXpH4",
ExportName = "sceAppContentAppParamGetInt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentAppParamGetInt(CpuContext ctx)
{
var paramId = (uint)ctx[CpuRegister.Rdi];
var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
int value;
if (paramId == AppParamSkuFlag)
{
value = AppParamSkuFlagFull;
}
else if (!TryReadUserDefinedParam(paramId, out value))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAppContent($"app_param_get_int id={paramId} value={value}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "buYbeLOGWmA",
ExportName = "sceAppContentTemporaryDataMount2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentTemporaryDataMount2(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rsi];
if (mountPointAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Directory.CreateDirectory(ResolveTemp0Root());
var mountPointBytes = Encoding.ASCII.GetBytes($"{Temp0MountPoint}\0");
if (!ctx.Memory.TryWrite(mountPointAddress, mountPointBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryReadUserDefinedParam(uint paramId, out int value)
{
value = 0;
if (paramId is < 1 or > 4)
{
return false;
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return true;
}
var paramJsonPath = Path.Combine(app0Root, "sce_sys", "param.json");
if (!File.Exists(paramJsonPath))
{
return true;
}
try
{
using var stream = File.OpenRead(paramJsonPath);
using var document = JsonDocument.Parse(stream);
var propertyName = $"userDefinedParam{paramId}";
if (document.RootElement.TryGetProperty(propertyName, out var element) &&
element.TryGetInt32(out var parsedValue))
{
value = parsedValue;
}
return true;
}
catch (IOException)
{
return true;
}
catch (UnauthorizedAccessException)
{
return true;
}
catch (JsonException)
{
return true;
}
}
private static void TraceAppContent(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] app_content.{message}");
}
private static string ResolveTemp0Root()
{
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName);
if (!string.IsNullOrWhiteSpace(configuredRoot))
{
return Path.GetFullPath(configuredRoot);
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
var appName = string.IsNullOrWhiteSpace(app0Root)
? "default"
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
if (string.IsNullOrWhiteSpace(appName))
{
appName = "default";
}
var invalidChars = Path.GetInvalidFileNameChars();
appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
var root = Path.Combine(Path.GetTempPath(), "SharpEmu", appName, "temp0");
Environment.SetEnvironmentVariable(temp0VariableName, root);
return root;
}
}
+215
View File
@@ -0,0 +1,215 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Audio;
public static class AudioOut2Exports
{
// 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 static long _nextContextHandle = 1;
private static long _nextUserHandle = 1;
private static int _nextPortId;
[SysAbiExport(
Nid = "g2tViFIohHE",
ExportName = "sceAudioOut2Initialize",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2Initialize(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "t5YrizufpQc",
ExportName = "sceAudioOut2ContextResetParam",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextResetParam(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
param.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(param[0x00..], AudioOut2ContextParamSize);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
return ctx.Memory.TryWrite(paramAddress, param)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "pDmme7Bgm6E",
ExportName = "sceAudioOut2ContextQueryMemory",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || outMemorySizeAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "0x6o1VVAYSY",
ExportName = "sceAudioOut2ContextCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextCreate(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var memoryAddress = ctx[CpuRegister.Rsi];
var memorySize = ctx[CpuRegister.Rdx];
var outContextAddress = ctx[CpuRegister.Rcx];
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
return ctx.TryWriteUInt64(outContextAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "on6ZH7Abo10",
ExportName = "sceAudioOut2ContextDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "JK2wamZPzwM",
ExportName = "sceAudioOut2PortCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortCreate(CpuContext ctx)
{
var type = unchecked((int)ctx[CpuRegister.Rdi]);
var paramAddress = ctx[CpuRegister.Rsi];
var outPortAddress = ctx[CpuRegister.Rdx];
var contextAddress = ctx[CpuRegister.Rcx];
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return ctx.TryWriteUInt64(outPortAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "gatEUKG+Ea4",
ExportName = "sceAudioOut2PortGetState",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortGetState(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var stateAddress = ctx[CpuRegister.Rsi];
if (handle == 0 || stateAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var type = (int)((handle >> 16) & 0xFF);
Span<byte> state = stackalloc byte[0x20];
state.Clear();
var output = type == 2 ? 0x40 : 0x01;
var channels = type == 2 ? 1 : 2;
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], unchecked((ushort)output));
state[0x02] = unchecked((byte)channels);
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
return ctx.Memory.TryWrite(stateAddress, state)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "DImz2Ft9E2g",
ExportName = "sceAudioOut2GetSpeakerInfo",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2GetSpeakerInfo(CpuContext ctx)
{
var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> info = stackalloc byte[0x40];
info.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
return ctx.Memory.TryWrite(infoAddress, info)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "cd+Rtw+D1x8",
ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "IaZXJ9M79uo",
ExportName = "sceAudioOut2UserDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "xywYcRB7nbQ",
ExportName = "sceAudioOut2UserCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserCreate(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return ctx.TryWriteUInt64(outUserAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
+234
View File
@@ -0,0 +1,234 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace SharpEmu.Libs.Audio;
public static class AudioOutExports
{
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
private static int _nextPortHandle;
private sealed class PortState : IDisposable
{
private readonly object _paceGate = new();
private long _nextSilentOutput;
public PortState(
int userId,
int type,
uint bufferLength,
uint frequency,
int format,
int channels,
int bytesPerSample,
bool isFloat,
WinMmAudioPort? backend)
{
UserId = userId;
Type = type;
BufferLength = bufferLength;
Frequency = frequency;
Format = format;
Channels = channels;
BytesPerSample = bytesPerSample;
IsFloat = isFloat;
Backend = backend;
}
public int UserId { get; }
public int Type { get; }
public uint BufferLength { get; }
public uint Frequency { get; }
public int Format { get; }
public int Channels { get; }
public int BytesPerSample { get; }
public bool IsFloat { get; }
public WinMmAudioPort? Backend { get; }
public int BufferByteLength =>
checked((int)BufferLength * Channels * BytesPerSample);
public void PaceSilence()
{
long delay;
lock (_paceGate)
{
var now = Stopwatch.GetTimestamp();
if (_nextSilentOutput < now)
{
_nextSilentOutput = now;
}
delay = _nextSilentOutput - now;
_nextSilentOutput += checked(
(long)Math.Ceiling(
Stopwatch.Frequency * (double)BufferLength / Frequency));
}
if (delay > 0)
{
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
}
}
public void Dispose() => Backend?.Dispose();
}
[SysAbiExport(
Nid = "JfEPXVxhFqA",
ExportName = "sceAudioOutInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutInit(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "ekNvsT22rsY",
ExportName = "sceAudioOutOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutOpen(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var bufferLength = unchecked((uint)ctx[CpuRegister.Rcx]);
var frequency = unchecked((uint)ctx[CpuRegister.R8]);
var format = unchecked((int)ctx[CpuRegister.R9]);
if (bufferLength == 0 || frequency == 0 ||
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
WinMmAudioPort? backend = null;
string backendName;
try
{
backend = new WinMmAudioPort(frequency);
backendName = "winmm";
}
catch (Exception exception)
{
backendName = "silent";
Console.Error.WriteLine(
$"[LOADER][WARN] AudioOut host backend unavailable: {exception.Message}");
}
var handle = Interlocked.Increment(ref _nextPortHandle);
Ports[handle] = new PortState(
userId,
type,
bufferLength,
frequency,
format,
channels,
bytesPerSample,
isFloat,
backend);
Console.Error.WriteLine(
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
$"{bufferLength} frames, backend={backendName}");
return ctx.SetReturn(handle);
}
[SysAbiExport(
Nid = "s1--uE9mBFw",
ExportName = "sceAudioOutClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Ports.TryRemove(handle, out var port))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
port.Dispose();
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "QOQtbeDqsT4",
ExportName = "sceAudioOutOutput",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutOutput(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var sourceAddress = ctx[CpuRegister.Rsi];
if (!Ports.TryGetValue(handle, out var port))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (sourceAddress == 0)
{
return ctx.SetReturn(0);
}
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
try
{
var source = buffer.AsSpan(0, port.BufferByteLength);
if (!ctx.Memory.TryRead(sourceAddress, source))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (port.Backend is null ||
!port.Backend.Submit(
source,
port.BufferLength,
port.Channels,
port.BytesPerSample,
port.IsFloat))
{
port.PaceSilence();
}
return ctx.SetReturn(0);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
[SysAbiExport(
Nid = "b+uAV89IlxE",
ExportName = "sceAudioOutSetVolume",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutSetVolume(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return ctx.SetReturn(
Ports.ContainsKey(handle)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
private static bool TryGetFormat(
int rawFormat,
out int channels,
out int bytesPerSample,
out bool isFloat)
{
var format = rawFormat & 0xFF;
channels = format switch
{
0 or 3 => 1,
1 or 4 => 2,
2 or 5 or 6 or 7 => 8,
_ => 0,
};
bytesPerSample = format is >= 3 and <= 5 or 7 ? 4 : 2;
isFloat = bytesPerSample == 4;
return channels != 0;
}
}
+302
View File
@@ -0,0 +1,302 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Audio;
internal sealed class WinMmAudioPort : IDisposable
{
private const uint WaveMapper = uint.MaxValue;
private const uint CallbackEvent = 0x0005_0000;
private const ushort WaveFormatPcm = 1;
private const uint WaveHeaderDone = 0x0000_0001;
private const int MaximumQueuedPcmBytes = 32 * 1024;
private readonly object _gate = new();
private readonly AutoResetEvent _completion = new(false);
private readonly Queue<NativeBuffer> _buffers = new();
private IntPtr _device;
private int _queuedPcmBytes;
private bool _disposed;
public WinMmAudioPort(uint sampleRate)
{
if (!OperatingSystem.IsWindows())
{
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
}
var format = new WaveFormat
{
FormatTag = WaveFormatPcm,
Channels = 2,
SamplesPerSecond = sampleRate,
AverageBytesPerSecond = checked(sampleRate * 4),
BlockAlign = 4,
BitsPerSample = 16,
ExtraSize = 0,
};
var result = WaveOutOpen(
out _device,
WaveMapper,
ref format,
_completion.SafeWaitHandle.DangerousGetHandle(),
IntPtr.Zero,
CallbackEvent);
if (result != 0)
{
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
}
}
public bool Submit(
ReadOnlySpan<byte> source,
uint frames,
int channels,
int bytesPerSample,
bool isFloat)
{
lock (_gate)
{
if (_disposed)
{
return false;
}
var outputLength = checked((int)frames * 4);
ReapCompletedBuffers();
while (_queuedPcmBytes != 0 &&
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
{
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
{
return false;
}
ReapCompletedBuffers();
}
var output = ArrayPool<byte>.Shared.Rent(outputLength);
try
{
ConvertToStereoPcm16(
source,
output.AsSpan(0, outputLength),
checked((int)frames),
channels,
bytesPerSample,
isFloat);
return QueueBuffer(output.AsSpan(0, outputLength));
}
finally
{
ArrayPool<byte>.Shared.Return(output);
}
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_device != IntPtr.Zero)
{
WaveOutReset(_device);
while (_buffers.TryDequeue(out var buffer))
{
ReleaseBuffer(buffer);
}
WaveOutClose(_device);
_device = IntPtr.Zero;
}
_completion.Dispose();
}
}
private bool QueueBuffer(ReadOnlySpan<byte> data)
{
var dataAddress = Marshal.AllocHGlobal(data.Length);
var headerAddress = IntPtr.Zero;
try
{
unsafe
{
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
}
var header = new WaveHeader
{
Data = dataAddress,
BufferLength = checked((uint)data.Length),
};
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
Marshal.StructureToPtr(header, headerAddress, false);
var result = WaveOutPrepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
return false;
}
result = WaveOutWrite(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
WaveOutUnprepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
return false;
}
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
_queuedPcmBytes += data.Length;
dataAddress = IntPtr.Zero;
headerAddress = IntPtr.Zero;
return true;
}
finally
{
if (headerAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(headerAddress);
}
if (dataAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(dataAddress);
}
}
}
private void ReapCompletedBuffers()
{
while (_buffers.TryPeek(out var buffer))
{
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
if ((header.Flags & WaveHeaderDone) == 0)
{
return;
}
_buffers.Dequeue();
ReleaseBuffer(buffer);
}
}
private void ReleaseBuffer(NativeBuffer buffer)
{
WaveOutUnprepareHeader(
_device,
buffer.Header,
checked((uint)Marshal.SizeOf<WaveHeader>()));
_queuedPcmBytes -= buffer.Length;
Marshal.FreeHGlobal(buffer.Header);
Marshal.FreeHGlobal(buffer.Data);
}
private static void ConvertToStereoPcm16(
ReadOnlySpan<byte> source,
Span<byte> destination,
int frames,
int channels,
int bytesPerSample,
bool isFloat)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
{
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
}
}
private static short ReadSample(
ReadOnlySpan<byte> frame,
int channel,
int bytesPerSample,
bool isFloat)
{
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
if (!isFloat)
{
return BinaryPrimitives.ReadInt16LittleEndian(sample);
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat
{
public ushort FormatTag;
public ushort Channels;
public uint SamplesPerSecond;
public uint AverageBytesPerSecond;
public ushort BlockAlign;
public ushort BitsPerSample;
public ushort ExtraSize;
}
[StructLayout(LayoutKind.Sequential)]
private struct WaveHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public nuint User;
public uint Flags;
public uint Loops;
public IntPtr Next;
public nuint Reserved;
}
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
private static extern uint WaveOutOpen(
out IntPtr device,
uint deviceId,
ref WaveFormat format,
IntPtr callback,
IntPtr instance,
uint flags);
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
private static extern uint WaveOutReset(IntPtr device);
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
private static extern uint WaveOutClose(IntPtr device);
}
@@ -0,0 +1,104 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
namespace SharpEmu.Libs.AvPlayer;
public static class AvPlayerExports
{
private const int InvalidParameters = unchecked((int)0x806A0001);
private static readonly object StateGate = new();
private static readonly HashSet<ulong> Players = new();
[SysAbiExport(
Nid = "aS66RI0gGgo",
ExportName = "sceAvPlayerInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerInit(CpuContext ctx)
{
if (ctx[CpuRegister.Rdi] == 0 ||
!KernelMemoryCompatExports.TryAllocateHleData(ctx, 0x40, 16, out var handle))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
lock (StateGate)
{
Players.Add(handle);
}
ctx[CpuRegister.Rax] = handle;
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",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerPostInit(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var dataAddress = ctx[CpuRegister.Rsi];
lock (StateGate)
{
return ctx.SetReturn(
handle != 0 && dataAddress != 0 && Players.Contains(handle)
? 0
: InvalidParameters);
}
}
[SysAbiExport(
Nid = "NkJwDzKmIlw",
ExportName = "sceAvPlayerClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerClose(CpuContext ctx)
{
lock (StateGate)
{
return ctx.SetReturn(Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
}
}
}
@@ -0,0 +1,38 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.CommonDialog;
public static class CommonDialogExports
{
private static int _initialized;
[SysAbiExport(
Nid = "uoUpLGNkygk",
ExportName = "sceCommonDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceCommonDialog")]
public static int CommonDialogInitialize(CpuContext ctx)
{
// 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(
Nid = "BQ3tey0JmQM",
ExportName = "sceCommonDialogIsUsed",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceCommonDialog")]
public static int CommonDialogIsUsed(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -0,0 +1,185 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Threading;
namespace SharpEmu.Libs.CommonDialog;
public static class MsgDialogExports
{
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",
ExportName = "sceMsgDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogInitialize(CpuContext ctx)
{
// 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}");
}
}

Some files were not shown because too many files have changed in this diff Show More