Compare commits

...

96 Commits

Author SHA1 Message Date
Pacuka 7b86a91dfa Add files via upload (#202)
Added Hungarian tranlation. -Pacuka
2026-07-15 14:39:42 +03:00
Gutemberg Ribeiro 72645cb373 [Host] Abstract audio output and pad/keyboard input behind the host platform seam (#192)
* [Host] Abstract audio output behind IHostAudioOutput

Add IHostAudioOutput (opens streams, names the backend for diagnostics)
and IHostAudioStream (submit interleaved stereo 16-bit PCM, Dispose) to
the host seam, with the winmm waveOut implementation moving whole into
Host/Windows/WindowsWaveOutAudio — same device open, queueing,
32 KB backpressure wait, and buffer lifetime as WinMmAudioPort had. The
DllImports become source-generated LibraryImports in the move, matching
the other Windows backends.

The guest-format conversion (mono/stereo/7.1, s16/float32 -> stereo
PCM16) is platform policy, not device code, so it stays in Libs as
AudioPcmConversion; AudioOutOutput converts into a pooled buffer and
submits the result through the stream. Open failures still degrade to
the silent paced port with the same warning, and the port log line now
takes its backend name from the platform instead of a hardcoded string.

* [Host] Abstract pad and keyboard input behind IHostInput

Add IHostInput to the host seam: gamepad state snapshots, rumble /
trigger-rumble / lightbar sinks, and the keyboard-fallback queries
(window focus, key state). Gamepad state crosses the seam as the new
unmanaged HostGamepadState with HostGamepadButtons flags — named after
the PlayStation layout the guest API exposes but with the seam's own
values, so SCE_PAD_BUTTON bits never leak into host backends and the
per-frame poll can stackalloc its snapshot buffer.

The DualSense raw-HID reader, the XInput reader, and the Win32 HID
interop move whole into Host/Windows (report parsing, hot-plug loops,
rumble/lightbar output reports, and log strings unchanged), translating
to the neutral flags instead of ORBIS bits and converting their
DllImports to source-generated LibraryImports. WindowsHostInput
composes them plus the user32 keyboard queries; rumble still fans out
to both readers, trigger rumble stays XInput-only, lightbar stays
DualSense-only.

PadExports keeps all policy: the keyboard mapping (now via named
OrbisPadButton constants instead of raw hex), the controller-beats-
keyboard-past-deadzone merge, and the new host->ORBIS button
translation. The GUI's source-linked reader copies re-point to the
moved files (it still cannot reference SharpEmu.HLE wholesale), which
requires AllowUnsafeBlocks for the generated marshalling stubs; its
navigation code switches to the neutral flags.

* [Host] Move the timer-resolution request behind IHostThreading

IHostThreading gains RequestTimerResolution (idempotent, best-effort
~1 ms timed-wait granularity; a no-op wherever the platform default is
already fine). The winmm timeBeginPeriod call, its once-only latch, and
both warning strings move from the Libs-level HostTimerResolution
helper into WindowsHostThreading as a source-generated LibraryImport;
the vblank pump requests it through the platform instead.

HostSystemInfo in SharpEmu.Logging keeps its direct user32/kernel32
imports deliberately: Logging sits below HLE in the dependency chain so
it cannot see the host seam, every path is already OS-gated with
fallbacks, and it only runs once for the diagnostics banner.
2026-07-15 13:24:53 +03:00
SamuelEzequias 2ad9836d13 Add Brazilian translation to Environment tab (#196) 2026-07-15 13:00:30 +03:00
Gutemberg Ribeiro 62e1775c5c [HLE] Remove steady-state allocations from the hot HLE paths (#190)
* [HLE] Stop allocating on the memcpy/memset and trace hot paths

memcpy/memmove no longer allocate a bounce buffer sized to the whole
copy (large copies previously landed on the LOH); they loop through a
single pooled 256 KB rental, copying high-to-low when the destination
overlaps above the source so memmove semantics survive the chunking.
memset reuses a shared zero chunk for the dominant zero-fill case and
rents/fills only min(length, 16K) bytes for non-zero values instead of
allocating and filling a fresh 16 KB array per call; the map-time
zero-fill loop shares the same zero chunk.

SHARPEMU_LOG_SEMA / SHARPEMU_LOG_VIDEOOUT are now read once into cached
bools and every TraceSemaphore/TraceVideoOut call site is guarded, so
trace messages are no longer interpolated (and the env var no longer
queried) on every semaphore op and every flip with tracing off. Trace
output when the flags are set is unchanged.

* [HLE] Remove per-frame allocations from the vblank/flip/equeue plumbing

The 60 Hz vblank pump no longer allocates per edge: PumpVblanks reuses a
pump-thread-only port list instead of a LINQ Where/ToArray, and
SignalVblank/SubmitFlip snapshot their event registrations into pooled
rentals instead of copying the List on every edge and every flip (the
snapshot must still be taken, since triggers run outside _stateGate and
a per-port reusable buffer would race the pump thread against a guest
thread's first-edge signal).

sceKernelWaitEqueue delivery rents the dequeue buffer from the pool
instead of allocating an array per wait, and event-queue wake keys are
formatted once per handle (cached in a ConcurrentDictionary, dropped on
queue delete) instead of building the string on every enqueue. The
semaphore wake key moves onto KernelSemaphoreState at creation, the
same pattern the pthread mutex state already uses, removing the
per-signal/per-wait formatting. SHARPEMU_LOG_EQUEUE is read once into a
cached bool like the sema/videoout flags.

* [HLE] Read guest C-strings without per-call buffer allocations

CpuContext.TryReadNullTerminatedUtf8 allocated a byte[capacity] and
issued one TryRead per byte for every string-argument import. It now
reads through a stack buffer (pooled above 512 bytes) in 128-byte bulk
chunks, falling back to per-byte reads only when a chunk touches an
unreadable range so a terminator sitting just before unmapped memory
still resolves exactly as before. The chunk bound also keeps the
overread past the terminator smaller than the old loop's worst case is
wide, so no fault can appear where the byte loop succeeded.

TryReadAsciiZ (dlsym/symbol resolution) drops its List<byte> + ToArray
round-trip for the same stack/pooled buffer, keeping the byte-by-byte
TryReadByteCompat reads because their Marshal.ReadByte fallback must
probe exactly up to the terminator. Only the final string is allocated
on either path now.

* [HLE] Replace blocking-wait closures with waiter continuation objects

Every wait that actually parked a guest thread allocated two capturing
lambdas (plus their display classes) for the scheduler's resume/wake
callbacks. RequestCurrentThreadBlock and the backend's blocked-thread
state now carry a single IGuestThreadBlockWaiter instead of the
Func<int>/Func<bool> pair: TryWake keeps the run-under-the-scheduler-
gate contract and Resume still produces the guest's RAX on the woken
thread. The waiter stays attached through the wake transition (the old
code nulled only the wake handler there) and is consumed at resume.

The existing waiter objects absorb the captured state as fields, so a
blocking wait now allocates exactly one object: SemaphoreWaiter,
PthreadMutexWaiter, and EventFlagWaiter implement the interface
directly, and the equeue, cond, and rwlock waits get small waiter
classes replacing their closures. Handler bodies delegate to the same
static methods with the same arguments as before; the untimed event
flag wait's mutable captured result becomes a field on its waiter.

* [HLE] Back pending event queues with a ring deque instead of LinkedList

LinkedList<KernelQueuedEvent> allocated a node object on every
non-coalesced enqueue — one per vblank/flip edge per registered queue,
60+ times a second in steady state. KernelEventDeque is a grow-only
ring buffer over a KernelQueuedEvent[] with the three operations the
queue actually uses (AddLast, RemoveFirst, find-and-update-in-place by
ident/filter), so steady-state enqueue/dequeue allocates nothing and
the coalescing update writes the struct back through an indexer instead
of a node reference. All accesses stay under _eventQueueGate, matching
the LinkedList usage it replaces.

* [HLE] Cap memcpy chunk iterations at the requested size, not the rented length

Address Copilot review: ArrayPool.Rent may return a larger array than
requested, so sizing each iteration by chunk.Length let the copy
granularity depend on pool bucketing internals instead of the intended
256 KB chunking. Behavior was already correct for any chunk size (each
iteration re-reads the source, and the overlap ordering is size-
independent), but the loop now mins against the requested chunkLength,
matching what memset already does.

* [HLE] Skip the flip/vblank snapshot rental when no events are registered

Address Copilot review: SignalVblank and SubmitFlip rented (and
returned) a pooled snapshot even with zero registrations — steady
per-frame pool traffic for games that never register flip events and
only poll flip status. Zero-count signals now skip the rental, the
copy, and the trigger loop entirely, which also retires the
Math.Max(count, 1) minimum-rent guard.
2026-07-15 12:57:40 +03:00
SamuelEzequias 6dacd59a08 [GUI] Add Portuguese (Portugal) translation (#197)
* [GUI] Add Portuguese (Portugal) translation

* [GUI] Add Portuguese (Portugal) translation
2026-07-15 12:56:25 +03:00
Spooks 9d88542efd Fix virtual memory allocation and access (#193)
* Fix virtual memory allocation and access

* Update test dependency lock file
2026-07-14 21:50:54 -06:00
StealUrKill 373100a6b0 Add 21 missing SysAbi exports and GUI Environment tab for SHARPEMU_* toggles (#189)
Fills NID gaps hit by PS5 titles during boot, controller setup, and
rendering, and surfaces the common runtime switches in the GUI. All
exports are additive (no behavior change to existing exports) and free of
NID and export-name collisions with upstream.

New export libraries:
- libSceBluetoothHid: Init/RegisterDevice/RegisterCallback success stubs so
  titles proceed past Bluetooth controller setup (opt-out via
  SHARPEMU_BTHID_UNAVAILABLE=1).
- libSceNpCppWebApi: Common::initialize no-op success; UE5 online titles
  abort PS5-component startup on a negative SCE error.

Additions to existing libraries:
- libScePad: scePadOpenExt (shared PadOpenCore, accepts special ports 1/2 and
  the ScePadOpenExtParam pointer), scePadClose, scePadGetExtControllerInformation.
- libSceVideoOut: sceVideoOutConfigureOutput, sceVideoOutInitializeOutputOptions.
- libSceAgc: DCB builders sceAgcDcbSetIndexCount, sceAgcDcbJump, DcbSetPredication,
  SetPacketPredication (emit valid skippable packets; full draw processing TODO).
- libSceAmpr: measure and write KernelEventQueueOnCompletion pair.
- libKernel: scePthreadGet/Setschedparam, sceKernelChmod (validate and accept;
  POSIX permission bits have no host equivalent on Windows).
- libSceNetCtl: sceNetCtlRegisterCallbackV6 (delegates to the v4 callback).
- libSceMouse: sceMouseInit.
- libSceUserService: sceUserServiceGetAgeLevel (adult, skips parental gates).

GUI: new Options Environment tab exposing common SHARPEMU_* switches as
toggles (BTHID_UNAVAILABLE, DISABLE_IMPORT_LOOP_GUARD, VK_VALIDATION,
DUMP_SPIRV, LOG_DIRECT_MEMORY, LOG_NP). Persisted in gui-settings.json and
applied to the emulator process environment at launch; localized with
English fallback.
2026-07-15 03:36:15 +03:00
Gutemberg Ribeiro f23161be9a Host platform abstraction layer for the execution engine (#181)
* [Host] Introduce host platform abstraction with IHostMemory

Add SharpEmu.HLE/Host with IHostPlatform/IHostMemory interfaces, neutral
page-protection/region enums, and a HostPlatform.Current factory that
resolves the Windows backend (or throws PlatformNotSupportedException on
other OSes, matching today's de-facto behavior). WindowsHostMemory wraps
the exact VirtualAlloc/VirtualFree/VirtualProtect/VirtualQuery calls used
across the engine today, with identical MEM_*/PAGE_* constants.

Migrate StubManager as the first consumer: its private kernel32 P/Invokes
and enums are replaced by IHostMemory calls that issue the same two
native operations (RWX commit+reserve of the PLT arena, release on
Dispose). No behavior change.

This is the first step toward supporting non-Windows hosts; subsequent
commits move the remaining direct P/Invokes in Core and Libs behind the
same seam.

* [Host] Route PhysicalVirtualMemory through IHostMemory

Replace the class's private VirtualAlloc/VirtualFree/VirtualProtect/
VirtualQuery P/Invokes with IHostMemory calls. Every site maps 1:1 onto
the exact native call it issued before: MEM_COMMIT|MEM_RESERVE ->
Allocate, MEM_RESERVE -> Reserve, fault-path commits -> Commit, and
MEM_RELEASE -> Free, with identical protection values produced by the
Windows backend.

IHostMemory gains ProtectRaw so the save/restore protection sequences in
TryWriteExclusive and TryTemporarilyProtectForRead round-trip the raw OS
protection word (including modifier bits the neutral enum cannot
represent) exactly as before. Raw PAGE_* constants remain only for the
internal region-classification helpers, which only ever see values this
class itself assigned.

The exact-address free-on-mismatch, lazy reserve-only threshold, prime
loop, and all trace strings are unchanged.

* [Host] Add IGuestAddressSpace and retire the reflection-based allocator lookup

Introduce IGuestAddressSpace in SharpEmu.HLE (fixed-address AllocateAt /
TryAllocateAtOrAbove and guest mprotect via TryProtect) with signatures
copied from PhysicalVirtualMemory, which now implements it. TryProtect
reproduces the read/write/execute decomposition that
KernelMemoryCompatExports.ResolveHostProtection performs, yielding the
same PAGE_* values through the Windows backend.

KernelVirtualRangeAllocator previously located AllocateAt via cached
MethodInfo reflection (because SharpEmu.Libs cannot see Core types) and
walked wrapper memories through an untyped 'Inner' property. Both are
now typed: ICpuMemoryWrapper exposes the decorated memory (implemented
by TrackedCpuMemory, whose Inner property already existed) and the
allocator type-tests for IGuestAddressSpace with the same bounded
unwrap depth. Failure paths keep the exact [LOADER][TRACE] strings.

* [Host] Move Kernel HLE memory exports off direct kernel32 P/Invokes

KernelMemoryCompatExports loses its private VirtualQuery/VirtualProtect/
VirtualAlloc/VirtualFree declarations and MemoryBasicInformation struct:

- Guest mprotect (sceKernelMprotect/sceKernelMtypeprotect) now routes
  through IGuestAddressSpace.TryProtect resolved from ctx.Memory. The
  orbis read/write/execute decomposition moves into a GuestPageProtection
  conversion whose mapping is value-identical to the removed
  ResolveHostProtection.
- The guarded libc heap and host-page accessibility checks go through
  IHostMemory (same commit+reserve/protect/free sequence; guard-page and
  protection-mask checks compare HostRegionInfo.RawProtection against the
  same PAGE_* literals as before).
- HostMemory is exposed as a property so merely loading the type never
  resolves the platform backend on non-Windows hosts.

KernelRuntimeCompatExports' RDTSC stub allocates its 16-byte RWX page via
IHostMemory.Allocate; the OperatingSystem.IsWindows() gate returning null
is unchanged.

* [Host] Abstract thread, TLS, and symbol primitives in the execution backend

Add IHostThreading (native TLS slots, current-thread id, affinity, raw
thread create/join, diagnostic register capture) and IHostSymbolResolver
(enum-keyed host function addresses baked into emitted stubs), with
Windows implementations wrapping the exact kernel32 calls the backend
made directly before.

DirectExecutionBackend takes an optional IHostPlatform (defaulting to
HostPlatform.Current) and routes every TlsAlloc/TlsFree/TlsSet/GetValue,
GetCurrentThreadId, SetThreadAffinityMask, GetModuleHandle/GetProcAddress
and the suspend+GetThreadContext diagnostic snapshot through it. The
snapshot moves wholesale into WindowsHostThreading (including the Win64
CONTEXT size/flags/offsets, which are Windows-specific by nature) and
returns a neutral HostCapturedRegisters.

NativeGuestExecutor resolves WaitForSingleObject/SetEvent/ExitThread via
the symbol resolver — the same addresses end up in the emitted run loop,
so stub bytes are unchanged — and creates/joins its raw worker thread
through IHostThreading with the same stack-reservation semantics. The
run-loop emitter itself does not move.

Marshal.GetLastWin32Error() in the affinity-failure log still observes
SetThreadAffinityMask's error because the wrapper makes no intervening
SetLastError call.

* [Host] Move fault handling and remaining backend memory ops behind the seam

Add IHostFaultHandling (handler-thunk creation, first-chance handler
install/remove, unhandled-filter set) with WindowsFaultHandling in a new
Cpu/Native/Windows/ folder. The exception-handler trampoline emitter
moves there whole — same pre-filtered NTSTATUS codes, same TEB gs:[8]/
gs:[0x10] stack-limit reads, same host-RSP TLS switch — parameterized
only by (managed callback, TLS slot, TlsGetValue address), which is
exactly what SetupExceptionHandler passed it before. Handler
installation order, the AddVectoredExceptionHandler(first=1) flag, the
SHARPEMU_DISABLE_RAW_HANDLER gate, and all install/teardown log strings
are unchanged.

Every remaining VirtualAlloc/VirtualProtect/VirtualFree/VirtualQuery/
FlushInstructionCache in the backend partials routes through IHostMemory
with 1:1 call mapping (RWX emit -> RX downgrade -> flush for stub
emission, reserve/commit for the PRT aperture and lazy-commit fault
path, raw-protection round-trips via ProtectRaw). HostRegionInfo gains
RawState/RawAllocationProtection so the lazy-commit trace lines and
protection-mask checks keep printing and comparing the exact native
values.

Windows semantics leaked as bare literals become named constants with
identical values: NTSTATUS codes (WindowsFaultCodes) and Win64 CONTEXT
byte offsets (Win64ContextOffsets, with the existing CTX_* constants
aliased to it and handler-local numeric offsets replaced by the names).

* [Host] Resolve the host platform explicitly at the composition root

SharpEmuRuntime.CreateDefault() now resolves HostPlatform.Current once
and passes it explicitly to PhysicalVirtualMemory and (via a new
optional CpuDispatcher parameter) to DirectExecutionBackend, replacing
the implicit default-argument fallbacks. On unsupported OSes boot now
fails at the root with PlatformNotSupportedException and a clear
message instead of on the first native call. A future Linux/macOS
backend plugs in by returning a different IHostPlatform here.

* [Host] Convert the platform backends to source-generated P/Invokes

Replace [DllImport] with [LibraryImport] in the four Windows backend
files added by this branch (WindowsHostMemory, WindowsHostThreading,
WindowsHostSymbolResolver, WindowsFaultHandling). Marshalling stubs are
now generated at compile time instead of JIT-emitted at runtime, which
fits the pre-JIT-everything boot model and keeps the backends
NativeAOT/trimming ready.

Interop stays zero-copy: all signatures are blittable, GetModuleHandleW
now pins the managed string via Utf16 marshalling instead of copying,
and GetProcAddress names marshal through a stack-allocated Utf8 buffer.
Implicit contracts become explicit where LibraryImport requires it:
TlsFree/TlsSetValue gain [MarshalAs(UnmanagedType.Bool)] (the 4-byte
Win32 BOOL DllImport assumed silently), and GetModuleHandle targets the
W entry point directly since LibraryImport never probes suffixes.

The CONTEXT snapshot buffer stays a NativeMemory allocation rather than
stackalloc: CONTEXT requires 16-byte alignment, now documented at the
call site. Native call sequences are unchanged.

* [Host] Address Copilot review: harden failure paths, honor injected platform

- Free the handler thunk page when the RX protection downgrade fails
  (the leak predates this branch, but the failure path is boot-fatal so
  releasing the page is unobservable).
- TraceThreadMode and the static diagnostics helpers now resolve host
  primitives through the backend bound to the current thread, falling
  back to HostPlatform.Current only when no run is active (identical on
  supported configs, honors injection everywhere a backend exists).
- HostPlatform.Create additionally requires an x64 process so native
  Windows ARM64 fails with the promised PlatformNotSupportedException
  instead of emitting x86-64 stubs into an ARM64 process.
2026-07-15 03:15:36 +03:00
Dafenx 081760be3f [AGC/Vulkan] Support multiple render targets (#149)
* [AGC] Support multiple typed pixel outputs

Emit dense float, uint, and sint fragment outputs for sparse guest MRT slots. Preserve disabled components across partial exports, validate dense host locations, and retain the single-output compiler overload for compatibility.

* [Vulkan] Execute translated draws with multiple color attachments

Carry every active color target and its effective shader/register write mask through one Vulkan draw. Add per-attachment blending, independentBlend negotiation, device/format validation, multi-attachment synchronization, and safe image recreation after in-flight work completes.

* [ShaderDump] Add MRT edge-case coverage

Cover sparse mixed-type outputs, partial exports, merged partial exports, independent blend layouts, eight attachments, and invalid host locations. Run the synthetic shader suite in CI.

---------

Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-15 02:41:39 +03:00
José Luis Caravaca Carretero e604fb606d Fix pak size-collision that crashed Quake right after the intro demo (#187)
* [Tests] Add SharpEmu.Libs.Tests project

Introduce an xunit project for the HLE libs with a minimal ICpuMemory fake,
so library-level exports and helpers can be exercised without a live guest.

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

* [Ampr] Disambiguate pak size-collisions by read locality

PakDirectoryTracker resolves a sequential AMPR read (offset -1) back to an
absolute pak offset by matching the requested byte count against the PACK
directory. When several files share that byte count it took the first
unconsumed match in directory order, which mis-resolves out-of-order reads:
progs/h_ogre.mdl and bots/navigation/death32c.nav are both 0x3A34 bytes, and
death32c.nav sits earlier in the directory and is never read during Quake's
intro demo, so requesting h_ogre.mdl returned the nav file's bytes. The engine
then parsed "NAV2" as a brush model, failed the version check and aborted.

Pick the unconsumed same-size entry nearest the running read cursor instead.
id archives cluster related assets and the guest streams them with locality,
so this lands on the intended file; contiguous same-size runs (the
gfx/weapons/ww_*.lmp icons) still resolve in packed order.

Verified against a Quake dump: the abort is gone, h_ogre.mdl reads correctly,
and the intro demo reaches its main loop and renders instead of dying at the
error dialog.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:49:41 +03:00
José Luis Caravaca Carretero df53ff59d9 [Json] Implement sce::Json::Value and String (construct / set / destroy) (#169)
* [Json] Implement sce::Json::Value and Json::String construct/set/destroy

libSceJson previously only had the Initializer/MemAllocator setup path.
The Value and String classes themselves were entirely absent, so a
Prospero title that builds a JSON tree (Quake PPSA01880 does, to shape
a web-API request) hit unresolved imports and faulted on the call. The
imports it left unresolved right before its access violation are exactly
these Value ctors/setters and String ctor/dtor.

Model the Value/String payload host-side (JsonObjectHeap), keyed by the
guest `this` pointer, following the handle-shadow pattern already used
by Ngs2Exports. The guest object bytes are deliberately not written:
these objects are usually stack-allocated with an unknown real layout,
and writing a guessed layout risks smashing an adjacent stack canary
(the same hazard the AudioOut2 context-param note in this tree records).
Constructors and setters follow the Itanium ABI and return `this` in rax,
which is correct whether the real setter returns void or Value&.

Covered NIDs (complete-object C1/D1 variants, matching the observed
imports): Value(default/bool/long/ulong/double/ValueType/char*/String),
Value::~Value, Value::set(bool/long/ulong/double/ValueType/char*/String),
Value::clear, String(char*/default/copy), String::~String.

Only the payload the guest can reach through library methods is modelled;
direct guest reads of the object bytes are out of scope and would need
observed layout evidence.

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

* [Tests] Add SharpEmu.Libs.Tests covering the Json Value/String exports

First test project for SharpEmu.Libs (xunit), the SharpEmu.Libs.Tests
layout the maintainer already agreed to in issue #36.

- A FakeCpuMemory (single contiguous region) drives the exports at the
  CpuContext level with no live guest.
- Direct-call tests: ctor/setter round-trips for bool/int/uint/double
  (read from xmm0)/char*/String/ValueType, destructor cleanup, and the
  graceful-degradation paths (missing String shadow and a faulting char*
  pointer both fall back to the empty string instead of throwing).
- Registration test: a real ModuleManager scans SharpEmu.Libs and the
  nine NIDs Quake left unresolved now resolve to the libSceJson exports
  and dispatch cleanly (returns `this` in rax).

InternalsVisibleTo exposes JsonObjectHeap to the test assembly. The test
project's packages.lock.json is committed for CI locked-mode restore;
CI does not run tests yet, left as a maintainer decision.

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

* [Json] Add Initializer::setGlobalNullAccessCallback

Quake calls it during kexPSNWebAPI::Initialize and treats the
not-found error as fatal for the whole Np Web API bring-up. Store the
guest hook (never invoked by this HLE: shadows degrade to defaults
instead of dereferencing missing members) and return success.

Verified against the dump: the "setGlobalNullAccessCallback failed
(0x80020002)" line is gone and kexPSNWebAPI::Initialize now logs
"Np Web API Initialized"; the next blockers are sceNpAuthCreateRequest
and sceUserServiceInitialize ordering, outside libSceJson.

Also pins both Json test classes to one xunit collection: they share
JsonObjectHeap statics and parallel class execution raced ResetForTests
against a running test.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:49:33 +03:00
Randomuser8219 4c35831cb8 Add code -1073741819 as an emulation error (#188)
There's currently games that crash on this code due to emulation errors, so it'd make sense to add this error code as an emulation error.
2026-07-15 01:47:44 +03:00
miles 90fdd20f9a Create nl.json (#186) 2026-07-15 01:36:38 +03:00
Mike Saito ae5ef0abe7 Add SaveData transaction and NP UDS layout HLE stubs (#168)
* Add SaveData transaction and NP UDS layout HLE stubs

Wire Prepare, Commit, and Umount2 for implicit save transactions,
unregister guest mounts on Umount2, and add NP UDS CreateEvent,
DestroyEvent, and EventPropertyObjectSetString for layout-load imports.

* Add NP UDS SetArray and PostEvent layout HLE stubs

Add sceNpUniversalDataSystemEventPropertyObjectSetArray and
sceNpUniversalDataSystemPostEvent for layout-load imports on PPSA02929.
2026-07-15 01:34:59 +03:00
Mike Saito 5e2c21edf1 Fix historic SysAbi exports bound to wrong symbol names (#167)
Move KMcEa+rHsIo from libKernel MapMemory mislabel to sceAvPlayerAddSource.
Align WV1GwM32NgY ExportName with sceNpWebApi2PushEventCreateHandle. Behavior unchanged.
2026-07-15 01:34:27 +03:00
Deeptanshu Lal 3fb9d4db1c [Tools] Fix ShaderDump reflection invoke against new optional parameters (#166)
TryCompileVertexShader gained an optional scalarRegisterBufferIndex
parameter (#156), and reflection Invoke does not apply C# default
parameter values, so ShaderDump crashed with
TargetParameterCountException. Pad trailing optional parameters with
Type.Missing under BindingFlags.OptionalParamBinding so the declared
defaults are used; only a new required parameter now needs a tool
update, and that fails with a named error instead of a crash.

Verified: all five programs behave as expected (exit 0), all eight
emitted blobs pass spirv-val --target-env vulkan1.3.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:34:08 +03:00
José Luis Caravaca Carretero de13735972 [CommonDialog] Fix dialog state machine and add MsgDialog progress-bar exports (#163)
Rework the sceMsgDialog and sceSaveDataDialog HLE state machines so the full
Initialize -> Open -> poll -> GetResult -> Close/Terminate lifecycle honors the
common-dialog contract, and add the three missing sceMsgDialogProgressBar* exports.

- Fix an unreachable close path: sceSaveDataDialogClose already did a
  RUNNING -> FINISHED compare-exchange, but Open jumped straight to FINISHED, so
  RUNNING never existed and Close could only return NOT_RUNNING. Open now enters
  RUNNING and the first status poll advances it to FINISHED. Same model applied to
  sceMsgDialog.
- Return the real SCE_COMMON_DIALOG_ERROR_* codes (0x80B8xxxx) from sceMsgDialog*
  instead of emulator-internal result codes, with the missing argument/state guards
  (ARG_NULL, NOT_INITIALIZED, BUSY, NOT_FINISHED, NOT_RUNNING).
- GetResult reports buttonId = 1 (affirmative) instead of 0, the invalid sentinel a
  yes/no prompt could mis-branch on.
- Add sceMsgDialogProgressBarSetValue, sceMsgDialogProgressBarInc and
  sceMsgDialogProgressBarSetMsg (NIDs wTpfglkmv34, Gc5k1qcK4fs, 6H-71OdrpXM), gated
  on the service being initialized.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:33:48 +03:00
AlexC fc0efca297 Fixed deutch language file, it had invalid syntax (#162) 2026-07-15 01:31:54 +03:00
anesr5 2a9a261913 loader: support ps5 SELF and validate ELF signatures (#157)
Co-authored-by: anes <anesrachedi@outlook.fr>
2026-07-15 01:31:16 +03:00
Mike Saito 290f5fd3d7 Add SysAbi ExportName name2nid check script (#152)
* Add SysAbi ExportName name2nid check script

* Make SysAbi ExportName check green on tip with catalog skips and one Np rename
2026-07-15 01:29:23 +03:00
tensorcrush c06c70cad7 [Aerolib] Add ulobjmgr and NpEAAccess symbol names (#150)
Resolves _sceUlobjmgrRegisterObject (BG26hBGiNlw) and
_sceUlobjmgrUnregisterObject (Smf+fUNblPc), reported as unresolved by
testers, plus four sceNpEAAccess exports. Names taken from shadPS4's
NID tables and each verified by recomputing the NID with the repo's
name2nid derivation before inclusion. aerolib.bin regenerated with
scripts/generate_aerolib_binary.py.

Co-authored-by: tensorcrush <tensorcrush@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:28:17 +03:00
Deeptanshu Lal 5e54250752 [Tools] Add GPU conformance executor for dumped shader blobs (#127)
SharpEmu.Tools.GpuConformance executes the exec-cs.spv blob produced by
SharpEmu.Tools.ShaderDump on a real Vulkan device (preferring a discrete
GPU) and compares every word of the 64-byte storage buffer against
CPU-computed expectations, bit for bit. Creating the compute pipeline
doubles as a driver-acceptance check for SharpEmu's emitted SPIR-V.

The checks cover the three ALU results, the store attempted with EXEC=0
(its destination must keep the sentinel), the store after EXEC is
restored, and all trailing sentinel words. Any mismatch counts toward the
failure total and makes the tool exit non-zero.

Verified on an RTX 3060 Laptop GPU (NVIDIA) with all values matching, and
the failure path verified to exit 1 by running a non-storing blob.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:26:18 +03:00
AlexC be6a6a5935 [GUI] New box "About" in configuration (#165)
* Added about tab with github and discord

* Added discord & github svgs and svg support

* Changed svg to pngs and localization text in english & spanish
2026-07-15 00:59:13 +03:00
Mike Saito caf859cc52 Fix guest shutdown when VideoOut window is closed (#184)
Propagate Silk window close to runtime teardown so audio and CPU workers stop instead of continuing after the presentation window is dismissed.
2026-07-15 00:52:01 +03:00
brbrhuehue-matrix d2f3511002 Add Brazilian Portuguese translation (#153) 2026-07-15 00:42:44 +03:00
Nolan 90a5d5176f Add Korean (ko-KR) localization (#154) 2026-07-15 00:42:31 +03:00
Nolan 28a43e09c7 Add Japanese (ja) localization (#160) 2026-07-15 00:42:17 +03:00
AlexC 093cfa1f3e Fallback to english if it doesnt find the string in current language (#161) 2026-07-15 00:42:10 +03:00
Spooks d8397b022e Performance Improvements and Optimization Tweaks (#156)
* Improve Gen5 rendering performance and compatibility

* Pin .NET SDK for locked restore

---------

Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
2026-07-14 20:22:52 +03:00
anesr5 85cc2b9892 added french support (#147)
Co-authored-by: anes <anesrachedi@outlook.fr>
2026-07-14 18:06:57 +03:00
AlexC 293194c40b [GUI] Add Spanish language (#148)
* Added localization to spanish language

* Changed Options.Strict.Desc because i didnt like the way i localized it first
2026-07-14 18:06:48 +03:00
tensorcrush 1f09de8896 [AGC] Complete gfx10 v_cmpx_f32 decode and emit ordered/unordered float compares (#122)
* [AGC] Complete gfx10 v_cmpx_f32 decode and emit ordered/unordered float compares

Add the missing v_cmpx_*_f32 VOPC decode entries (0x17-0x1C, 0x1F) and
emission for the ordered/unordered predicates: nlg maps to OpFUnordEqual,
while o/u are lowered from OpIsNan (unordered = isnan(a) || isnan(b),
ordered = !unordered) because SPIR-V's OpOrdered/OpUnordered require the
Kernel capability and are invalid in Vulkan shader modules.

Opcode numbers cross-checked against LLVM's llvm-mc regression tests
(llvm/test/MC/AMDGPU/gfx10_asm_vopc.s, gfx10_asm_vopcx.s); emitted
lowering validated with spirv-val --target-env vulkan1.1.

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

* [AGC] Write VCC only for non-X vector compares

On gfx10 the VCmpx encodings have no sdst and define EXEC only, so the
unconditional VCC store clobbered VCC on every VCmpx. Move the VCC store
to the non-X path; EXEC keeps the existing old-EXEC & condition update.

Addresses review feedback on #122.

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

---------

Co-authored-by: tensorcrush <tensorcrush@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:05:33 +03:00
Dafenx ddc452b4fc [Pad] Approximate trigger vibration on XInput (#140)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-14 18:02:20 +03:00
Dafenx 61a97baf85 [AGC] Emit Gen5 v_sad_u32 (#138)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-14 17:11:11 +03:00
Mike Saito e80f96ecf5 Align SysAbi export names with Aerolib NID catalog (#137) 2026-07-14 17:10:57 +03:00
Dafenx d49c0f1f10 Emit Gen5 packed-integer and bit-count ops (#135)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-14 17:10:40 +03:00
Dafenx 1d33ef90fc Harden param.json metadata parsing (#134)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-14 17:10:32 +03:00
j92580498-max 26a570633c [HLE] Add strchr/strrchr/memchr/strcat/strncat/strstr libc exports (#132)
Implement six missing libc string/memory search and concatenation
routines in the kernel compat layer. Titles frequently call these
during startup string handling (path parsing, config lookups, format
string assembly), and without them the loader currently falls through
to unresolved-import handling.

The implementations follow the existing byte-at-a-time compat helpers
(TryReadCompat/TryWriteCompat) already used by strcpy/strncpy/memcmp,
matching native semantics: strchr/strrchr scan through and including
the terminator, memchr is bounded strictly by count, strcat/strncat
overwrite the destination terminator and re-terminate, and strstr
returns the haystack pointer for an empty needle. NIDs are the
libSceLibcInternal/libc symbol hashes for each name.
2026-07-14 17:10:00 +03:00
Deeptanshu Lal e4f89445b9 [Tools] Add synthetic shader dump tool for the Gen5 translator (#111)
SharpEmu.Tools.ShaderDump feeds hand-assembled Gen5 (gfx10) instruction
words — cross-checked against LLVM's AMDGPU target definitions — through
the real Gen5ShaderTranslator -> Gen5SpirvTranslator pipeline via
reflection (no emulator source changes; the project is not in the main
solution) and dumps the resulting vertex/compute SPIR-V blobs for
inspection with spirv-val / spirv-dis.

Each bundled program carries an expectation: fmac/muls/sopp-hints/exec
must decode and emit both stages, while sopp-mode (s_round_mode,
s_denorm_mode) pins the loud unknown-sopp decode failure those FP MODE
writes must keep producing until their semantics are modeled (#108). Any
unexpected outcome makes the tool exit non-zero, so it can gate scripts
or CI.

The exec program computes real ALU results and stores them with
buffer_store_dword, toggling EXEC off and on around a pair of stores; its
exec-cs.spv blob is designed for numeric verification on a real Vulkan
device (follow-up tool).

All dumped blobs pass spirv-val --target-env vulkan1.3.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 17:09:11 +03:00
Dawid Imbrzykowski 1254cc1564 added german language (#136) 2026-07-14 17:06:04 +03:00
Hayyan 503b3f4d6b [GUI] Add Arabic language (#142)
* [GUI] Add Arabic language

* [GUI] Add Arabic language

* [GUI] Add Arabic language
2026-07-14 17:05:53 +03:00
Greenz 6b37ab54f2 [GUI] Add Danish language (#143) 2026-07-14 17:05:47 +03:00
Berk a84d2344fb Deadcell fix (#144)
* [agc] add resource registration

* [libc] use C locale for printf
2026-07-14 17:01:16 +03:00
Spooks 787d3a1efb Fix Gen5 boot and restore stable AGC rendering (#139)
Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
2026-07-14 16:13:42 +03:00
Kushida 884584da67 fix: restore WaitSema loop guard boundary (#133) 2026-07-14 15:07:25 +03:00
Spooks d43edc865a Agent/fix gen5 thread agc compat (#130)
* Fix Gen5 thread and AGC compatibility

* Trim compatibility comments

* Report selected Vulkan GPU

* Clean up CPU title label

* Improve emulator frame pacing and performance

* Regenerate package locks with pinned SDK

---------

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

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

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

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

* Fix REUSE annotation for scripts/nids.csv

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

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

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

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

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

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

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

* [AGC] Narrow SOPP additions to scheduling hints only

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

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

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

---------

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

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

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

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

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

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

Ported from the unreal-boot-fixes branch.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* reuse

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

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

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

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

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

Deferred Commit in KernelGetdirentriesCore:

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

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

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

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

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

Files: KernelMemoryCompatExports.cs

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* [cpu] Isolate guest execution on native worker threads

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

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

* Add console search filter and log file path selector

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

[LOADER] prefix dropped — category is in LogEntry.

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

Build: 0 errors, 0 warnings.

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

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

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

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

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

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+3
View File
@@ -100,6 +100,9 @@ jobs:
- name: Build solution
run: dotnet build SharpEmu.slnx -c Release --no-restore
- name: Validate synthetic shaders
run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
- name: Publish win-x64 CLI
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
+1
View File
@@ -39,3 +39,4 @@ Desktop.ini
ehthumbs.db
.vs/
.idea/
+70
View File
@@ -0,0 +1,70 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Contributing
Contributions are always welcome!
Before opening a pull request, please keep the following in mind:
- Keep PRs small and focused on a single topic.
- Discuss large architectural changes before implementing them.
- Follow the project's existing coding style.
- Do not submit generated code unless you fully understand and can maintain it.
- Do not introduce Sony proprietary code, firmware, keys, decrypted assets, or other copyrighted PlayStation materials.
- All reverse engineering should be based on publicly available information, clean-room techniques, or your own original research.
- Game-specific hacks should be avoided whenever possible. Prefer generic implementations that improve overall compatibility.
- New features should not break existing behavior.
- Ensure the project builds successfully before submitting a PR.
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
## AI-Assisted Contributions
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.
However, contributors are expected to fully understand every line of code they submit. By opening a pull request, you confirm that you are able to explain, modify, debug, and maintain the submitted code without relying on the AI that generated it.
When submitting an AI-assisted PR:
- Clearly explain **what the change does**, **why it is needed**, and **what problem it solves**, using your own words.
- Describe **how you verified the change**, including the games, applications, or test cases used.
- Avoid excessive product-level logging. Use logging only when it provides meaningful diagnostic value.
- Comments should document design decisions or implementation details in your own words. Avoid generic AI-generated comments that merely restate what the code already does.
- Be prepared to answer review questions about the implementation. "The AI generated it" is not considered a sufficient explanation.
- Large AI-generated changes without a clear understanding of the implementation are unlikely to be accepted.
- If the implementation cannot be reasonably explained during code review, the pull request may be rejected regardless of whether it works.
The quality, correctness, maintainability, and long-term ownership of the submitted code remain the responsibility of the contributor.
## Coding Style
SharpEmu follows a consistent coding style across the project. Please ensure your contributions match the existing style.
- Use **4 spaces** for indentation (no tabs).
- Use **2 spaces** for XML-based files (e.g. `.csproj`, `.props`, `.targets`, `.xml`, `yml`, GitHub workflow files where applicable).
- Respect the project's `.editorconfig`.
- Ensure every text file ends with a **single trailing newline**
- Avoid formatting-only commits unless they are the purpose of the PR.
- Keep naming, formatting, and file organization consistent with the surrounding code.
- Prefer small, focused changes over large refactors.
### REUSE Compliance
This repository follows the REUSE Specification.
Every new file must contain the appropriate SPDX license header. Pull requests that do not comply with the project's REUSE requirements will fail CI and will not be merged.
### Recommended Development Environment
The repository includes an `.editorconfig` and Visual Studio solution files.
For the best experience, we recommend using:
- Visual Studio Code with;
- C#
- C# Dev Kit
Most editors that support `.editorconfig` will automatically apply the project's formatting rules.
+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>
+3
View File
@@ -12,11 +12,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
<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="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<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" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
</ItemGroup>
</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.
+7
View File
@@ -8,6 +8,7 @@ path = [
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
"src/SharpEmu.GUI/Languages/**",
"_logs/**",
".github/images/**",
"assets/images/**"
@@ -15,3 +16,9 @@ path = [
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"
+3
View File
@@ -12,4 +12,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
</Folder>
</Solution>
Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestFeature"
"rollForward": "disable"
}
}
+172
View File
@@ -0,0 +1,172 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
#!/usr/bin/env python3
"""Offline check: SysAbiExport ExportName must hash to its Nid (name2nid).
NIDs absent from aerolib.bin are skipped (unknown/unresolved symbols).
Known historic mislabels may be allowlisted with a one-line reason.
Run from the repository root:
python scripts/check_sysabi_aerolib.py
python scripts/check_sysabi_aerolib.py --strict
"""
from __future__ import annotations
import argparse
import hashlib
import re
import struct
import sys
from base64 import b64encode as base64enc
from binascii import unhexlify as uhx
from pathlib import Path
SRC_ROOT = Path("src")
AEROLIB_BIN = Path("src/SharpEmu.HLE/Aerolib/aerolib.bin")
SYSABI_EXPORT_RE = re.compile(r"\[SysAbiExport\((.*?)\)\]", re.DOTALL)
NID_RE = re.compile(r'Nid\s*=\s*"([^"]+)"')
EXPORT_NAME_RE = re.compile(r'ExportName\s*=\s*"([^"]+)"')
# NID -> reason. Keep minimal; fix ExportName when safe instead of growing this list.
ALLOWLISTED_NIDS: dict[str, str] = {
"KMcEa+rHsIo": "Historic kernel MapMemory stub bound to sceAvPlayerAddSource NID; API rewrite deferred.",
"WV1GwM32NgY": "Historic WebApi2 init alias for PushEventCreateHandle NID; ABI rewrite deferred.",
}
def name2nid(name: str) -> str:
symbol = hashlib.sha1(name.encode() + uhx("518D64A635DED8C1E6B039B1C3E55230")).digest()
id_val = struct.unpack("<Q", symbol[:8])[0]
nid = base64enc(uhx("%016x" % id_val), b"+-").rstrip(b"=")
return nid.decode("utf-8")
def find_repo_root() -> Path:
cwd = Path.cwd()
if (cwd / SRC_ROOT).is_dir() and (cwd / "scripts").is_dir():
return cwd
script_root = Path(__file__).resolve().parent.parent
if (script_root / SRC_ROOT).is_dir():
return script_root
raise SystemExit("Run from the repository root (src/ and scripts/ expected).")
def load_aerolib_nids(aerolib_path: Path) -> set[str]:
data = aerolib_path.read_bytes()
if len(data) < 4:
raise SystemExit(f"Aerolib binary too small: {aerolib_path}")
count = struct.unpack_from("<I", data, 0)[0]
offset = 4
nids: set[str] = set()
for _ in range(count):
if offset >= len(data):
raise SystemExit(f"Truncated aerolib.bin while reading NIDs: {aerolib_path}")
nid_len = data[offset]
offset += 1
nid = data[offset : offset + nid_len].decode("utf-8")
offset += nid_len
if offset + 2 > len(data):
raise SystemExit(f"Truncated aerolib.bin name length: {aerolib_path}")
name_len = struct.unpack_from("<H", data, offset)[0]
offset += 2 + name_len
nids.add(nid)
return nids
def iter_sysabi_exports(cs_path: Path, text: str):
for match in SYSABI_EXPORT_RE.finditer(text):
block = match.group(1)
nid_match = NID_RE.search(block)
export_match = EXPORT_NAME_RE.search(block)
if nid_match is None or export_match is None:
continue
nid = nid_match.group(1)
export_name = export_match.group(1)
nid_attr = f'Nid = "{nid}"'
abs_pos = text.find(nid_attr, match.start(), match.end())
if abs_pos < 0:
abs_pos = match.start()
line = text.count("\n", 0, abs_pos) + 1
yield cs_path, line, nid, export_name
def scan(src_root: Path, catalog_nids: set[str]):
checked = 0
mismatches = []
skipped_no_catalog = 0
allowlisted = 0
for cs_path in sorted(src_root.rglob("*.cs")):
text = cs_path.read_text(encoding="utf-8")
for path, line, nid, export_name in iter_sysabi_exports(cs_path, text):
checked += 1
computed = name2nid(export_name)
if computed == nid:
continue
if nid not in catalog_nids:
skipped_no_catalog += 1
continue
if nid in ALLOWLISTED_NIDS:
allowlisted += 1
continue
mismatches.append((path, line, nid, export_name, computed))
return checked, mismatches, skipped_no_catalog, allowlisted
def main() -> int:
parser = argparse.ArgumentParser(
description="Check that SysAbiExport ExportName values hash to their Nid via name2nid."
)
parser.add_argument(
"--strict",
action="store_true",
help="Exit 1 when any non-skipped/non-allowlisted ExportName does not hash to its Nid.",
)
parser.add_argument(
"--quiet",
action="store_true",
help="Print only the summary line.",
)
args = parser.parse_args()
repo_root = find_repo_root()
aerolib_path = repo_root / AEROLIB_BIN
if not aerolib_path.is_file():
raise SystemExit(f"Missing Aerolib catalog: {aerolib_path.as_posix()}")
catalog_nids = load_aerolib_nids(aerolib_path)
checked, mismatches, skipped_no_catalog, allowlisted = scan(
repo_root / SRC_ROOT, catalog_nids
)
ok = checked - len(mismatches) - skipped_no_catalog - allowlisted
if not args.quiet:
for path, line, nid, export_name, computed in mismatches:
rel = path.relative_to(repo_root).as_posix()
print(
f"{rel}:{line}: NID={nid} ExportName={export_name!r} "
f"computed={computed}"
)
print(
f"checked={checked} ok={ok} fail={len(mismatches)} "
f"skipped_no_catalog={skipped_no_catalog} allowlisted={allowlisted} "
f"allowlist_size={len(ALLOWLISTED_NIDS)}"
)
if args.strict and mismatches:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+7 -6
View File
@@ -3,8 +3,8 @@
#!/usr/bin/env python3
import struct
import hashlib
import struct
from base64 import b64encode as base64enc
from binascii import unhexlify as uhx
from pathlib import Path
@@ -21,7 +21,7 @@ def name2nid(name):
def generate():
names_path = Path(NAMES)
output_path = Path(OUTPUT)
entries = []
with open(names_path, 'r', encoding='utf-8') as f:
for line in f:
@@ -29,12 +29,12 @@ def generate():
if name:
nid = name2nid(name)
entries.append((nid, name))
print(f"Found {len(entries)} entries")
data = bytearray()
data.extend(struct.pack('<I', len(entries)))
for nid, name in entries:
nid_bytes = nid.encode('utf-8')
name_bytes = name.encode('utf-8')
@@ -42,10 +42,11 @@ def generate():
data.extend(nid_bytes)
data.extend(struct.pack('<H', len(name_bytes)))
data.extend(name_bytes)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(data)
print(f"Generated: {output_path} ({len(data):,} bytes)")
print(f"Total entries: {len(entries)}")
File diff suppressed because it is too large Load Diff
+434 -13
View File
@@ -5,15 +5,19 @@ using SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu;
using SharpEmu.GUI;
using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Logging;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
namespace SharpEmu.CLI;
internal static partial class Program
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
private static readonly object ConsoleMirrorSync = new();
private static StreamWriter? _consoleMirrorFile;
private const int DefaultImportTraceLimit = 32;
private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
@@ -21,11 +25,15 @@ internal static partial class Program
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
private const int JobObjectExtendedLimitInformation = 9;
private const int STARTF_USESTDHANDLES = 0x00000100;
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
private const string MitigatedChildEnvironment = "SHARPEMU_MITIGATED_CHILD";
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const int ATTACH_PARENT_PROCESS = -1;
private const int STD_INPUT_HANDLE = -10;
private const int STD_OUTPUT_HANDLE = -11;
private const int STD_ERROR_HANDLE = -12;
private const uint GENERIC_READ = 0x80000000;
@@ -43,6 +51,7 @@ internal static partial class Program
}
finally
{
DropConsoleFileMirror();
SharpEmuLog.Shutdown();
}
}
@@ -60,6 +69,11 @@ internal static partial class Program
// 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");
@@ -68,14 +82,22 @@ internal static partial class Program
return childExitCode;
}
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel))
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
{
PrintUsage();
return 1;
}
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
{
TryEnableConsoleFileMirror(logFilePath);
}
SharpEmuLog.MinimumLevel = logLevel;
Log.Info(BuildInfo.Banner);
Log.Info(HostSystemInfo.Summary);
ebootPath = Path.GetFullPath(ebootPath);
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
@@ -90,8 +112,16 @@ internal static partial class Program
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
OrbisGen2Result result;
ConsoleCancelEventHandler? cancelHandler = null;
try
{
cancelHandler = (_, eventArgs) =>
{
eventArgs.Cancel = true;
VideoOutExports.NotifyHostInterrupt();
};
Console.CancelKeyPress += cancelHandler;
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
result = runtime.Run(ebootPath);
Console.Error.WriteLine($"[DEBUG] Result: {result}");
@@ -102,6 +132,13 @@ internal static partial class Program
Log.Error("SharpEmu failed to run.", ex);
return 3;
}
finally
{
if (cancelHandler is not null)
{
Console.CancelKeyPress -= cancelHandler;
}
}
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
@@ -160,6 +197,48 @@ internal static partial class Program
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)
@@ -189,6 +268,10 @@ internal static partial class Program
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
{
isMitigatedChild = false;
var trustedMitigatedChild = string.Equals(
Environment.GetEnvironmentVariable(MitigatedChildEnvironment),
"1",
StringComparison.Ordinal);
if (args.Length == 0)
{
return args;
@@ -199,7 +282,7 @@ internal static partial class Program
{
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
{
isMitigatedChild = true;
isMitigatedChild = trustedMitigatedChild;
continue;
}
@@ -238,9 +321,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;
@@ -248,7 +333,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;
@@ -256,8 +343,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));
@@ -272,24 +358,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
@@ -343,6 +436,8 @@ internal static partial class Program
}
finally
{
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
if (attributeList != 0)
{
DeleteProcThreadAttributeList(attributeList);
@@ -356,6 +451,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();
@@ -458,27 +785,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++)
@@ -508,6 +838,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -521,6 +852,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
logFilePath = null;
return false;
}
@@ -528,6 +860,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))
{
@@ -551,6 +900,7 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -573,11 +923,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;
}
@@ -589,6 +955,7 @@ internal static partial class Program
ebootPath = string.Empty;
runtimeOptions = default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
}
@@ -690,6 +1057,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(
@@ -774,6 +1191,10 @@ internal static partial class Program
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetStdHandle(int stdHandle, nint handle);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetHandleInformation(nint handle, uint mask, uint flags);
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern nint CreateFileW(
string fileName,
+8
View File
@@ -24,6 +24,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>0.0.1</Version>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
@@ -48,6 +51,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TargetPath>LICENSE.txt</TargetPath>
<Visible>False</Visible>
</Content>
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
<Visible>False</Visible>
</Content>
</ItemGroup>
<!-- Keep glfw as a loose file next to the executable; every other native
+9 -5
View File
@@ -4,9 +4,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.9, )",
"resolved": "10.0.9",
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
@@ -211,11 +211,15 @@
"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",
@@ -484,4 +488,4 @@
}
}
}
}
}
+6 -2
View File
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu;
@@ -41,16 +42,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
];
private readonly IVirtualMemory _virtualMemory;
private readonly IModuleManager _moduleManager;
private readonly IHostPlatform? _hostPlatform;
private INativeCpuBackend? _nativeCpuBackend;
public CpuDispatcher(
IVirtualMemory virtualMemory,
IModuleManager moduleManager,
INativeCpuBackend? nativeCpuBackend = null)
INativeCpuBackend? nativeCpuBackend = null,
IHostPlatform? hostPlatform = null)
{
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
_nativeCpuBackend = nativeCpuBackend;
_hostPlatform = hostPlatform;
}
public ulong? LastEntryPoint { get; private set; }
@@ -266,7 +270,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
entryFrameDiagnostic,
Environment.NewLine,
"CpuEngine: native-only");
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
if (_nativeCpuBackend.TryExecute(
context,
entryPoint,
@@ -8,6 +8,8 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu.Native;
@@ -37,13 +39,78 @@ public sealed partial class DirectExecutionBackend
}
}
private void RecordDeferredBootstrapTrace(
long dispatchIndex,
ulong op,
ulong symbolPointer,
ulong outputPointer,
ulong returnRip)
{
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
dispatchIndex,
op,
symbolPointer,
outputPointer,
returnRip);
_deferredBootstrapTraceWriteIndex =
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
{
_deferredBootstrapTraceCount++;
}
}
}
private void DrainDeferredBootstrapTraces()
{
if (!_logBootstrap)
{
return;
}
DeferredBootstrapTraceEntry[] pending;
lock (_deferredBootstrapTraceGate)
{
if (_deferredBootstrapTraceCount == 0)
{
return;
}
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
{
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
}
_deferredBootstrapTraceCount = 0;
}
foreach (var entry in pending)
{
var symbolText = "<unreadable>";
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
}
}
private void DumpRecentImportTrace()
{
if (_recentImportTraceCount == 0)
{
return;
}
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
for (int i = 0; i < _recentImportTraceCount; i++)
{
@@ -51,8 +118,8 @@ public sealed partial class DirectExecutionBackend
var entry = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(entry.Nid))
{
Console.Error.WriteLine(
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
Log.Info(
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
}
}
@@ -68,8 +135,9 @@ public sealed partial class DirectExecutionBackend
int num2 = 0;
List<ulong> list = new List<ulong>(16);
ulong num3 = scanStart;
MEMORY_BASIC_INFORMATION64 lpBuffer;
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
var hostMemory = ResolveDiagnosticsHostMemory();
HostRegionInfo lpBuffer;
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
{
ulong baseAddress = lpBuffer.BaseAddress;
ulong num4 = baseAddress + lpBuffer.RegionSize;
@@ -79,7 +147,7 @@ public sealed partial class DirectExecutionBackend
}
ulong value = Math.Max(num3, baseAddress);
ulong num5 = Math.Min(num4, scanEnd);
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
{
ulong num6 = AlignUp(value, 8uL);
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
@@ -91,12 +159,12 @@ public sealed partial class DirectExecutionBackend
list.Add(num7);
if (num2 < 32)
{
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
Log.Info($"Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
num2++;
}
if (num >= 16384)
{
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
Log.Warning($"Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
return list;
}
}
@@ -106,7 +174,7 @@ public sealed partial class DirectExecutionBackend
}
if (num != 0)
{
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
Log.Warning($"Suspicious unresolved pointer hits: {num}");
}
return list;
}
@@ -121,18 +189,18 @@ public sealed partial class DirectExecutionBackend
Span<byte> destination = stackalloc byte[128];
if (!cpuContext.Memory.TryRead(returnRip, destination))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
Log.Debug($"Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
return;
}
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
Log.Debug($"Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
{
int num = BitConverter.ToInt32(destination.Slice(2, 4));
ulong num2 = returnRip + 6 + (ulong)num;
if (cpuContext.TryReadUInt64(num2, out var value2))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
Log.Debug($"Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
}
}
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
@@ -141,7 +209,7 @@ public sealed partial class DirectExecutionBackend
ulong num4 = returnRip + 7 + (ulong)num3;
if (cpuContext.TryReadUInt64(num4, out var value3))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
Log.Debug($"Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
}
}
for (int i = 0; i + 6 <= destination.Length; i++)
@@ -153,7 +221,7 @@ public sealed partial class DirectExecutionBackend
ulong num7 = num6 + 6 + (ulong)num5;
if (cpuContext.TryReadUInt64(num7, out var value4))
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
Log.Debug($"Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
}
}
}
@@ -168,32 +236,33 @@ public sealed partial class DirectExecutionBackend
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
ulong callRip = returnRip + (ulong)i;
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
var importEntries = _importEntries;
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != target)
if (importEntries[importIndex].Address != target)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
Log.Debug(
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
Log.Debug(
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
}
break;
}
if (cpuContext.Memory.TryRead(target, targetBytes))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
Log.Debug(
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
{
@@ -201,25 +270,25 @@ public sealed partial class DirectExecutionBackend
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
Log.Debug(
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
{
if (_importEntries[importIndex].Address != slotTarget)
if (importEntries[importIndex].Address != slotTarget)
{
continue;
}
string nid = _importEntries[importIndex].Nid;
string nid = importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
Log.Debug(
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
}
else
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
Log.Debug(
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
}
break;
}
@@ -283,7 +352,7 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
{
return false;
}
@@ -292,7 +361,7 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
{
return false;
}
@@ -324,12 +393,12 @@ public sealed partial class DirectExecutionBackend
return true;
}
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
{
return false;
}
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
if (executable)
{
_knownExecutablePages.TryAdd(pageAddress, 0);
@@ -348,6 +417,14 @@ public sealed partial class DirectExecutionBackend
return (value + num) & ~num;
}
// Diagnostics helpers are static (reachable from static handler paths), so
// they use the platform injected into the backend active on this thread and
// fall back to the process-wide singleton only when no run is bound.
private static IHostMemory ResolveDiagnosticsHostMemory()
{
return _activeExecutionBackend?._hostMemory ?? HostPlatform.Current.Memory;
}
private static bool IsReadableProtection(uint protect)
{
if ((protect & 0x100) != 0 || (protect & 1) != 0)
@@ -9,7 +9,9 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Cpu.Native.Windows;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -22,12 +24,12 @@ public sealed partial class DirectExecutionBackend
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
{
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
if (_rawExceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
}
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
}
else
@@ -37,22 +39,22 @@ public sealed partial class DirectExecutionBackend
_handlerDelegate = VectoredHandler;
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
if (_exceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create exception handler trampoline");
}
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
if (_unhandledFilterStub == 0)
{
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
}
SetUnhandledExceptionFilter(_unhandledFilterStub);
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
}
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
@@ -60,8 +62,8 @@ public sealed partial class DirectExecutionBackend
try
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
@@ -100,10 +102,19 @@ public sealed partial class DirectExecutionBackend
return 0;
}
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
// 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 == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
}
@@ -118,10 +129,10 @@ public sealed partial class DirectExecutionBackend
switch (exceptionCode)
{
case 3221225477u:
case WindowsFaultCodes.AccessViolation:
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
break;
case 3221226505u:
case WindowsFaultCodes.FastFail:
{
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
@@ -131,21 +142,21 @@ public sealed partial class DirectExecutionBackend
}
}
ulong rax = ReadCtxU64(contextRecord, 120);
ulong rbx = ReadCtxU64(contextRecord, 144);
ulong rcx = ReadCtxU64(contextRecord, 128);
ulong rdx = ReadCtxU64(contextRecord, 136);
ulong rsi = ReadCtxU64(contextRecord, 168);
ulong rdi = ReadCtxU64(contextRecord, 176);
ulong rbp = ReadCtxU64(contextRecord, 160);
ulong r8 = ReadCtxU64(contextRecord, 184);
ulong r9 = ReadCtxU64(contextRecord, 192);
ulong r10 = ReadCtxU64(contextRecord, 200);
ulong r11 = ReadCtxU64(contextRecord, 208);
ulong r12 = ReadCtxU64(contextRecord, 216);
ulong r13 = ReadCtxU64(contextRecord, 224);
ulong r14 = ReadCtxU64(contextRecord, 232);
ulong r15 = ReadCtxU64(contextRecord, 240);
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
Console.Error.WriteLine("[LOADER][INFO] =========================================");
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
@@ -176,7 +187,7 @@ public sealed partial class DirectExecutionBackend
ulong accessType = 0;
ulong target = 0;
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
{
accessType = *exceptionRecord->ExceptionInformation;
target = exceptionRecord->ExceptionInformation[1];
@@ -189,9 +200,9 @@ public sealed partial class DirectExecutionBackend
};
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
if (_hostMemory.Query(target, out var mbi))
{
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08}");
Console.Error.WriteLine($"[LOADER][INFO] AV target region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} state=0x{mbi.RawState:X08} protect=0x{mbi.RawProtection:X08}");
}
}
@@ -239,7 +250,7 @@ public sealed partial class DirectExecutionBackend
switch (exceptionCode)
{
case 3221225477u:
case WindowsFaultCodes.AccessViolation:
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
@@ -286,11 +297,11 @@ public sealed partial class DirectExecutionBackend
DumpGuestReferenceDiagnostics();
DumpGuestPointerWindowDiagnostics();
break;
case 2147483651u:
case WindowsFaultCodes.Breakpoint:
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
break;
case 3221225501u:
case WindowsFaultCodes.IllegalInstruction:
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
break;
}
@@ -323,8 +334,8 @@ public sealed partial class DirectExecutionBackend
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 rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
Console.Error.WriteLine(
@@ -470,7 +481,7 @@ public sealed partial class DirectExecutionBackend
ulong address = scanBase;
while (address < scanEnd)
{
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
if (!_hostMemory.Query(address, out var mbi))
{
break;
}
@@ -482,9 +493,9 @@ public sealed partial class DirectExecutionBackend
break;
}
if (mbi.State == MEM_COMMIT &&
IsReadableProtection(mbi.Protect) &&
IsExecutableProtection(mbi.Protect))
if (mbi.State == HostRegionState.Committed &&
IsReadableProtection(mbi.RawProtection) &&
IsExecutableProtection(mbi.RawProtection))
{
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
}
@@ -789,13 +800,13 @@ public sealed partial class DirectExecutionBackend
return false;
}
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
if (!_hostMemory.Query(address, out var mbi))
{
return false;
}
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
{
return false;
}
@@ -907,25 +918,25 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
if (!_hostMemory.Query(faultAddress, out var mbi))
{
return false;
}
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
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}");
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.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection:X08}");
}
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
{
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
}
return true;
}
@@ -934,10 +945,10 @@ public sealed partial class DirectExecutionBackend
ulong committedBase = 0;
ulong committedSize = 0;
if (mbi.State == 65536)
if (mbi.State == HostRegionState.Free)
{
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
{
committed = true;
committedBase = windowBase;
@@ -946,7 +957,7 @@ public sealed partial class DirectExecutionBackend
else
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
@@ -957,13 +968,13 @@ public sealed partial class DirectExecutionBackend
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
@@ -976,7 +987,7 @@ public sealed partial class DirectExecutionBackend
return false;
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
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}");
@@ -984,13 +995,13 @@ public sealed partial class DirectExecutionBackend
return true;
}
if (mbi.State != 8192)
if (mbi.State != HostRegionState.Reserved)
{
return false;
}
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
{
committed = true;
committedBase = commitWindowBase;
@@ -999,7 +1010,7 @@ public sealed partial class DirectExecutionBackend
else
{
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
@@ -1010,19 +1021,19 @@ public sealed partial class DirectExecutionBackend
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 8192uL;
}
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
@@ -1035,7 +1046,7 @@ public sealed partial class DirectExecutionBackend
return false;
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
@@ -1076,31 +1087,33 @@ public sealed partial class DirectExecutionBackend
return true;
}
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
// The commit protection is one of the two raw values ResolveLazyCommitProtection
// produces (0x40 RWX / 0x04 RW); the enum mapping reproduces those exactly.
static bool TryCommitRange(IHostMemory hostMemory, ulong baseAddress, ulong length, uint protection)
{
if (length == 0)
{
return false;
}
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
}
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
{
if (length == 0)
{
return false;
}
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
}
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
{
if (!TryReserveRange(reserveAddress, reserveSize))
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
{
return false;
}
return TryCommitRange(commitAddress, commitSize, protection);
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
}
static bool IsAccessCompatible(ulong accessType, uint protection)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,562 @@
// 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;
using SharpEmu.HLE.Host;
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;
// 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)
{
_hostThreading.SetTlsValue(_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 (!EnsureHostRuntimeExports(backend._hostSymbols))
{
return null;
}
var executor = new NativeGuestExecutor(backend);
if (!executor.Initialize())
{
executor.Dispose();
return null;
}
return executor;
}
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
{
if (_exitThreadAddress != 0)
{
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
}
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
}
private bool Initialize()
{
_selfHandle = GCHandle.Alloc(this);
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
if (_controlBlock == null)
{
return false;
}
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
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 (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
{
return false;
}
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
_threadHandle = _backend._hostThreading.CreateNativeThread(
(nint)_loopStub,
0,
WorkerStackReservation,
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 = backend._hostThreading.GetTlsValue(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!);
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (_runState is { } state)
{
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
}
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);
}
_backend._hostThreading.SetTlsValue(_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 = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
_backend._hostThreading.CloseThreadHandle(_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)
{
_backend._hostMemory.Free((ulong)_loopStub);
_loopStub = null;
}
if (_controlBlock != null)
{
_backend._hostMemory.Free((ulong)_controlBlock);
_controlBlock = null;
}
if (_selfHandle.IsAllocated)
{
_selfHandle.Free();
}
_workAvailable.Dispose();
_workCompleted.Dispose();
}
}
}
File diff suppressed because it is too large Load Diff
+6 -31
View File
@@ -3,6 +3,7 @@
using System.Runtime.InteropServices;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -11,17 +12,15 @@ public sealed unsafe class StubManager : IDisposable
private readonly List<nint> _allocatedStubs = new();
private readonly Dictionary<string, nint> _importHandlers = new();
private readonly Dictionary<ulong, nint> _stubAddresses = new();
private readonly IHostMemory _hostMemory;
private byte* _pltMemory;
private int _pltOffset;
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
public StubManager()
public StubManager(IHostMemory? hostMemory = null)
{
_pltMemory = (byte*)VirtualAlloc(
null,
(nuint)PltMemorySize,
AllocationType.Reserve | AllocationType.Commit,
MemoryProtection.ExecuteReadWrite);
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
if (_pltMemory == null)
{
@@ -185,7 +184,7 @@ public sealed unsafe class StubManager : IDisposable
{
if (_pltMemory != null)
{
VirtualFree(_pltMemory, 0, FreeType.Release);
_hostMemory.Free((ulong)_pltMemory);
_pltMemory = null;
}
@@ -194,29 +193,5 @@ public sealed unsafe class StubManager : IDisposable
_stubAddresses.Clear();
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType);
[Flags]
private enum AllocationType : uint
{
Commit = 0x1000,
Reserve = 0x2000,
}
[Flags]
private enum MemoryProtection : uint
{
ExecuteReadWrite = 0x40,
}
private enum FreeType : uint
{
Release = 0x8000,
}
public delegate void ImportHandler(CpuContext context);
}
@@ -0,0 +1,32 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Byte offsets into the Win64 CONTEXT record delivered to vectored exception
/// handlers. The handlers read/write guest registers directly at these offsets
/// (no managed CONTEXT struct exists); a future POSIX backend gets a sibling
/// class for its mcontext layout.
/// </summary>
internal static class Win64ContextOffsets
{
public const int Mxcsr = 52;
public const int Rax = 120;
public const int Rcx = 128;
public const int Rdx = 136;
public const int Rbx = 144;
public const int Rsp = 152;
public const int Rbp = 160;
public const int Rsi = 168;
public const int Rdi = 176;
public const int R8 = 184;
public const int R9 = 192;
public const int R10 = 200;
public const int R11 = 208;
public const int R12 = 216;
public const int R13 = 224;
public const int R14 = 232;
public const int R15 = 240;
public const int Rip = 248;
}
@@ -0,0 +1,24 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Windows NTSTATUS exception codes and EXCEPTION_RECORD access-type values the
/// fault handlers filter on. Values are the same numbers the handlers previously
/// compared as bare literals; only the spelling changed.
/// </summary>
internal static class WindowsFaultCodes
{
public const uint AccessViolation = 0xC0000005u; // 3221225477
public const uint Breakpoint = 0x80000003u; // 2147483651
public const uint IllegalInstruction = 0xC000001Du; // 3221225501
public const uint FastFail = 0xC0000409u; // 3221226505
public const uint StackOverflow = 0xC00000FDu;
public const uint ClrManagedException = 0xE0434352u;
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
public const ulong AccessRead = 0;
public const ulong AccessWrite = 1;
public const ulong AccessExecute = 8;
}
@@ -0,0 +1,191 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Vectored-exception-handler installation and the handler pre-filter thunk.
/// The thunk is inherently Windows-shaped (TEB stack-limit reads via gs:,
/// NTSTATUS pre-filtering, Win64 calling convention) and moved here whole from
/// DirectExecutionBackend; a POSIX backend supplies a sibling built around
/// sigaction/sigaltstack instead.
/// </summary>
internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
{
private readonly IHostMemory _memory;
public WindowsFaultHandling(IHostMemory memory)
{
_memory = memory;
}
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
{
const uint stubSize = 256u;
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
if (ptr == null)
{
return 0;
}
byte* code = (byte*)ptr;
int offset = 0;
// Native pre-filter: these exception codes are raised while the thread can be in
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
// code" — this is why no managed throw (even one with a catch handler) ever
// survived inside the emulator. Continue the handler search without touching
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
// returned CONTINUE_SEARCH for them.
ReadOnlySpan<uint> nonManagedExceptionCodes =
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
EmitByte(code, ref offset, 0x74); // je pass
passJumpOffsets[i] = offset;
EmitByte(code, ref offset, 0x00);
}
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
int passOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
EmitByte(code, ref offset, 0xC3); // ret
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
}
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, 8u);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x83); // jae guestStack
int aboveStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[0x10]
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, 0x10u);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x82); // jb guestStack
int belowStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = managedCallback;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xE9);
int hostRestoreJump = offset;
EmitUInt32(code, ref offset, 0u);
int guestStackOffset = offset;
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xB9);
EmitUInt32(code, ref offset, hostRspSwitchTlsSlot);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = tlsGetValueAddress;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
int missingTlsJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x18); // mov r11, [rax]
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xDB); // test r11, r11
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
int missingHostStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = managedCallback;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xE9);
int guestRestoreJump = offset;
EmitUInt32(code, ref offset, 0u);
int passThroughOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
int restoreOffset = offset;
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov rsp, r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
EmitByte(code, ref offset, 0xC3);
*(int*)(code + aboveStackJump) = guestStackOffset - (aboveStackJump + sizeof(int));
*(int*)(code + belowStackJump) = guestStackOffset - (belowStackJump + sizeof(int));
*(int*)(code + hostRestoreJump) = restoreOffset - (hostRestoreJump + sizeof(int));
*(int*)(code + missingTlsJump) = passThroughOffset - (missingTlsJump + sizeof(int));
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
if (!_memory.Protect((ulong)ptr, stubSize, HostPageProtection.ReadExecute, out _))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
_ = _memory.Free((ulong)ptr);
return 0;
}
_memory.FlushInstructionCache((ulong)ptr, (ulong)offset);
return (nint)ptr;
}
public void FreeThunk(nint thunk)
{
_ = _memory.Free((ulong)thunk);
}
public nint AddFirstChanceHandler(nint thunk)
{
return (nint)AddVectoredExceptionHandler(1u, thunk);
}
public void RemoveHandler(nint handle)
{
_ = RemoveVectoredExceptionHandler((void*)handle);
}
public void SetUnhandledFilter(nint thunk)
{
_ = SetUnhandledExceptionFilter(thunk);
}
private static void EmitByte(byte* code, ref int offset, byte value)
{
code[offset++] = value;
}
private static void EmitUInt32(byte* code, ref int offset, uint value)
{
*(uint*)(code + offset) = value;
offset += sizeof(uint);
}
[LibraryImport("kernel32.dll")]
private static partial void* AddVectoredExceptionHandler(uint first, IntPtr handler);
[LibraryImport("kernel32.dll")]
private static partial uint RemoveVectoredExceptionHandler(void* handle);
[LibraryImport("kernel32.dll")]
private static partial IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
}
+6 -1
View File
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator, ICpuMemoryWrapper
{
private readonly ICpuMemory _inner;
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
address = 0;
return false;
}
public bool TryFreeGuestMemory(ulong address)
{
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
}
}
+39 -15
View File
@@ -45,7 +45,13 @@ public static class Ps5ParamJsonReader
try
{
using var doc = JsonDocument.Parse(data);
ReadOnlyMemory<byte> json = data;
if (json.Span.StartsWith("\uFEFF"u8))
{
json = json[3..];
}
using var doc = JsonDocument.Parse(json);
return TryReadPs5Param(doc.RootElement);
}
catch (JsonException)
@@ -56,12 +62,15 @@ public static class Ps5ParamJsonReader
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
{
string? titleId = root.TryGetProperty("titleId", out var eTid) ? eTid.GetString() : null;
if (root.ValueKind != JsonValueKind.Object)
return (null, null, null);
var titleId = GetString(root, "titleId");
string? ver =
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
GetString(root, "contentVersion")
?? GetString(root, "masterVersion")
?? GetString(root, "targetContentVersion");
string? title = ExtractTitleName(root);
@@ -70,34 +79,49 @@ public static class Ps5ParamJsonReader
private static string? ExtractTitleName(JsonElement root)
{
if (!root.TryGetProperty("localizedParameters", out var lp))
if ((!root.TryGetProperty("localizedParameters", out var lp) || lp.ValueKind != JsonValueKind.Object) &&
root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
{
if (root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
{
disc.TryGetProperty("localizedParameters", out lp);
}
disc.TryGetProperty("localizedParameters", out lp);
}
if (lp.ValueKind != JsonValueKind.Object)
return null;
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
var defLang = GetString(lp, "defaultLanguage");
if (!string.IsNullOrEmpty(defLang))
{
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
{
if (langObj.TryGetProperty("titleName", out var tn))
return tn.GetString();
var title = GetString(langObj, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
}
}
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
{
if (en.TryGetProperty("titleName", out var tn2))
return tn2.GetString();
var title = GetString(en, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
}
foreach (var property in lp.EnumerateObject())
{
if (property.Value.ValueKind == JsonValueKind.Object)
{
var title = GetString(property.Value, "titleName");
if (!string.IsNullOrWhiteSpace(title))
return title;
}
}
return null;
}
private static string? GetString(JsonElement parent, string propertyName) =>
parent.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
}
+60 -47
View File
@@ -10,12 +10,16 @@ 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 const uint SelfMagic = 0x4F153D1D;
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
private const uint ElfMagic = 0x7F454C46;
private const uint Ps4SelfMagic = 0x4F153D1D;
private const uint Ps5SelfMagic = 0x5414F5EE;
private const ulong SelfSegmentFlag = 0x800;
private const int PageSize = 0x1000;
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
@@ -155,7 +159,7 @@ public sealed class SelfLoader : ISelfLoader
}
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
Console.Error.WriteLine($"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
var loadContext = ParseLayout(imageData);
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
@@ -255,7 +259,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,
@@ -321,7 +325,8 @@ public sealed class SelfLoader : ISelfLoader
throw new InvalidDataException("Input image is too small to contain an ELF header.");
}
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
if (magic is Ps4SelfMagic or Ps5SelfMagic)
{
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
@@ -343,6 +348,12 @@ public sealed class SelfLoader : ISelfLoader
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
}
if (magic != ElfMagic)
{
throw new InvalidDataException(
$"Unsupported executable signature 0x{magic:X8}");
}
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
}
@@ -403,15 +414,15 @@ public sealed class SelfLoader : ISelfLoader
var virtualAddress = header.VirtualAddress + imageBase;
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
if (header.Alignment > 1)
{
var vaddrMod = virtualAddress % header.Alignment;
var offsetMod = header.Offset % header.Alignment;
if (vaddrMod != offsetMod)
{
Console.Error.WriteLine(
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
Log.Warning(
$"Segment {index} ELF alignment mismatch! " +
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
}
@@ -671,13 +682,13 @@ public sealed class SelfLoader : ISelfLoader
{
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
{
Console.Error.WriteLine(
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
Log.Debug(
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
}
else
{
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
}
}
@@ -689,8 +700,8 @@ public sealed class SelfLoader : ISelfLoader
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
{
Console.Error.WriteLine(
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
Log.Debug(
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
}
}
@@ -783,15 +794,15 @@ public sealed class SelfLoader : ISelfLoader
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Console.Error.WriteLine(
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
Log.Debug(
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
}
if (!IsSupportedRelocationType(relocation.Type))
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
}
continue;
}
@@ -800,7 +811,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
Log.Debug("[FOCUS][SKIP] target address not mapped");
}
continue;
}
@@ -840,7 +851,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
}
continue;
}
@@ -852,10 +863,9 @@ public sealed class SelfLoader : ISelfLoader
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Console.Error.WriteLine(
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
Log.Warning(
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
continue;
}
descriptors.Add(new RelocationDescriptor(
@@ -873,10 +883,9 @@ public sealed class SelfLoader : ISelfLoader
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Console.Error.WriteLine(
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
Log.Warning(
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
continue;
}
descriptors.Add(new RelocationDescriptor(
@@ -893,7 +902,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
}
continue;
}
@@ -902,7 +911,7 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
}
continue;
}
@@ -950,8 +959,8 @@ public sealed class SelfLoader : ISelfLoader
if (sectionSymbols > 0 || dynamicSymbols > 0)
{
Console.Error.WriteLine(
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
Log.Info(
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
}
}
@@ -1025,8 +1034,8 @@ public sealed class SelfLoader : ISelfLoader
if (preInitializers.Count != 0 || initializers.Count != 0)
{
Console.Error.WriteLine(
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
Log.Info(
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
}
}
@@ -1409,8 +1418,8 @@ public sealed class SelfLoader : ISelfLoader
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
Console.Error.WriteLine(
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
Log.Debug(
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
if (mapSize > int.MaxValue)
{
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
@@ -1842,16 +1851,16 @@ public sealed class SelfLoader : ISelfLoader
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
var guestAddr = location + imageBase;
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
if (virtualMemory.TryRead(guestAddr, tableBytes))
{
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
return true;
}
if (virtualMemory.TryRead(location, tableBytes))
{
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
return true;
}
@@ -1859,11 +1868,11 @@ public sealed class SelfLoader : ISelfLoader
{
var slice = elfData.Slice((int)location, (int)size);
tableBytes = slice.ToArray();
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
return true;
}
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
tableBytes = Array.Empty<byte>();
return false;
}
@@ -1920,17 +1929,17 @@ public sealed class SelfLoader : ISelfLoader
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
{
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
if (address == 0)
{
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
Log.Debug("-> return 0 (null)");
return 0;
}
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
{
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
Log.Debug($"-> resolved raw 0x{resolved:X16}");
return resolved;
}
@@ -1939,18 +1948,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;
}
@@ -2380,10 +2389,14 @@ public sealed class SelfLoader : ISelfLoader
public ulong FileSize => _fileSize;
public bool HasKnownLayout =>
_ident0 == 0x4F &&
_ident1 == 0x15 &&
_ident2 == 0x3D &&
_ident3 == 0x1D &&
((_ident0 == 0x4F &&
_ident1 == 0x15 &&
_ident2 == 0x3D &&
_ident3 == 0x1D) ||
(_ident0 == 0x54 &&
_ident1 == 0x14 &&
_ident2 == 0xF5 &&
_ident3 == 0xEE)) &&
_ident4 == 0x00 &&
_ident5 == 0x01 &&
_ident6 == 0x01 &&
+249 -95
View File
@@ -4,11 +4,12 @@
using System.Runtime.InteropServices;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging;
namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IGuestAddressSpace, IDisposable
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
@@ -28,41 +29,27 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RESERVE = 0x2000;
private const uint MEM_RELEASE = 0x8000;
// Raw Windows PAGE_* values retained for the internal region/protection
// bookkeeping: regions and saved old-protection values always carry the raw
// value of the host platform in use, and these classification helpers only
// ever see values this class itself assigned (see IHostMemory.ProtectRaw).
private const uint PAGE_EXECUTE_READ = 0x20;
private const uint PAGE_EXECUTE_READWRITE = 0x40;
private const uint PAGE_EXECUTE = 0x10;
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
private const uint PAGE_NOACCESS = 0x01;
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_READONLY = 0x02;
private readonly IHostMemory _hostMemory;
private ulong _guestAllocationArenaBase;
private ulong _guestAllocationOffset;
private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[DllImport("kernel32.dll")]
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 PhysicalVirtualMemory(IHostMemory? hostMemory = null)
{
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
}
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
{
@@ -74,17 +61,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var allocationType = MEM_COMMIT | MEM_RESERVE;
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
if (result == null)
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
if (result == 0)
{
return false;
}
actualAddress = (ulong)result;
actualAddress = result;
if (actualAddress != desiredAddress)
{
VirtualFree(result, 0, MEM_RELEASE);
_hostMemory.Free(result);
actualAddress = 0;
return false;
}
@@ -119,33 +106,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var alignedSize = (size + 0xFFF) & ~0xFFFUL;
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var allocationType = MEM_COMMIT | MEM_RESERVE;
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
var reservedOnly = false;
var preferReserveOnly = !executable &&
alignedSize >= LargeDataReserveThreshold &&
alignedSize > FullCommitRegionLimit;
void* result = null;
ulong result = 0;
if (preferReserveOnly)
{
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
if (result == null && allowAlternative)
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
if (result == 0 && allowAlternative)
{
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
}
if (result != null)
if (result != 0)
{
reservedOnly = true;
}
}
if (result == null)
if (result == 0)
{
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection);
result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
}
if (result == null)
if (result == 0)
{
if (!allowAlternative)
{
@@ -153,32 +140,32 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
result = _hostMemory.Allocate(0, alignedSize, hostProtection);
if (result == null)
if (result == 0)
{
if (!executable)
{
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
if (result == null && allowAlternative)
result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
if (result == 0 && allowAlternative)
{
result = VirtualAlloc(null, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE);
result = _hostMemory.Reserve(0, alignedSize, HostPageProtection.ReadWrite);
}
if (result != null)
if (result != 0)
{
reservedOnly = true;
}
}
if (result == null)
if (result == 0)
{
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
}
}
}
var actualAddress = (ulong)result;
var actualAddress = result;
var lazyPrimeState = "n/a";
if (reservedOnly)
@@ -191,9 +178,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{
var remaining = primeBytes - committedBytes;
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
var commitAddress = (void*)(actualAddress + committedBytes);
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE);
if (committed == null)
var commitAddress = actualAddress + committedBytes;
if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
{
break;
}
@@ -316,7 +302,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
GuestAllocationArenaSize,
executable: false,
allowAlternative: true);
_guestAllocationOffset = GuestAllocationArenaStartOffset;
_guestAllocationFreeRanges.Add(
GuestAllocationArenaStartOffset,
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
}
catch (Exception)
{
@@ -324,18 +312,128 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
ulong rangeOffset = 0;
ulong rangeSize = 0;
ulong alignedOffset = 0;
var found = false;
foreach (var range in _guestAllocationFreeRanges)
{
alignedOffset = AlignUp(range.Key, alignment);
if (alignedOffset >= range.Key &&
alignedOffset - range.Key <= range.Value &&
size <= range.Value - (alignedOffset - range.Key))
{
rangeOffset = range.Key;
rangeSize = range.Value;
found = true;
break;
}
}
if (!found)
{
return false;
}
_guestAllocationFreeRanges.Remove(rangeOffset);
if (alignedOffset > rangeOffset)
{
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
}
var allocationEnd = alignedOffset + size;
var rangeEnd = rangeOffset + rangeSize;
if (allocationEnd < rangeEnd)
{
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
}
address = _guestAllocationArenaBase + alignedOffset;
_guestAllocationOffset = alignedOffset + size;
_guestAllocations.Add(address, (alignedOffset, size));
return true;
}
}
public bool TryFreeGuestMemory(ulong address)
{
lock (_guestAllocationGate)
{
if (!_guestAllocations.Remove(address, out var allocation))
{
return false;
}
var freeOffset = allocation.Offset;
var freeSize = allocation.Size;
ulong? previousOffset = null;
ulong? nextOffset = null;
foreach (var range in _guestAllocationFreeRanges)
{
if (range.Key < freeOffset)
{
previousOffset = range.Key;
continue;
}
nextOffset = range.Key;
break;
}
if (previousOffset is { } previous &&
previous + _guestAllocationFreeRanges[previous] == freeOffset)
{
freeOffset = previous;
freeSize += _guestAllocationFreeRanges[previous];
_guestAllocationFreeRanges.Remove(previous);
}
if (nextOffset is { } next && freeOffset + freeSize == next)
{
freeSize += _guestAllocationFreeRanges[next];
_guestAllocationFreeRanges.Remove(next);
}
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
return true;
}
}
public bool TryProtect(ulong address, ulong size, GuestPageProtection protection)
{
if (size == 0)
{
return false;
}
return _hostMemory.Protect(address, size, ResolveProtection(protection), out _);
}
// Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection
// performed before this seam existed; the Windows backend maps each case back
// to the identical PAGE_* value.
private static HostPageProtection ResolveProtection(GuestPageProtection protection)
{
var read = (protection & GuestPageProtection.Read) != 0;
var write = (protection & GuestPageProtection.Write) != 0;
var execute = (protection & GuestPageProtection.Execute) != 0;
if (execute)
{
return write
? HostPageProtection.ReadWriteExecute
: read
? HostPageProtection.ReadExecute
: HostPageProtection.Execute;
}
return write
? HostPageProtection.ReadWrite
: read
? HostPageProtection.ReadOnly
: HostPageProtection.NoAccess;
}
public void Clear()
{
lock (_guestAllocationGate)
@@ -345,7 +443,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{
foreach (var region in _regions)
{
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
_hostMemory.Free(region.VirtualAddress);
}
_regions.Clear();
_pageProtections.Clear();
@@ -360,7 +458,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
_guestAllocationArenaBase = 0;
_guestAllocationOffset = 0;
_guestAllocationFreeRanges.Clear();
_guestAllocations.Clear();
}
}
@@ -419,46 +518,67 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
{
var runStart = mapStart;
var runFlags = ProgramHeaderFlags.None;
var hasRun = false;
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
{
_pageProtections.TryGetValue(pageAddress, out var existingFlags);
var mergedFlags = existingFlags | flags;
_pageProtections[pageAddress] = mergedFlags;
SetProtection(pageAddress, PageSize, mergedFlags);
if (!hasRun)
{
runStart = pageAddress;
runFlags = mergedFlags;
hasRun = true;
}
else if (mergedFlags != runFlags)
{
SetProtection(runStart, pageAddress - runStart, runFlags);
runStart = pageAddress;
runFlags = mergedFlags;
}
}
if (hasRun)
{
SetProtection(runStart, mapEnd - runStart, runFlags);
}
}
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
{
uint protection;
HostPageProtection protection;
if (flags == ProgramHeaderFlags.None)
{
protection = PAGE_NOACCESS;
protection = HostPageProtection.NoAccess;
}
else if ((flags & ProgramHeaderFlags.Execute) != 0)
{
protection = (flags & ProgramHeaderFlags.Write) != 0
? PAGE_EXECUTE_READWRITE
: PAGE_EXECUTE_READ;
? HostPageProtection.ReadWriteExecute
: HostPageProtection.ReadExecute;
}
else if ((flags & ProgramHeaderFlags.Write) != 0)
{
protection = PAGE_READWRITE;
protection = HostPageProtection.ReadWrite;
}
else
{
protection = PAGE_READONLY;
protection = HostPageProtection.ReadOnly;
}
if (!VirtualProtect((void*)address, (nuint)size, protection, out _))
if (!_hostMemory.Protect(address, size, protection, out _))
{
throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
}
if ((flags & ProgramHeaderFlags.Execute) != 0)
{
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
_hostMemory.FlushInstructionCache(address, size);
}
}
@@ -550,6 +670,47 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected)
{
_gate.EnterReadLock();
try
{
var region = FindRegion(virtualAddress, (ulong)expected.Length);
if (region is null ||
!TryResolveRegionOffset(
virtualAddress,
(ulong)expected.Length,
region,
out var offset))
{
return false;
}
if (expected.IsEmpty)
{
return true;
}
var srcPtr = (void*)(region.VirtualAddress + offset);
if (region.IsReservedOnly &&
!EnsureRangeCommitted((ulong)srcPtr, (ulong)expected.Length, region))
{
return false;
}
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)expected.Length, region))
{
return false;
}
return new ReadOnlySpan<byte>(srcPtr, expected.Length).SequenceEqual(expected);
}
finally
{
_gate.ExitReadLock();
}
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
var requiresExclusiveAccess = false;
@@ -689,7 +850,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true;
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
if (!_hostMemory.Protect((ulong)destPtr, (ulong)source.Length, HostPageProtection.ReadWriteExecute, out var oldProtect))
{
return false;
}
@@ -703,10 +864,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
finally
{
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
_hostMemory.ProtectRaw((ulong)destPtr, (ulong)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
_hostMemory.FlushInstructionCache((ulong)destPtr, (ulong)source.Length);
}
}
@@ -728,9 +889,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
return FindRegion(virtualAddress, 1) is not null
? (void*)virtualAddress
: null;
var region = FindRegion(virtualAddress, 1);
if (region is null ||
(region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
{
return null;
}
return (void*)virtualAddress;
}
finally
{
@@ -932,12 +1098,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
}
private static uint GetCommitProtection(MemoryRegion region)
private static HostPageProtection GetCommitProtection(MemoryRegion region)
{
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
return region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
}
private static unsafe bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
private bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
{
if (size == 0 || !region.IsReservedOnly)
{
@@ -951,7 +1117,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var pageAddress = startPage;
while (pageAddress < endPage)
{
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
if (!_hostMemory.Query(pageAddress, out var info))
{
return false;
}
@@ -965,19 +1131,19 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false;
}
if (info.State == MEM_COMMIT)
if (info.State == HostRegionState.Committed)
{
pageAddress = rangeEnd;
continue;
}
if (info.State != MEM_RESERVE)
if (info.State != HostRegionState.Reserved)
{
return false;
}
var commitSize = rangeEnd - pageAddress;
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
if (!_hostMemory.Commit(pageAddress, commitSize, commitProtection))
{
return false;
}
@@ -998,11 +1164,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
var temporaryProtection = region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var temporaryProtection = region.IsExecutable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
{
if (!VirtualProtect((void*)pageAddress, (nuint)PageSize, temporaryProtection, out var oldProtection))
if (!_hostMemory.Protect(pageAddress, PageSize, temporaryProtection, out var oldProtection))
{
RestorePageProtections(touchedPages);
touchedPages.Clear();
@@ -1015,11 +1181,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true;
}
private static void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
private void RestorePageProtections(List<(ulong Address, uint Protection)> touchedPages)
{
foreach (var (pageAddress, protection) in touchedPages)
{
VirtualProtect((void*)pageAddress, (nuint)PageSize, protection, out _);
_hostMemory.ProtectRaw(pageAddress, PageSize, protection, out _);
}
}
@@ -1076,16 +1242,4 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public uint Protection { get; set; }
}
private struct MemoryBasicInformation64
{
public ulong BaseAddress;
public ulong AllocationBase;
public uint AllocationProtect;
public uint Alignment1;
public ulong RegionSize;
public uint State;
public uint Protect;
public uint Type;
public uint Alignment2;
}
}
+116 -29
View File
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
lock (_gate)
{
foreach (var existing in _regions)
var insertionIndex = FindInsertionIndex(virtualAddress);
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
{
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress)
{
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
}
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
}
_regions.Add(new MappedRegion(
_regions.Insert(insertionIndex, new MappedRegion(
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
endAddress,
backingMemory));
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
{
lock (_gate)
{
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset))
if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
{
return false;
}
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination);
CopyFromRegions(virtualAddress, destination, regionIndex);
return true;
}
}
@@ -88,39 +87,127 @@ public sealed class VirtualMemory : IVirtualMemory
{
lock (_gate)
{
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset))
if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
{
return false;
}
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length));
CopyToRegions(virtualAddress, source, regionIndex);
return true;
}
}
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset)
private bool TryValidateRange(
ulong virtualAddress,
int length,
ProgramHeaderFlags requiredProtection,
out int regionIndex)
{
foreach (var candidate in _regions)
regionIndex = FindContainingRegionIndex(virtualAddress);
if (regionIndex < 0)
{
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress)
{
continue;
}
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
if (candidateOffset + length > candidate.BackingMemory.Length)
{
break;
}
region = candidate;
offset = candidateOffset;
return true;
return false;
}
region = default;
offset = 0;
return false;
var currentAddress = virtualAddress;
var remaining = length;
var currentIndex = regionIndex;
while (true)
{
if (currentIndex >= _regions.Count)
{
return false;
}
var region = _regions[currentIndex];
if (currentAddress < region.Region.VirtualAddress ||
currentAddress >= region.EndAddress ||
(region.Region.Protection & requiredProtection) == 0)
{
return false;
}
if (remaining == 0)
{
return true;
}
var available = region.EndAddress - currentAddress;
var chunkLength = (int)Math.Min((ulong)remaining, available);
remaining -= chunkLength;
if (remaining == 0)
{
return true;
}
currentAddress += (ulong)chunkLength;
currentIndex++;
}
}
private int FindContainingRegionIndex(ulong virtualAddress)
{
var insertionIndex = FindInsertionIndex(virtualAddress);
if (insertionIndex < _regions.Count &&
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
{
return insertionIndex;
}
var candidateIndex = insertionIndex - 1;
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
? candidateIndex
: -1;
}
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
{
var copied = 0;
var currentAddress = virtualAddress;
while (copied < destination.Length)
{
var region = _regions[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
copied += chunkLength;
currentAddress += (ulong)chunkLength;
}
}
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
{
var copied = 0;
var currentAddress = virtualAddress;
while (copied < source.Length)
{
var region = _regions[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
copied += chunkLength;
currentAddress += (ulong)chunkLength;
}
}
private int FindInsertionIndex(ulong virtualAddress)
{
var lower = 0;
var upper = _regions.Count;
while (lower < upper)
{
var middle = lower + ((upper - lower) / 2);
if (_regions[middle].Region.VirtualAddress < virtualAddress)
{
lower = middle + 1;
}
else
{
upper = middle;
}
}
return lower;
}
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
+71 -4
View File
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
@@ -16,6 +17,7 @@ using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace SharpEmu.Core.Runtime;
@@ -82,18 +84,22 @@ 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();
var virtualMemory = new PhysicalVirtualMemory();
// Resolve the host platform once at the composition root; on unsupported
// OSes this throws PlatformNotSupportedException with a clear message
// instead of failing on the first native call.
var hostPlatform = HostPlatform.Current;
var virtualMemory = new PhysicalVirtualMemory(hostPlatform.Memory);
var fileSystem = new PhysicalFileSystem();
return new SharpEmuRuntime(
new SelfLoader(),
virtualMemory,
new CpuDispatcher(virtualMemory, moduleManager),
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
moduleManager,
Aerolib.Instance,
cpuExecutionOptions,
@@ -139,8 +145,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
@@ -357,6 +364,66 @@ 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";
+4 -1
View File
@@ -65,7 +65,10 @@
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project"
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
+28
View File
@@ -127,6 +127,34 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Top-level page switcher (Library / Options): plain transparent
buttons, not TabItem, so there is no Fluent selected-tab underline.
The active page is conveyed by brightness alone; LB/RB gamepad
hints flank the pair. -->
<Style Selector="Button.segment">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
<Setter Property="FontSize" Value="22" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Padding" Value="6,4" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Button.segment:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="Button.segment.active">
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
</Style>
<!-- Gamepad shoulder-button hint chip (LB/RB, L1/R1). -->
<Style Selector="Border.padHint">
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="8,3" />
</Style>
<Style Selector="ContextMenu">
<Setter Property="Background" Value="{StaticResource CardBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
+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;
}
}
}
+162
View File
@@ -0,0 +1,162 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Threading;
using System.Collections.Specialized;
namespace SharpEmu.GUI;
public sealed class ConsoleWindow : Window
{
private readonly AvaloniaList<LogLine> _sourceLines;
private readonly AvaloniaList<LogLine> _visibleLines = new();
private readonly ListBox _list;
private readonly TextBox _searchBox;
private readonly CheckBox _autoScrollCheck;
public ConsoleWindow(
AvaloniaList<LogLine> lines,
Action clear,
bool autoScroll)
{
var loc = Localization.Instance;
_sourceLines = lines;
Title = loc.Get("Console.WindowTitle");
Width = 980;
Height = 620;
MinWidth = 520;
MinHeight = 320;
Background = new SolidColorBrush(Color.Parse("#0D1017"));
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
_searchBox = new TextBox
{
Watermark = loc.Get("Console.SearchWatermark"),
Width = 320,
Margin = new Thickness(0, 0, 12, 0),
};
_autoScrollCheck = new CheckBox
{
Content = loc.Get("Console.AutoScroll"),
IsChecked = autoScroll,
FontSize = 12,
Margin = new Thickness(0, 0, 12, 0),
VerticalAlignment = VerticalAlignment.Center,
};
var copyButton = new Button
{
Classes = { "ghost" },
Content = loc.Get("Console.Copy"),
Padding = new Thickness(10, 4),
Margin = new Thickness(0, 0, 8, 0),
};
var clearButton = new Button
{
Classes = { "ghost" },
Content = loc.Get("Console.Clear"),
Padding = new Thickness(10, 4),
};
copyButton.Click += async (_, _) => await CopyAsync();
clearButton.Click += (_, _) => clear();
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
_list = new ListBox
{
Classes = { "console" },
ItemsSource = _visibleLines,
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
{
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
return text;
}),
};
Content = new Grid
{
Margin = new Thickness(12),
RowDefinitions = new RowDefinitions("Auto,*"),
Children =
{
new Grid
{
Margin = new Thickness(0, 0, 0, 8),
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
Children =
{
new TextBlock
{
Classes = { "sectionTitle" },
Text = loc.Get("Console.Title"),
VerticalAlignment = VerticalAlignment.Center,
},
_searchBox.WithGridColumn(1),
_autoScrollCheck.WithGridColumn(2),
copyButton.WithGridColumn(3),
clearButton.WithGridColumn(4),
},
},
_list.WithGridRow(1),
},
};
lines.CollectionChanged += OnLinesChanged;
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
RefreshVisibleLines();
}
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshVisibleLines();
if (_autoScrollCheck.IsChecked == true)
{
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
}
}
private void RefreshVisibleLines()
{
var query = _searchBox.Text ?? string.Empty;
_visibleLines.Clear();
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
? _sourceLines
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
}
private async Task CopyAsync()
{
if (_visibleLines.Count == 0 || Clipboard is null)
{
return;
}
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
}
}
file static class GridExtensions
{
public static T WithGridColumn<T>(this T control, int column) where T : Control
{
Grid.SetColumn(control, column);
return control;
}
public static T WithGridRow<T>(this T control, int row) where T : Control
{
Grid.SetRow(control, row);
return control;
}
}
+252
View File
@@ -0,0 +1,252 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.IO.Pipes;
using System.Text;
using System.Text.Json;
namespace SharpEmu.GUI;
/// <summary>
/// Minimal Discord Rich Presence client. Speaks Discord's local IPC
/// protocol (framed JSON over the discord-ipc-N named pipe) directly, so no
/// external dependency is needed. All failures are silent: no Discord, no
/// presence, no impact on the launcher.
/// </summary>
public sealed class DiscordRichPresence : IDisposable
{
private const int OpHandshake = 0;
private const int OpFrame = 1;
private const int MaxFrameBytes = 64 * 1024;
private static readonly JsonSerializerOptions JsonOptions = new()
{
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
};
private readonly object _gate = new();
private readonly string _clientId;
private readonly AutoResetEvent _signal = new(false);
private readonly Thread _worker;
private NamedPipeClientStream? _pipe;
private PresenceState? _desired;
private PresenceState? _sent;
private volatile bool _disposed;
private sealed record PresenceState(string Details, string? State, long? StartUnixSeconds);
public DiscordRichPresence(string clientId)
{
_clientId = clientId;
_worker = new Thread(WorkerLoop)
{
IsBackground = true,
Name = "DiscordRichPresence",
};
_worker.Start();
}
/// <summary>
/// Requests the given presence; the worker applies it asynchronously and
/// retries (with reconnection) until it is delivered or replaced.
/// </summary>
public void SetPresence(string details, string? state = null, long? startUnixSeconds = null)
{
lock (_gate)
{
_desired = new PresenceState(details, state, startUnixSeconds);
}
Trace($"queued details=\"{details}\" state=\"{state}\"");
_signal.Set();
}
public void Dispose()
{
_disposed = true;
_signal.Set();
ClosePipe();
}
private void WorkerLoop()
{
while (!_disposed)
{
// Woken immediately on presence changes; the periodic tick
// retries after connection failures (e.g. Discord starts later).
_signal.WaitOne(TimeSpan.FromSeconds(15));
if (_disposed || _clientId.Length == 0)
{
continue;
}
PresenceState? desired;
lock (_gate)
{
desired = _desired;
}
if (desired is null || desired == _sent)
{
continue;
}
try
{
EnsureConnected();
SendActivity(desired);
_sent = desired;
Trace($"sent details=\"{desired.Details}\" state=\"{desired.State}\"");
}
catch (Exception exception)
{
// Connection died (or never existed); reconnect on a later tick.
Trace($"send failed: {exception.Message}");
ClosePipe();
_sent = null;
}
}
}
private void EnsureConnected()
{
if (_pipe is { IsConnected: true })
{
return;
}
ClosePipe();
Exception? lastError = null;
for (var index = 0; index < 10; index++)
{
var pipe = new NamedPipeClientStream(
".",
$"discord-ipc-{index}",
PipeDirection.InOut,
PipeOptions.Asynchronous);
try
{
pipe.Connect(500);
_pipe = pipe;
WriteFrame(OpHandshake, JsonSerializer.Serialize(new
{
v = 1,
client_id = _clientId,
}));
// Discord replies with a READY (or error) frame; consume it
// so the pipe buffer stays drained.
_ = ReadFrame();
return;
}
catch (Exception exception)
{
lastError = exception;
pipe.Dispose();
_pipe = null;
}
}
throw lastError ?? new IOException("Discord IPC pipe not found.");
}
private void SendActivity(PresenceState presence)
{
var payload = new Dictionary<string, object?>
{
["cmd"] = "SET_ACTIVITY",
["nonce"] = Guid.NewGuid().ToString(),
["args"] = new Dictionary<string, object?>
{
["pid"] = Environment.ProcessId,
["activity"] = new Dictionary<string, object?>
{
["details"] = presence.Details,
["state"] = presence.State,
["timestamps"] = presence.StartUnixSeconds is { } start
? new Dictionary<string, object?> { ["start"] = start }
: null,
["assets"] = new Dictionary<string, object?>
{
["large_image"] = "logo",
["large_text"] = "SharpEmu",
},
},
},
};
WriteFrame(OpFrame, JsonSerializer.Serialize(payload, JsonOptions));
// Consume Discord's acknowledgement to keep the pipe drained.
_ = ReadFrame();
}
private void WriteFrame(int opcode, string json)
{
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
var payload = Encoding.UTF8.GetBytes(json);
var frame = new byte[8 + payload.Length];
BitConverter.TryWriteBytes(frame.AsSpan(0, 4), opcode);
BitConverter.TryWriteBytes(frame.AsSpan(4, 4), payload.Length);
payload.CopyTo(frame, 8);
pipe.Write(frame, 0, frame.Length);
pipe.Flush();
}
private string ReadFrame()
{
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
var header = ReadExactly(pipe, 8);
var length = BitConverter.ToInt32(header, 4);
if (length is < 0 or > MaxFrameBytes)
{
throw new IOException($"Discord IPC frame length {length} is out of range.");
}
return Encoding.UTF8.GetString(ReadExactly(pipe, length));
}
private static byte[] ReadExactly(NamedPipeClientStream pipe, int count)
{
var buffer = new byte[count];
var read = 0;
while (read < count)
{
var chunk = pipe.Read(buffer, read, count - read);
if (chunk <= 0)
{
throw new IOException("Discord IPC pipe closed.");
}
read += chunk;
}
return buffer;
}
private static void Trace(string message)
{
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCORD"),
"1",
StringComparison.Ordinal))
{
Console.Error.WriteLine($"[DISCORD] {message}");
}
}
private void ClosePipe()
{
var pipe = _pipe;
_pipe = null;
try
{
pipe?.Dispose();
}
catch (Exception)
{
// Best effort; the pipe may already be broken.
}
}
}
+14 -22
View File
@@ -85,6 +85,14 @@ internal sealed class EmulatorProcess : IDisposable
return;
}
// Prefer terminating the job: it kills the whole tree, including
// any children the emulator spawned, even when the main process
// is wedged in a GPU driver call.
if (_jobHandle != 0)
{
_ = TerminateJobObject(_jobHandle, 1);
}
if (_processHandle != 0)
{
_ = TerminateProcess(_processHandle, 1);
@@ -160,8 +168,7 @@ internal sealed class EmulatorProcess : IDisposable
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
var policy2 =
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
@@ -192,29 +199,10 @@ internal sealed class EmulatorProcess : IDisposable
ref startupInfoEx,
out var processInfo);
if (!created)
{
// Some mitigation policy bits (e.g. XFG) are not supported on
// older Windows builds. Mirror the CLI's behavior and fall back
// to launching without the mitigation attribute list.
startupInfoEx.lpAttributeList = 0;
created = CreateProcessW(
exePath,
new StringBuilder(BuildCommandLine(exePath, arguments)),
0,
0,
true,
CREATE_NO_WINDOW,
0,
currentDirectory,
ref startupInfoEx,
out processInfo);
}
if (!created)
{
var error = Marshal.GetLastWin32Error();
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message}).");
throw new Win32Exception(error, $"Failed to start '{exePath}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
}
CloseHandle(processInfo.hThread);
@@ -639,6 +627,10 @@ internal sealed class EmulatorProcess : IDisposable
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool TerminateProcess(nint process, uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool TerminateJobObject(nint job, uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(nint handle);
+16 -5
View File
@@ -29,10 +29,12 @@ public sealed class GameEntry : INotifyPropertyChanged
private long _sizeBytes;
public GameEntry(
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
string name, string? titleId, string? version, string path, long sizeBytes,
string? coverPath, string? backgroundPath)
{
Name = name;
TitleId = titleId;
Version = version;
Path = path;
_sizeBytes = sizeBytes;
CoverPath = coverPath;
@@ -46,6 +48,9 @@ public sealed class GameEntry : INotifyPropertyChanged
public string? TitleId { get; }
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
public string? Version { get; }
public string Path { get; }
/// <summary>
@@ -65,7 +70,7 @@ public sealed class GameEntry : INotifyPropertyChanged
_sizeBytes = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeText)));
}
}
@@ -106,9 +111,15 @@ public sealed class GameEntry : INotifyPropertyChanged
public bool HasCover => _cover is not null;
public string Detail => TitleId is not null
? $"{TitleId} • {FormatSize(SizeBytes)}"
: FormatSize(SizeBytes);
public bool HasTitleId => TitleId is not null;
/// <summary>Badge text shown in the launch bar, e.g. "v01.000.000".</summary>
public string? VersionText => Version is null ? null : $"v{Version}";
public bool HasVersion => Version is not null;
/// <summary>Formatted install size badge shown in the launch bar.</summary>
public string SizeText => FormatSize(SizeBytes);
private static string ComputeInitials(string name)
{
+33
View File
@@ -23,8 +23,41 @@ public sealed class GuiSettings
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>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
public string Language { get; set; } = "en";
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
public bool DiscordRichPresence { get; set; } = true;
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
public List<string> EnvironmentToggles { get; set; } = new();
/// <summary>
/// Discord application ID used for Rich Presence; the default is the
/// SharpEmu application. Override to rebrand what Discord shows as
/// "Playing …" (register at discord.com/developers/applications).
/// </summary>
public string DiscordClientId { get; set; } = "1525606762248540221";
// The emulator is portable and keeps its data next to the executable;
// the GUI follows the same convention.
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "العربية",
"Page.Library": "المكتبة",
"Page.Options": "الخيارات",
"Page.GameCount.One": "لعبة واحدة",
"Page.GameCount.Other": "{0} لعبة",
"Library.SearchWatermark": "ابحث في المكتبة...",
"Library.AddFolder": "+ إضافة مجلد",
"Library.Rescan": "⟳ إعادة الفحص",
"Library.OpenFile": "فتح ملف...",
"Library.Context.Launch": "تشغيل",
"Library.Context.OpenFolder": "فتح مجلد اللعبة",
"Library.Context.CopyPath": "نسخ المسار",
"Library.Context.CopyTitleId": "نسخ معرف العنوان",
"Library.Context.Remove": "إزالة من المكتبة",
"Library.Empty.Title": "مكتبتك فارغة",
"Library.Empty.Hint": "أضف مجلداً يحتوي على ألعابك للبدء.",
"Library.Empty.SearchTitle": "لا توجد ألعاب تطابق بحثك",
"Library.Empty.SearchHint": "لا شيء في المكتبة يطابق “{0}”.",
"Library.Empty.AddFolder": "+ إضافة مجلد ألعاب",
"Library.Loading": "جارٍ تحميل المكتبة...",
"Options.General": "عام",
"Options.Section.Emulation": "المحاكاة",
"Options.Section.Logging": "التسجيل",
"Options.Section.Launcher": "المُشغِّل",
"Options.CpuEngine.Label": "محرك المعالج",
"Options.CpuEngine.Desc": "محرك التنفيذ المستخدم لتشغيل كود اللعبة.",
"Options.CpuEngine.Native": "أصلي",
"Options.Strict.Label": "ربط صارم للمكتبات الديناميكية (dynlib)",
"Options.Strict.Desc": "إفشال التشغيل عندما يتعذر التعرف على رمز مستورد.",
"Options.LogLevel.Label": "مستوى التسجيل",
"Options.LogLevel.Desc": "مدى تفصيل مخرجات نافذة سجلات المحاكي.",
"Options.LogLevel.Trace": "تتبع",
"Options.LogLevel.Debug": "تصحيح الأخطاء",
"Options.LogLevel.Info": "معلومات",
"Options.LogLevel.Warning": "تحذير",
"Options.LogLevel.Error": "خطأ",
"Options.LogLevel.Critical": "حرج",
"Options.TraceImports.Label": "حد تتبع الاستيراد",
"Options.TraceImports.Desc": "تتبع أول N استيراد لكل وحدة (0 = إيقاف).",
"Options.LogToFile.Label": "التسجيل في ملف",
"Options.LogToFile.Desc": "نسخ مخرجات المحاكي إلى ملف سجل.",
"Options.LogFilePath.Label": "مسار ملف السجل",
"Options.LogFilePath.Default": "لا يوجد مسار مخصص — تُحفظ السجلات في user/logs بجوار المحاكي.",
"Options.LogFilePath.Select": "تحديد...",
"Options.OverrideLogFile.Label": "الكتابة فوق ملف السجل",
"Options.OverrideLogFile.Desc": "استخدام مسار الملف الدقيق بدلاً من إلحاق معرف العنوان والطابع الزمني.",
"Options.TitleMusic.Label": "موسيقى اللعبة",
"Options.TitleMusic.Desc": "تكرار موسيقى المعاينة للعبة المحددة في المكتبة.",
"Options.Discord.Label": "حالة دسكورد",
"Options.Discord.Desc": "إظهار اللعبة قيد التشغيل في ملفك الشخصي على دسكورد.",
"Options.Language.Label": "لغة المحاكي",
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
"Common.On": "تشغيل",
"Common.Off": "إيقاف",
"Console.Title": "نافذة السجلات",
"Console.SearchWatermark": "بحث...",
"Console.AutoScroll": "تمرير تلقائي",
"Console.Split": "تقسيم",
"Console.Copy": "نسخ",
"Console.Clear": "مسح",
"Console.WindowTitle": "نافذة سجلات SharpEmu",
"Launch.NoGameSelected": "لم تُحدد أي لعبة",
"Launch.NoGameHint": "اختر لعبة من المكتبة، أو افتح ملف eboot.bin مباشرة.",
"Launch.Idle": "خامل",
"Launch.Console": "≡ نافذة السجلات",
"Launch.Launch": "▶ تشغيل",
"Launch.Stop": "■ إيقاف",
"Launch.Running": "قيد التشغيل — {0}",
"Launch.Stopping": "جارٍ الإيقاف...",
"Launch.Exited": "انتهى برمز {0} ({1})",
"Launch.ExeNotFound": "لم يُعثر على الملف التنفيذي SharpEmu. ابنِ مشروع SharpEmu.CLI أولاً (dotnet build).",
"Launch.LogFile": "ملف السجل: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "فشل بدء تشغيل المحاكي: {0}",
"Launch.ProcessExited": "انتهت العملية برمز {0} ({1}).",
"Exit.Ok": "موافق",
"Exit.InvalidArguments": "معطيات غير صالحة",
"Exit.EbootNotFound": "لم يُعثر على eboot",
"Exit.RuntimeException": "استثناء وقت التشغيل",
"Exit.EmulationError": "خطأ في المحاكاة",
"Exit.Unknown": "غير معروف",
"Status.EmulatorLocating": "المحاكي: جارٍ تحديد الموقع...",
"Status.EmulatorPath": "المحاكي: {0}",
"Status.EmulatorNotFound": "المحاكي: لم يُعثر على الملف التنفيذي SharpEmu — ابنِ SharpEmu.CLI أولاً.",
"Status.ScanningLibrary": "جارٍ فحص المكتبة...",
"Status.AddFolderPrompt": "أضف مجلد ألعاب لملء المكتبة.",
"Status.LibraryScanned": "فُحصت المكتبة: {0} لعبة في {1} مجلد.",
"Status.CouldNotOpenFolder": "تعذر فتح المجلد: {0}",
"Status.CopiedToClipboard": "نُسخ {0} إلى الحافظة.",
"Status.RemovedFromLibrary": "أُزيل “{0}” من المكتبة. أعد إضافة مجلده لاستعادته.",
"Status.Running": "جارٍ تشغيل {0}",
"Status.Stopping": "جارٍ الإيقاف...",
"Status.Idle": "خامل",
"Clipboard.Path": "المسار",
"Clipboard.TitleId": "معرف العنوان",
"Discord.Playing": "يلعب {0}",
"Discord.Browsing": "يتصفح المكتبة",
"Dialog.ChooseGameFolder": "اختر مجلداً يحتوي على ألعاب",
"Dialog.OpenExecutable": "افتح ملفاً تنفيذياً لتشغيله",
"Dialog.PsExecutables": "ملفات PS التنفيذية",
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
"Dialog.PlainTextFiles": "ملفات نصية عادية",
"Dialog.LogFiles": "ملفات السجل"
}
+138
View File
@@ -0,0 +1,138 @@
{
"_languageName": "Português (Brasil)",
"Page.Library": "Biblioteca",
"Page.Options": "Opções",
"Page.GameCount.One": "1 Jogo",
"Page.GameCount.Other": "{0} jogos",
"Library.SearchWatermark": "Pesquisar na biblioteca…",
"Library.AddFolder": " Adicionar pasta",
"Library.Rescan": "⟳ Atualizar biblioteca",
"Library.OpenFile": "Abrir arquivo…",
"Library.Context.Launch": "Jogar",
"Library.Context.OpenFolder": "Abrir pasta do jogo",
"Library.Context.CopyPath": "Copiar o caminho",
"Library.Context.CopyTitleId": "Copiar ID do título",
"Library.Context.Remove": "Remover da biblioteca",
"Library.Empty.Title": "Sua biblioteca está vazia",
"Library.Empty.Hint": "Adicione uma pasta contendo seus jogos para começar.",
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua busca",
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
"Library.Empty.AddFolder": " Adicionar pasta do jogo",
"Library.Loading": "Carregando biblioteca…",
"Options.General": "Opções Gerais",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"Options.Env.Desc": "Switches passados para o emulador como variáveis de ambiente na inicialização.",
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica esperando indefinidamente.\nDeixe desativado normalmente. Alguns títulos travam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada por tempo demais.\nExperimente isso quando um jogo fecha sozinho durante o carregamento.",
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração de GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "LOGS",
"Options.Section.Launcher": "INICIALIZADOR",
"Options.CpuEngine.Label": "Motor da CPU",
"Options.CpuEngine.Desc": "Motor de execução usado para executar o código do jogo.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Resolução estrita de bibliotecas dinâmicas",
"Options.Strict.Desc": "Interrompe a inicialização caso um símbolo importado não possa ser vinculado.",
"Options.LogLevel.Label": "Nível de log",
"Options.LogLevel.Desc": "Nível de detalhamento das mensagens exibidas no console do emulador.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Limite de rastreamento de importações",
"Options.TraceImports.Desc": "Rastreia as primeiras N importações de cada módulo (0 = desativado).",
"Options.LogToFile.Label": "Salvar log em arquivo",
"Options.LogToFile.Desc": "Copia a saída do emulador para um arquivo de log.",
"Options.LogFilePath.Label": "Caminho do arquivo de log",
"Options.LogFilePath.Default": "Nenhum caminho definido — logs vão para user/logs na pasta do emulador.",
"Options.LogFilePath.Select": "Selecionar…",
"Options.OverrideLogFile.Label": "Sobrescrever arquivo de log",
"Options.OverrideLogFile.Desc": "Use o caminho exato do arquivo em vez de adicionar o ID do título e o log de data e hora.",
"Options.TitleMusic.Label": "Música de prévia",
"Options.TitleMusic.Desc": "Reproduz em loop a música de prévia do jogo selecionado na biblioteca.",
"Options.Discord.Label": "Status do Discord",
"Options.Discord.Desc": "Exibir o jogo em execução no seu perfil do Discord.",
"Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
"Common.On": "Ativado",
"Common.Off": "Desativado",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Pesquisar...",
"Console.AutoScroll": "Rolagem automática",
"Console.Split": "Recortar",
"Console.Copy": "Copiar",
"Console.Clear": "Limpar",
"Console.WindowTitle": "Console do SharpEmu",
"Launch.NoGameSelected": "Nenhum jogo selecionado",
"Launch.NoGameHint": "Selecione um jogo na biblioteca ou abra um arquivo eboot.bin diretamente.",
"Launch.Idle": "Ocioso",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Iniciar",
"Launch.Stop": "■ Parar",
"Launch.Running": "Em execução — {0}",
"Launch.Stopping": "Encerrando…",
"Launch.Exited": "Encerrado com código {0} ({1})",
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Arquivo de log: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
"Launch.ProcessExited": "O processo foi encerrado com código {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argumentos inválidos",
"Exit.EbootNotFound": "eboot.bin não encontrado",
"Exit.RuntimeException": "exceção em tempo de execução",
"Exit.EmulationError": "erro de emulação",
"Exit.Unknown": "desconhecido",
"Status.EmulatorLocating": "Emulador: localizando…",
"Status.EmulatorPath": "Emulador: {0}",
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile o SharpEmu.CLI primeiro.",
"Status.ScanningLibrary": "Verificando biblioteca…",
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
"Status.LibraryScanned": "Biblioteca verificada: {0} jogo(s) em {1} pasta(s).",
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente sua pasta para restaurá-lo.",
"Status.Running": "Executando {0}",
"Status.Stopping": "Encerrando…",
"Status.Idle": "Ocioso",
"Clipboard.Path": "Caminho",
"Clipboard.TitleId": "ID do título",
"Discord.Playing": "Jogando {0}",
"Discord.Browsing": "Navegando pela biblioteca",
"Dialog.ChooseGameFolder": "Escolha uma pasta contendo jogos",
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
"Dialog.PsExecutables": "Executáveis de PS",
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
"Dialog.PlainTextFiles": "Arquivos de texto simples",
"Dialog.LogFiles": "Arquivos de log"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "Deutsch",
"Page.Library": "Bibliothek",
"Page.Options": "Optionen",
"Page.GameCount.One": "1 Spiel",
"Page.GameCount.Other": "{0} Spiele",
"Library.SearchWatermark": "Bibliothek durchsuchen…",
"Library.AddFolder": " Spielordner hinzufügen",
"Library.Rescan": "⟳ Neu scannen",
"Library.OpenFile": "Datei öffnen…",
"Library.Context.Launch": "Starten",
"Library.Context.OpenFolder": "Spielordner öffnen",
"Library.Context.CopyPath": "Pfad kopieren",
"Library.Context.CopyTitleId": "Title ID kopieren",
"Library.Context.Remove": "Aus Bibliothek entfernen",
"Library.Empty.Title": "Deine Bibliothek ist leer",
"Library.Empty.Hint": "Füge einen Ordner mit deinen Spielen hinzu, um zu beginnen.",
"Library.Empty.SearchTitle": "Keine Spiele gefunden",
"Library.Empty.SearchHint": "Nichts in der Bibliothek entspricht “{0}”.",
"Library.Empty.AddFolder": " Spielordner hinzufügen",
"Library.Loading": "Bibliothek wird geladen…",
"Options.General": "Allgemein",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "PROTOKOLLIERUNG",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU-Engine",
"Options.CpuEngine.Desc": "Ausführungs-Engine, die zum Ausführen des Spiel-Codes verwendet wird.",
"Options.CpuEngine.Native": "Nativ",
"Options.Strict.Label": "Strikte dynlib-Auflösung",
"Options.Strict.Desc": "Starten abbrechen, wenn ein importiertes Symbol nicht aufgelöst werden kann.",
"Options.LogLevel.Label": "Protokollstufe",
"Options.LogLevel.Desc": "Ausführlichkeit der Emulator-Konsolenausgabe.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warnung",
"Options.LogLevel.Error": "Fehler",
"Options.LogLevel.Critical": "Kritisch",
"Options.TraceImports.Label": "Import-Trace-Limit",
"Options.TraceImports.Desc": "Die ersten N Imports pro Modul verfolgen (0 = aus).",
"Options.LogToFile.Label": "In Datei protokollieren",
"Options.LogToFile.Desc": "Emulator-Ausgabe zusätzlich in eine Log-Datei schreiben.",
"Options.LogFilePath.Label": "Protokolldatei-Pfad",
"Options.LogFilePath.Default": "Kein benutzerdefinierter Pfad Logs werden im Ordner user/logs neben dem Emulator gespeichert.",
"Options.LogFilePath.Select": "Auswählen…",
"Options.OverrideLogFile.Label": "Protokolldatei überschreiben",
"Options.OverrideLogFile.Desc": "Genauen Dateipfad verwenden, statt Title-ID und Zeitstempel anzuhängen.",
"Options.TitleMusic.Label": "Titel-Musik",
"Options.TitleMusic.Desc": "Die Vorschau-Musik des ausgewählten Spiels in der Bibliothek loopend abspielen.",
"Options.Discord.Label": "Discord-Präsenz",
"Options.Discord.Desc": "Zeigt das aktuell gespielte Spiel in deinem Discord-Profil an.",
"Options.Language.Label": "Emulator-Sprache",
"Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.",
"Common.On": "An",
"Common.Off": "Aus",
"Console.Title": "KONSOLE",
"Console.SearchWatermark": "Suchen...",
"Console.AutoScroll": "Auto-Scroll",
"Console.Split": "Teilen",
"Console.Copy": "Kopieren",
"Console.Clear": "Leeren",
"Console.WindowTitle": "SharpEmu Konsole",
"Launch.NoGameSelected": "Kein Spiel ausgewählt",
"Launch.NoGameHint": "Wähle ein Spiel aus der Bibliothek aus oder öffne eine eboot.bin direkt.",
"Launch.Idle": "Bereit",
"Launch.Console": "≡ Konsole",
"Launch.Launch": "▶ Starten",
"Launch.Stop": "■ Stoppen",
"Launch.Running": "Läuft -- {0}",
"Launch.Stopping": "Wird beendet…",
"Launch.Exited": "Beendet mit Code {0} ({1})",
"Launch.ExeNotFound": "SharpEmu-Executable wurde nicht gefunden. Baue zuerst das SharpEmu.CLI-Projekt (dotnet build).",
"Launch.LogFile": "Log-Datei: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emulator konnte nicht gestartet werden: {0}",
"Launch.ProcessExited": "Prozess wurde mit Code {0} ({1}) beendet.",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "Ungültige Argumente",
"Exit.EbootNotFound": "eboot nicht gefunden",
"Exit.RuntimeException": "Laufzeitfehler",
"Exit.EmulationError": "Emulationsfehler",
"Exit.Unknown": "unbekannt",
"Status.EmulatorLocating": "Emulator: wird gesucht…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu-Executable nicht gefunden -- baue zuerst SharpEmu.CLI.",
"Status.ScanningLibrary": "Bibliothek wird gescannt…",
"Status.AddFolderPrompt": "Füge einen Spielordner hinzu, um die Bibliothek zu füllen.",
"Status.LibraryScanned": "Bibliothek gescannt: {0} Spiel(e) in {1} Ordner(n).",
"Status.CouldNotOpenFolder": "Ordner konnte nicht geöffnet werden: {0}",
"Status.CopiedToClipboard": "{0} in die Zwischenablage kopiert.",
"Status.RemovedFromLibrary": "“{0}” wurde aus der Bibliothek entfernt. Füge den Ordner erneut hinzu, um es wiederherzustellen.",
"Status.Running": "Läuft {0}",
"Status.Stopping": "Wird gestoppt…",
"Status.Idle": "Bereit",
"Clipboard.Path": "Pfad",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Spielt {0}",
"Discord.Browsing": "Durchsucht die Bibliothek",
"Dialog.ChooseGameFolder": "Spielordner auswählen",
"Dialog.OpenExecutable": "Ausführbare Datei zum Starten öffnen",
"Dialog.PsExecutables": "PS-Ausführbare Dateien",
"Dialog.SaveLogFile": "Protokolldatei speichern unter",
"Dialog.PlainTextFiles": "Textdateien",
"Dialog.LogFiles": "Protokolldateien"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "Dansk",
"Page.Library": "Bibliotek",
"Page.Options": "Indstillinger",
"Page.GameCount.One": "1 spil",
"Page.GameCount.Other": "{0} spil",
"Library.SearchWatermark": "Søg i biblioteket…",
"Library.AddFolder": " Tilføj mappe",
"Library.Rescan": "⟳ Genindlæs",
"Library.OpenFile": "Åbn fil…",
"Library.Context.Launch": "Start",
"Library.Context.OpenFolder": "Åbn spilmappe",
"Library.Context.CopyPath": "Kopiér sti",
"Library.Context.CopyTitleId": "Kopiér titel-ID",
"Library.Context.Remove": "Fjern fra bibliotek",
"Library.Empty.Title": "Dit bibliotek er tomt",
"Library.Empty.Hint": "Tilføj en mappe med dine spil for at komme i gang.",
"Library.Empty.SearchTitle": "Ingen spil matcher din søgning",
"Library.Empty.SearchHint": "Intet i biblioteket matcher “{0}”.",
"Library.Empty.AddFolder": " Tilføj spilmappe",
"Library.Loading": "Indlæser bibliotek…",
"Options.General": "Generelt",
"Options.Section.Emulation": "EMULERING",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU-engine",
"Options.CpuEngine.Desc": "Den eksekveringsengine der bruges til at køre spilkode.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Streng dynlib-opløsning",
"Options.Strict.Desc": "Afbryd opstarten, når et importeret symbol ikke kan findes.",
"Options.LogLevel.Label": "Logniveau",
"Options.LogLevel.Desc": "Detaljeringsgrad for emulatorens konsoloutput.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Advarsel",
"Options.LogLevel.Error": "Fejl",
"Options.LogLevel.Critical": "Kritisk",
"Options.TraceImports.Label": "Grænse for import-trace",
"Options.TraceImports.Desc": "Spor de første N imports pr. modul (0 = fra).",
"Options.LogToFile.Label": "Log til fil",
"Options.LogToFile.Desc": "Spejl emulatorens output til en logfil.",
"Options.LogFilePath.Label": "Sti til logfil",
"Options.LogFilePath.Default": "Ingen brugerdefineret sti — logs gemmes i user/logs ved siden af emulatoren.",
"Options.LogFilePath.Select": "Vælg…",
"Options.OverrideLogFile.Label": "Tilsidesæt logfil",
"Options.OverrideLogFile.Desc": "Brug den præcise filsti i stedet for at tilføje titel-ID og tidsstempel.",
"Options.TitleMusic.Label": "Titelmusik",
"Options.TitleMusic.Desc": "Gentag det valgte spils forhåndsvisningsmusik i biblioteket.",
"Options.Discord.Label": "Discord-tilstedeværelse",
"Options.Discord.Desc": "Vis det kørende spil på din Discord-profil.",
"Options.Language.Label": "Emulatorsprog",
"Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.",
"Common.On": "Til",
"Common.Off": "Fra",
"Console.Title": "KONSOL",
"Console.SearchWatermark": "Søg...",
"Console.AutoScroll": "Auto-scroll",
"Console.Split": "Opdel",
"Console.Copy": "Kopiér",
"Console.Clear": "Ryd",
"Console.WindowTitle": "SharpEmu-konsol",
"Launch.NoGameSelected": "Intet spil valgt",
"Launch.NoGameHint": "Vælg et spil fra biblioteket, eller åbn en eboot.bin direkte.",
"Launch.Idle": "Inaktiv",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Start",
"Launch.Stop": "■ Stop",
"Launch.Running": "Kører — {0}",
"Launch.Stopping": "Stopper…",
"Launch.Exited": "Afsluttet med kode {0} ({1})",
"Launch.ExeNotFound": "SharpEmu-programmet blev ikke fundet. Byg SharpEmu.CLI-projektet først (dotnet build).",
"Launch.LogFile": "Logfil: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Kunne ikke starte emulatoren: {0}",
"Launch.ProcessExited": "Processen afsluttede med kode {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "ugyldige argumenter",
"Exit.EbootNotFound": "eboot ikke fundet",
"Exit.RuntimeException": "runtime-fejl",
"Exit.EmulationError": "emuleringsfejl",
"Exit.Unknown": "ukendt",
"Status.EmulatorLocating": "Emulator: lokaliserer…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu-programmet blev ikke fundet — byg SharpEmu.CLI først.",
"Status.ScanningLibrary": "Skanner bibliotek…",
"Status.AddFolderPrompt": "Tilføj en spilmappe for at udfylde biblioteket.",
"Status.LibraryScanned": "Bibliotek skannet: {0} spil i {1} mappe(r).",
"Status.CouldNotOpenFolder": "Kunne ikke åbne mappe: {0}",
"Status.CopiedToClipboard": "{0} kopieret til udklipsholderen.",
"Status.RemovedFromLibrary": "“{0}” fjernet fra biblioteket. Tilføj mappen igen for at gendanne det.",
"Status.Running": "Kører {0}",
"Status.Stopping": "Stopper…",
"Status.Idle": "Inaktiv",
"Clipboard.Path": "Sti",
"Clipboard.TitleId": "Titel-ID",
"Discord.Playing": "Spiller {0}",
"Discord.Browsing": "Gennemser biblioteket",
"Dialog.ChooseGameFolder": "Vælg en mappe der indeholder spil",
"Dialog.OpenExecutable": "Åbn et program der skal startes",
"Dialog.PsExecutables": "PS-programmer",
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
"Dialog.PlainTextFiles": "Almindelige tekstfiler",
"Dialog.LogFiles": "Logfiler"
}
+146
View File
@@ -0,0 +1,146 @@
{
"_languageName": "English",
"Page.Library": "Library",
"Page.Options": "Options",
"Page.GameCount.One": "1 game",
"Page.GameCount.Other": "{0} games",
"Library.SearchWatermark": "Search library…",
"Library.AddFolder": " Add folder",
"Library.Rescan": "⟳ Rescan",
"Library.OpenFile": "Open file…",
"Library.Context.Launch": "Launch",
"Library.Context.OpenFolder": "Open game folder",
"Library.Context.CopyPath": "Copy path",
"Library.Context.CopyTitleId": "Copy title ID",
"Library.Context.Remove": "Remove from library",
"Library.Empty.Title": "Your library is empty",
"Library.Empty.Hint": "Add a folder containing your games to get started.",
"Library.Empty.SearchTitle": "No games match your search",
"Library.Empty.SearchHint": "Nothing in the library matches “{0}”.",
"Library.Empty.AddFolder": " Add game folder",
"Library.Loading": "Loading library…",
"Options.General": "General",
"Options.Env.Tab": "Environment",
"Options.Section.Environment": "ENVIRONMENT VARIABLES",
"Options.Env.Desc": "Switches passed to the emulator as environment variables at launch.",
"Options.Env.Bthid.Desc": "Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.\nLeave off normally. Some titles freeze when init fails.",
"Options.Env.LoopGuard.Desc": "Do not force quit titles that repeat the same call for too long.\nTry this when a game exits on its own while loading.",
"Options.Env.VkValidation.Desc": "Enable Vulkan validation layers for GPU debugging.\nSlow. Requires the Vulkan SDK to be installed.",
"Options.Env.DumpSpirv.Desc": "Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.\nUse when reporting shader or rendering bugs.",
"Options.Env.LogDirectMemory.Desc": "Log direct memory allocations and failures to the console.\nUse when a game aborts or exits during boot.",
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Strict dynlib resolution",
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
"Options.LogLevel.Label": "Log level",
"Options.LogLevel.Desc": "Verbosity of the emulator console output.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Trace the first N imports per module (0 = off).",
"Options.LogToFile.Label": "Log to file",
"Options.LogToFile.Desc": "Mirror emulator output to a log file.",
"Options.LogFilePath.Label": "Log file path",
"Options.LogFilePath.Default": "No custom path — logs go to user/logs next to the emulator.",
"Options.LogFilePath.Select": "Select…",
"Options.OverrideLogFile.Label": "Override log file",
"Options.OverrideLogFile.Desc": "Use the exact file path instead of appending title ID and timestamp.",
"Options.TitleMusic.Label": "Title music",
"Options.TitleMusic.Desc": "Loop the selected game's preview music in the library.",
"Options.Discord.Label": "Discord presence",
"Options.Discord.Desc": "Show the running game on your Discord profile.",
"Options.Language.Label": "Emulator language",
"Options.Language.Desc": "Language used throughout the launcher. Applies immediately.",
"Common.On": "On",
"Common.Off": "Off",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Search...",
"Console.AutoScroll": "Auto-scroll",
"Console.Split": "Split",
"Console.Copy": "Copy",
"Console.Clear": "Clear",
"Console.WindowTitle": "SharpEmu Console",
"Launch.NoGameSelected": "No game selected",
"Launch.NoGameHint": "Pick a game from the library, or open an eboot.bin directly.",
"Launch.Idle": "Idle",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Launch",
"Launch.Stop": "■ Stop",
"Launch.Running": "Running — {0}",
"Launch.Stopping": "Stopping…",
"Launch.Exited": "Exited with code {0} ({1})",
"Launch.ExeNotFound": "SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).",
"Launch.LogFile": "Log file: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Failed to start the emulator: {0}",
"Launch.ProcessExited": "Process exited with code {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "invalid arguments",
"Exit.EbootNotFound": "eboot not found",
"Exit.RuntimeException": "runtime exception",
"Exit.EmulationError": "emulation error",
"Exit.Unknown": "unknown",
"Status.EmulatorLocating": "Emulator: locating…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.",
"Status.ScanningLibrary": "Scanning library…",
"Status.AddFolderPrompt": "Add a game folder to populate the library.",
"Status.LibraryScanned": "Library scanned: {0} game(s) in {1} folder(s).",
"Status.CouldNotOpenFolder": "Could not open folder: {0}",
"Status.CopiedToClipboard": "{0} copied to clipboard.",
"Status.RemovedFromLibrary": "Removed “{0}” from the library. Re-add its folder to restore it.",
"Status.Running": "Running {0}",
"Status.Stopping": "Stopping…",
"Status.Idle": "Idle",
"Clipboard.Path": "Path",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "Playing {0}",
"Discord.Browsing": "Browsing the library",
"Dialog.ChooseGameFolder": "Choose a folder containing games",
"Dialog.OpenExecutable": "Open an executable to launch",
"Dialog.PsExecutables": "PS executables",
"Dialog.SaveLogFile": "Select where to save the Log file",
"Dialog.PlainTextFiles": "Plain Text Files",
"Dialog.LogFiles": "Log Files",
"Options.About" : "About",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Source code, issues and project development.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Join the community, get support and follow development.",
"About.GithubButton": "Contribute in GitHub!",
"About.DiscordButton": "Join our Discord!"
}
+137
View File
@@ -0,0 +1,137 @@
{
"_languageName": "Español",
"Page.Library": "Biblioteca",
"Page.Options": "Opciones",
"Page.GameCount.One": "1 juego",
"Page.GameCount.Other": "{0} juegos",
"Library.SearchWatermark": "Buscar en la biblioteca…",
"Library.AddFolder": " Añadir carpeta",
"Library.Rescan": "⟳ Volver a escanear",
"Library.OpenFile": "Abrir archivo…",
"Library.Context.Launch": "Iniciar",
"Library.Context.OpenFolder": "Abrir carpeta de juegos",
"Library.Context.CopyPath": "Copiar ruta",
"Library.Context.CopyTitleId": "Copiar ID del título",
"Library.Context.Remove": "Eliminar de la biblioteca",
"Library.Empty.Title": "Tu biblioteca está vacía",
"Library.Empty.Hint": "Añade una carpeta que contenga tus juegos para empezar.",
"Library.Empty.SearchTitle": "Ningún juego coincide con la búsqueda",
"Library.Empty.SearchHint": "No se ha encontrado nada en la biblioteca que coincida con “{0}”.",
"Library.Empty.AddFolder": " Añadir carpeta de juegos",
"Library.Loading": "Cargando biblioteca…",
"Options.General": "General",
"Options.Section.Emulation": "EMULACIÓN",
"Options.Section.Logging": "LOGS",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "Motor de CPU",
"Options.CpuEngine.Desc": "Motor utilizado para ejecutar el código del juego.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Resolución estricta de dynlib (Bibliotecas dinámicas)",
"Options.Strict.Desc": "Detener la ejecución cuando un símbolo importado no se pueda resolver.",
"Options.LogLevel.Label": "Nivel de Log",
"Options.LogLevel.Desc": "Verbosidad de la salida en consola del emulador.",
"Options.LogLevel.Trace": "Trazas",
"Options.LogLevel.Debug": "Depuración",
"Options.LogLevel.Info": "Información",
"Options.LogLevel.Warning": "Advertencia",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Crítico",
"Options.TraceImports.Label": "Límite de trazado de importaciones",
"Options.TraceImports.Desc": "Trazar las primeras N importaciones por módulo (0 = off).",
"Options.LogToFile.Label": "Registrar log en archivo",
"Options.LogToFile.Desc": "Duplicar la salida del emulador en un archivo de logs.",
"Options.LogFilePath.Label": "Ruta del archivo de Log",
"Options.LogFilePath.Default": "Sin ruta personalizada — los logs van a user/logs al lado del emulador.",
"Options.LogFilePath.Select": "Seleccionar…",
"Options.OverrideLogFile.Label": "Sobreescribir archivo de logs",
"Options.OverrideLogFile.Desc": "Utilizar la misma ruta para el archivo de logs en vez de añadir la ID del título y marca de tiempo.",
"Options.TitleMusic.Label": "Música del título",
"Options.TitleMusic.Desc": "Repetir en bucle la preview de la música del juego seleccionado en la biblioteca.",
"Options.Discord.Label": "Actividad de Discord",
"Options.Discord.Desc": "Mostrar juego en ejecución en tu perfil de Discord.",
"Options.Language.Label": "Idioma del emulador",
"Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.",
"Common.On": "Encendido",
"Common.Off": "Apagado",
"Console.Title": "CONSOLA",
"Console.SearchWatermark": "Buscar...",
"Console.AutoScroll": "Desplazamiento automático",
"Console.Split": "Desacoplar",
"Console.Copy": "Copiar",
"Console.Clear": "Limpiar",
"Console.WindowTitle": "Consola SharpEmu",
"Launch.NoGameSelected": "No hay ningún juego seleccionado",
"Launch.NoGameHint": "Selecciona un juego de la biblioteca o abre un eboot.bin directamente.",
"Launch.Idle": "Inactivo",
"Launch.Console": "≡ Consola",
"Launch.Launch": "▶ Iniciar",
"Launch.Stop": "■ Detener",
"Launch.Running": "En ejecución — {0}",
"Launch.Stopping": "Deteniendo…",
"Launch.Exited": "Finalizó con el código {0} ({1})",
"Launch.ExeNotFound": "No se ha encontrado el ejecutable de SharpEmu. Compila previamente el proyecto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Archivo de Log: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Error al iniciar el emulador: {0}",
"Launch.ProcessExited": "El proceso finalizó con el código {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argumentos no válidos",
"Exit.EbootNotFound": "no se encontró eboot",
"Exit.RuntimeException": "excepción en tiempo de ejecución",
"Exit.EmulationError": "error de emulación",
"Exit.Unknown": "desconocido",
"Status.EmulatorLocating": "Emulador: localizando…",
"Status.EmulatorPath": "Emulador: {0}",
"Status.EmulatorNotFound": "Emulador: No se encontró el ejecutable de SharpEmu — compila previamente SharpEmu.CLI.",
"Status.ScanningLibrary": "Escaneando biblioteca…",
"Status.AddFolderPrompt": "Añade una carpeta de juegos para poblar la biblioteca.",
"Status.LibraryScanned": "Biblioteca escaneada: Se encontraron {0} juego(s) en {1} carpeta(s).",
"Status.CouldNotOpenFolder": "No se ha podido abrir la carpeta: {0}",
"Status.CopiedToClipboard": "{0} copiado al portapapeles.",
"Status.RemovedFromLibrary": "Se eliminó “{0}” de la biblioteca. Vuelve a añadir su carpeta para restaurarlo.",
"Status.Running": "Ejecutando {0}",
"Status.Stopping": "Deteniendo…",
"Status.Idle": "Inactivo",
"Clipboard.Path": "Ruta",
"Clipboard.TitleId": "ID del título",
"Discord.Playing": "Jugando a {0}",
"Discord.Browsing": "Navegando en la biblioteca, buscando un juego para divertirse.",
"Dialog.ChooseGameFolder": "Selecciona una carpeta que contenga juegos",
"Dialog.OpenExecutable": "Abrir un ejecutable para iniciar",
"Dialog.PsExecutables": "Ejecutables de PS",
"Dialog.SaveLogFile": "Selecciona dónde guardar el archivo de Logs",
"Dialog.PlainTextFiles": "Archivos en texto plano",
"Dialog.LogFiles": "Archivos de Log",
"Options.About" : "Informacion",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
"About.GithubButton": "Contribuye en GitHub!",
"About.DiscordButton": "Únete a nuestro Discord!"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "Français",
"Page.Library": "Bibliothèque",
"Page.Options": "Options",
"Page.GameCount.One": "1 jeu",
"Page.GameCount.Other": "{0} jeux",
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
"Library.AddFolder": " Ajouter un dossier",
"Library.Rescan": "⟳ Analyser à nouveau",
"Library.OpenFile": "Ouvrir un fichier…",
"Library.Context.Launch": "Lancer",
"Library.Context.OpenFolder": "Ouvrir le dossier du jeu",
"Library.Context.CopyPath": "Copier le chemin",
"Library.Context.CopyTitleId": "Copier lidentifiant du jeu",
"Library.Context.Remove": "Retirer de la bibliothèque",
"Library.Empty.Title": "Votre bibliothèque est vide",
"Library.Empty.Hint": "Ajoutez un dossier contenant vos jeux pour commencer.",
"Library.Empty.SearchTitle": "Aucun jeu ne correspond à votre recherche",
"Library.Empty.SearchHint": "Aucun élément de la bibliothèque ne correspond à « {0} ».",
"Library.Empty.AddFolder": " Ajouter un dossier de jeux",
"Library.Loading": "Chargement de la bibliothèque…",
"Options.General": "Général",
"Options.Section.Emulation": "ÉMULATION",
"Options.Section.Logging": "JOURNALISATION",
"Options.Section.Launcher": "LANCEUR",
"Options.CpuEngine.Label": "Moteur CPU",
"Options.CpuEngine.Desc": "Moteur dexécution utilisé pour exécuter le code du jeu.",
"Options.CpuEngine.Native": "Natif",
"Options.Strict.Label": "Résolution stricte des bibliothèques dynamiques",
"Options.Strict.Desc": "Interrompre le lancement lorsquun symbole importé ne peut pas être résolu.",
"Options.LogLevel.Label": "Niveau de journalisation",
"Options.LogLevel.Desc": "Niveau de détail des messages affichés dans la console de l’émulateur.",
"Options.LogLevel.Trace": "Traçage",
"Options.LogLevel.Debug": "Débogage",
"Options.LogLevel.Info": "Informations",
"Options.LogLevel.Warning": "Avertissements",
"Options.LogLevel.Error": "Erreurs",
"Options.LogLevel.Critical": "Erreurs critiques",
"Options.TraceImports.Label": "Limite de traçage des imports",
"Options.TraceImports.Desc": "Tracer les N premiers imports de chaque module (0 = désactivé).",
"Options.LogToFile.Label": "Enregistrer dans un fichier",
"Options.LogToFile.Desc": "Copier la sortie de l’émulateur dans un fichier journal.",
"Options.LogFilePath.Label": "Chemin du fichier journal",
"Options.LogFilePath.Default": "Aucun chemin personnalisé — les journaux sont enregistrés dans user/logs à côté de l’émulateur.",
"Options.LogFilePath.Select": "Sélectionner…",
"Options.OverrideLogFile.Label": "Remplacer le fichier journal",
"Options.OverrideLogFile.Desc": "Utiliser exactement ce chemin au lieu dajouter lidentifiant du jeu et lhorodatage.",
"Options.TitleMusic.Label": "Musique du jeu",
"Options.TitleMusic.Desc": "Lire en boucle la musique daperçu du jeu sélectionné dans la bibliothèque.",
"Options.Discord.Label": "Présence Discord",
"Options.Discord.Desc": "Afficher le jeu en cours dexécution sur votre profil Discord.",
"Options.Language.Label": "Langue de l’émulateur",
"Options.Language.Desc": "Langue utilisée dans lensemble du lanceur. Le changement est immédiat.",
"Common.On": "Activé",
"Common.Off": "Désactivé",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Rechercher…",
"Console.AutoScroll": "Défilement automatique",
"Console.Split": "Détacher",
"Console.Copy": "Copier",
"Console.Clear": "Effacer",
"Console.WindowTitle": "Console SharpEmu",
"Launch.NoGameSelected": "Aucun jeu sélectionné",
"Launch.NoGameHint": "Choisissez un jeu dans la bibliothèque ou ouvrez directement un fichier eboot.bin.",
"Launch.Idle": "Inactif",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Lancer",
"Launch.Stop": "■ Arrêter",
"Launch.Running": "En cours dexécution — {0}",
"Launch.Stopping": "Arrêt en cours…",
"Launch.Exited": "Processus terminé avec le code {0} ({1})",
"Launch.ExeNotFound": "Lexécutable SharpEmu est introuvable. Compilez dabord le projet SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Fichier journal : {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Impossible de démarrer l’émulateur : {0}",
"Launch.ProcessExited": "Le processus sest terminé avec le code {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "arguments non valides",
"Exit.EbootNotFound": "eboot introuvable",
"Exit.RuntimeException": "exception dexécution",
"Exit.EmulationError": "erreur d’émulation",
"Exit.Unknown": "inconnu",
"Status.EmulatorLocating": "Émulateur : recherche en cours…",
"Status.EmulatorPath": "Émulateur : {0}",
"Status.EmulatorNotFound": "Émulateur : lexécutable SharpEmu est introuvable — compilez dabord SharpEmu.CLI.",
"Status.ScanningLibrary": "Analyse de la bibliothèque…",
"Status.AddFolderPrompt": "Ajoutez un dossier de jeux pour remplir la bibliothèque.",
"Status.LibraryScanned": "Bibliothèque analysée : {0} jeu(x) dans {1} dossier(s).",
"Status.CouldNotOpenFolder": "Impossible douvrir le dossier : {0}",
"Status.CopiedToClipboard": "{0} copié dans le presse-papiers.",
"Status.RemovedFromLibrary": "« {0} » a été retiré de la bibliothèque. Ajoutez à nouveau son dossier pour le restaurer.",
"Status.Running": "Exécution de {0}",
"Status.Stopping": "Arrêt en cours…",
"Status.Idle": "Inactif",
"Clipboard.Path": "Chemin",
"Clipboard.TitleId": "Identifiant du jeu",
"Discord.Playing": "Joue à {0}",
"Discord.Browsing": "Parcourt la bibliothèque",
"Dialog.ChooseGameFolder": "Choisir un dossier contenant des jeux",
"Dialog.OpenExecutable": "Ouvrir un exécutable à lancer",
"Dialog.PsExecutables": "Exécutables PlayStation",
"Dialog.SaveLogFile": "Choisir lemplacement du fichier journal",
"Dialog.PlainTextFiles": "Fichiers texte brut",
"Dialog.LogFiles": "Fichiers journaux"
}
+146
View File
@@ -0,0 +1,146 @@
{
"_languageName": "Hungarian",
"Page.Library": "Könyvtár",
"Page.Options": "Beállítások",
"Page.GameCount.One": "1 játék",
"Page.GameCount.Other": "{0} játékok",
"Library.SearchWatermark": "Keresés a könyvtárban",
"Library.AddFolder": " Mappa hozzáadása",
"Library.Rescan": "⟳ Újrakeresés",
"Library.OpenFile": "Fájl megnyitása…",
"Library.Context.Launch": "Inditás",
"Library.Context.OpenFolder": "Játékmappa megnyitása",
"Library.Context.CopyPath": "Elérési út másolása",
"Library.Context.CopyTitleId": "Cím ID másolása",
"Library.Context.Remove": "Eltávolítás a Könyvtárból",
"Library.Empty.Title": "A könyvtárad üres",
"Library.Empty.Hint": "Add meg a játékaidat tartalmazó mappát a kezdáshez.",
"Library.Empty.SearchTitle": "Nincs találat a elemre",
"Library.Empty.SearchHint": "A könyvtárban nincs olyan elem, amely egyezne a „{0}” kifejezéssel.",
"Library.Empty.AddFolder": " Játékmappa hozzáadása",
"Library.Loading": "Könyvtár betöltése",
"Options.General": "Általános",
"Options.Env.Tab": "Környezet",
"Options.Section.Environment": "KÖRNYEZETI VÁLTOZÓK",
"Options.Env.Desc": "Indításkor környezeti változóként az emulátorhoz átadott kapcsolók.",
"Options.Env.Bthid.Desc": "Jelenti, amely címeknél a Bluetooth HID nem elérhető, amelyeknél a kormány/FFB-közbenső szoftver végtelenül lekérdezi az adatokat.\nNormál esetben hagyja ki. Egyes címek lefagyanak, ha az inicializálás sikertelen.",
"Options.Env.LoopGuard.Desc": "Ne erőltesse a kilépést azoknál a címeknél, amelyek túl sokáig ismételnek ugyanazt a hívást.\nPróbálja ki ezt, ha egy játék betöltés közben magától kilép.",
"Options.Env.VkValidation.Desc": "Engedélyezze a Vulkan-érvényesítési rétegeket a GPU hibakereséshez.\nLassú. A Vulkan SDK telepítését igényli.",
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
"Options.Section.Emulation": "EMULÁCIÓ",
"Options.Section.Logging": "LOGOLÁS",
"Options.Section.Launcher": "INDITÓ",
"Options.CpuEngine.Label": "CPU motor",
"Options.CpuEngine.Desc": "A játék kódjának futtatásához használt végrehajtó motor.",
"Options.CpuEngine.Native": "Natív",
"Options.Strict.Label": "Szigorú dynlib felbontás",
"Options.Strict.Desc": "Indítás megszakítása, ha egy importált szimbólum nem oldható fel.",
"Options.LogLevel.Label": "Naplózási szint",
"Options.LogLevel.Desc": "Az emulátor konzol kimenetének részletessége.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Információ",
"Options.LogLevel.Warning": "Figyelmeztetés",
"Options.LogLevel.Error": "Hiba",
"Options.LogLevel.Critical": "Kritikus",
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Az első N darab import nyomon követése modulonként (0 = ki).",
"Options.LogToFile.Label": "Naplozás fájlba",
"Options.LogToFile.Desc": "Az emulátor kimenetének tükrözése egy log fájlba.",
"Options.LogFilePath.Label": "Naplófájl elérési útja",
"Options.LogFilePath.Default": "Nincs egyéni út — a logok az emulátor melletti user/logs mappába kerülnek.",
"Options.LogFilePath.Select": "Kiválasztás…",
"Options.OverrideLogFile.Label": "Naplófájl felülírása",
"Options.OverrideLogFile.Desc": "A pontos fájlútvonal használata a cím ID és időbélyeg hozzáfűzése helyett.",
"Options.TitleMusic.Label": "Címzene",
"Options.TitleMusic.Desc": "A kiválasztott játék előnézeti zenéjének ismétlése a könyvtárban.",
"Options.Discord.Label": "Discord jelenlét",
"Options.Discord.Desc": "A futó játék megjelenítése a Discord profilodon.",
"Options.Language.Label": "Emulátor nyelve",
"Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.",
"Common.On": "Be",
"Common.Off": "Ki",
"Console.Title": "KONZOL",
"Console.SearchWatermark": "Keresés...",
"Console.AutoScroll": "Automatikus görgetés",
"Console.Split": "Felosztás",
"Console.Copy": "Másolás",
"Console.Clear": "Törlés",
"Console.WindowTitle": "SharpEmu Konzol",
"Launch.NoGameSelected": "Nincs játék kiválasztva",
"Launch.NoGameHint": "Válassz egy játékot a könyvtárból, vagy nyiss meg közvetlenül egy eboot.bin fájlt.",
"Launch.Idle": "Tétlen",
"Launch.Console": "≡ Konzol",
"Launch.Launch": "▶ Inditás",
"Launch.Stop": "■ Leállítás",
"Launch.Running": "Fut — {0}",
"Launch.Stopping": "Leállítás…",
"Launch.Exited": "Kilépett a következő kóddal: {0} ({1})",
"Launch.ExeNotFound": "A SharpEmu futtatható fájl nem található. Előbb építsd fel a SharpEmu.CLI projektet (dotnet build).",
"Launch.LogFile": "Naplófájl: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Nem sikerült elindítani az emulátort: {0}",
"Launch.ProcessExited": "A folyamat kilépett a következő kóddal: {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "nemérvényes argumentumok",
"Exit.EbootNotFound": "eboot nem található",
"Exit.RuntimeException": "runtime exception",
"Exit.EmulationError": "emulációs hiba",
"Exit.Unknown": "ismeretlen",
"Status.EmulatorLocating": "Emulátor: keresés…",
"Status.EmulatorPath": "Emulátor: {0}",
"Status.EmulatorNotFound": "Emulátor: a SharpEmu futtatható fájl nem található — előbb építsd fel a SharpEmu.CLI-t.",
"Status.ScanningLibrary": "Könyvtár beolvasása…",
"Status.AddFolderPrompt": "Adj hozzá egy játékmappát a könyvtár feltöltéséhez.",
"Status.LibraryScanned": "Könyvtár beolvasva: {0} játék {1} mappában.",
"Status.CouldNotOpenFolder": "Nem sikerült megnyitni a mappát: {0}",
"Status.CopiedToClipboard": "{0} másolva a vágólapra.",
"Status.RemovedFromLibrary": "„{0}” eltávolítva a könyvtárból. A visszaállításához add hozzá újra a mappáját.",
"Status.Running": "Fut {0}",
"Status.Stopping": "Leállítás…",
"Status.Idle": "Nyugodt",
"Clipboard.Path": "Út",
"Clipboard.TitleId": "Cím ID",
"Discord.Playing": "Játékban {0}",
"Discord.Browsing": "Böngéssz a könyvtárban",
"Dialog.ChooseGameFolder": "Válassz egy mappát ami a játékaidat tartalmazza",
"Dialog.OpenExecutable": "Futtatható fájl megnyitása az indításhoz",
"Dialog.PsExecutables": "PS futtatható fájlok",
"Dialog.SaveLogFile": "Válaszd ki, hogy hova szeretnéd menteni a napló fájlokat",
"Dialog.PlainTextFiles": "Egyszerű szöveges fájlok",
"Dialog.LogFiles": "Naplózási fájlok",
"Options.About" : "Erről",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Forrás kód, hibajelentések és a projekt fejlesztése.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
"About.GithubButton": "Járulj hozzá GitHubon!",
"About.DiscordButton": "Csatlakozz a Discordunhoz!"
}
+134
View File
@@ -0,0 +1,134 @@
{
"_languageName": "Italiano",
"Page.Library": "Libreria",
"Page.Options": "Opzioni",
"Page.GameCount.One": "1 gioco",
"Page.GameCount.Other": "{0} giochi",
"Library.SearchWatermark": "Cerca nella libreria…",
"Library.AddFolder": " Aggiungi cartella",
"Library.Rescan": "⟳ Riscansiona",
"Library.OpenFile": "Apri file…",
"Library.Context.Launch": "Avvia",
"Library.Context.OpenFolder": "Apri cartella gioco",
"Library.Context.CopyPath": "Copia percorso",
"Library.Context.CopyTitleId": "Copia ID titolo",
"Library.Context.Remove": "Rimuovi dalla libreria",
"Library.Empty.Title": "La tua libreria è vuota",
"Library.Empty.Hint": "Aggiungi la cartella che contiene i tuoi giochi per partire.",
"Library.Empty.SearchTitle": "Nessun gioco corrisponde alla ricerca",
"Library.Empty.SearchHint": "Nulla nella libreria corrisponde a “{0}”.",
"Library.Empty.AddFolder": " Aggiungi cartella giochi",
"Library.Loading": "Caricamento libreria…",
"Options.General": "Generale",
"Options.Section.Emulation": "EMULAZIONE",
"Options.Section.Logging": "LOG",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Motore di esecuzione utilizzato per eseguire il codice del gioco.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Risoluzione rigorosa dynlib",
"Options.Strict.Desc": "Interrompi l'avvio quando un simbolo importato non può essere trovato.",
"Options.LogLevel.Label": "Livello Log",
"Options.LogLevel.Desc": "Livello di dettaglio dell'output console dell'emulatore.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Limite tracciamento import",
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
"Options.LogToFile.Label": "Salva log su file",
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
"Options.LogFilePath.Label": "Percorso file di log",
"Options.LogFilePath.Default": "Nessun percorso personalizzato — i log vengono salvati in user/logs accanto all'emulatore.",
"Options.LogFilePath.Select": "Seleziona…",
"Options.OverrideLogFile.Label": "Sovrascrivi file di log",
"Options.OverrideLogFile.Desc": "Usa esattamente il percorso specificato invece di aggiungere Title ID e timestamp.",
"Options.TitleMusic.Label": "Musica del titolo",
"Options.TitleMusic.Desc": "Riproduci in loop la musica di anteprima del gioco selezionato nella libreria.",
"Options.Discord.Label": "Presenza Discord",
"Options.Discord.Desc": "Mostra il gioco in esecuzione sul tuo profilo Discord.",
"Options.Language.Label": "Lingua dell'emulatore",
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
"Common.On": "On",
"Common.Off": "Off",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Cerca...",
"Console.AutoScroll": "Scorrimento automatico",
"Console.Split": "Dividi",
"Console.Copy": "Copia",
"Console.Clear": "Cancella",
"Console.WindowTitle": "Console SharpEmu",
"Launch.NoGameSelected": "Nessun gioco selezionato",
"Launch.NoGameHint": "Scegli un gioco dalla libreria, oppure apri direttamente un eboot.bin.",
"Launch.Idle": "Inattivo",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Avvia",
"Launch.Stop": "■ Ferma",
"Launch.Running": "In esecuzione — {0}",
"Launch.Stopping": "Arresto in corso…",
"Launch.Exited": "Terminato con codice {0} ({1})",
"Launch.ExeNotFound": "Eseguibile SharpEmu non trovato. Compila prima il progetto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "File di log: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Impossibile avviare l'emulatore: {0}",
"Launch.ProcessExited": "Processo terminato con codice {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argomenti non validi",
"Exit.EbootNotFound": "eboot non trovato",
"Exit.RuntimeException": "errore di runtime",
"Exit.EmulationError": "errore di emulazione",
"Exit.Unknown": "sconosciuto",
"Status.EmulatorLocating": "Emulatore: ricerca in corso…",
"Status.EmulatorPath": "Emulatore: {0}",
"Status.EmulatorNotFound": "Emulatore: eseguibile SharpEmu non trovato — compila prima SharpEmu.CLI.",
"Status.ScanningLibrary": "Scansione della libreria…",
"Status.AddFolderPrompt": "Aggiungi una cartella di giochi per popolare la libreria.",
"Status.LibraryScanned": "Libreria scansionata: {0} gioco/giochi in {1} cartella/e.",
"Status.CouldNotOpenFolder": "Impossibile aprire la cartella: {0}",
"Status.CopiedToClipboard": "{0} copiato negli appunti.",
"Status.RemovedFromLibrary": "“{0}” rimosso dalla libreria. Riaggiungi la sua cartella per ripristinarlo.",
"Status.Running": "In esecuzione: {0}",
"Status.Stopping": "Arresto in corso…",
"Status.Idle": "Inattivo",
"Clipboard.Path": "Percorso",
"Clipboard.TitleId": "ID Titolo",
"Discord.Playing": "Sta giocando a {0}",
"Discord.Browsing": "Sta esplorando la libreria",
"Dialog.ChooseGameFolder": "Scegli una cartella contenente giochi",
"Dialog.OpenExecutable": "Apri un eseguibile da avviare",
"Dialog.PsExecutables": "Eseguibili PS",
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
"Dialog.PlainTextFiles": "File di testo semplice",
"Dialog.LogFiles": "File di log"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "日本語",
"Page.Library": "ライブラリ",
"Page.Options": "オプション",
"Page.GameCount.One": "ゲーム 1本",
"Page.GameCount.Other": "ゲーム {0}本",
"Library.SearchWatermark": "ライブラリを検索…",
"Library.AddFolder": " フォルダーを追加",
"Library.Rescan": "⟳ 再スキャン",
"Library.OpenFile": "ファイルを開く…",
"Library.Context.Launch": "起動",
"Library.Context.OpenFolder": "ゲームフォルダーを開く",
"Library.Context.CopyPath": "パスをコピー",
"Library.Context.CopyTitleId": "ゲームIDをコピー",
"Library.Context.Remove": "ライブラリから削除",
"Library.Empty.Title": "ライブラリが空です",
"Library.Empty.Hint": "開始するには、ゲームが含まれるフォルダーを追加してください。",
"Library.Empty.SearchTitle": "検索条件に一致するゲームが見つかりません",
"Library.Empty.SearchHint": "ライブラリに「{0}」と一致する項目はありません。",
"Library.Empty.AddFolder": "+ ゲームフォルダーを追加",
"Library.Loading": "ライブラリを読み込み中…",
"Options.General": "一般",
"Options.Section.Emulation": "エミュレーション",
"Options.Section.Logging": "ロギング",
"Options.Section.Launcher": "ランチャー",
"Options.CpuEngine.Label": "CPUエンジン",
"Options.CpuEngine.Desc": "ゲームコードを実行するために使用される実行エンジン。",
"Options.CpuEngine.Native": "ネイティブ",
"Options.Strict.Label": "厳格な動的ライブラリ解決",
"Options.Strict.Desc": "インポートされたシンボルが解決できない場合、起動を中断します。",
"Options.LogLevel.Label": "ログレベル",
"Options.LogLevel.Desc": "エミュレータコンソールに表示されるメッセージの詳細度。",
"Options.LogLevel.Trace": "トレース",
"Options.LogLevel.Debug": "デバッグ",
"Options.LogLevel.Info": "情報",
"Options.LogLevel.Warning": "警告",
"Options.LogLevel.Error": "エラー",
"Options.LogLevel.Critical": "致命的なエラー",
"Options.TraceImports.Label": "インポートトレース制限",
"Options.TraceImports.Desc": "各モジュールの最初のN個のインポートをトレースします(0 = 無効)。",
"Options.LogToFile.Label": "ファイルに保存",
"Options.LogToFile.Desc": "エミュレータの出力をログファイルにコピーします。",
"Options.LogFilePath.Label": "ログファイルのパス",
"Options.LogFilePath.Default": "カスタムパスなし — ログはエミュレータと同じ場所の user/logs フォルダーに保存されます。",
"Options.LogFilePath.Select": "選択…",
"Options.OverrideLogFile.Label": "ログファイルを上書き",
"Options.OverrideLogFile.Desc": "ゲームIDやタイムスタンプを追加せず、指定されたパスをそのまま使用します。",
"Options.TitleMusic.Label": "ゲーム内音楽",
"Options.TitleMusic.Desc": "ライブラリで選択したゲームのプレビュー音楽をループ再生します。",
"Options.Discord.Label": "Discordステータス表示",
"Options.Discord.Desc": "現在プレイ中のゲームをDiscordのプロフィールに表示します。",
"Options.Language.Label": "エミュレータの言語",
"Options.Language.Desc": "ランチャー全体で使用される言語。変更はすぐに適用されます。",
"Common.On": "オン",
"Common.Off": "オフ",
"Console.Title": "コンソール",
"Console.SearchWatermark": "検索…",
"Console.AutoScroll": "自動スクロール",
"Console.Split": "ウィンドウを分離",
"Console.Copy": "コピー",
"Console.Clear": "消去",
"Console.WindowTitle": "SharpEmu コンソール",
"Launch.NoGameSelected": "ゲームが選択されていません",
"Launch.NoGameHint": "ライブラリからゲームを選択するか、eboot.bin ファイルを直接開いてください。",
"Launch.Idle": "待機中",
"Launch.Console": "≡ コンソール",
"Launch.Launch": "▶ 起動",
"Launch.Stop": "■ 停止",
"Launch.Running": "実行中 — {0}",
"Launch.Stopping": "停止中…",
"Launch.Exited": "プロセスがコード {0} ({1}) で終了しました",
"Launch.ExeNotFound": "SharpEmuの実行ファイルが見つかりません。先に SharpEmu.CLI プロジェクトをビルドしてください(dotnet build)。",
"Launch.LogFile": "ログファイル: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "エミュレータを起動できませんでした: {0}",
"Launch.ProcessExited": "プロセスがコード {0} ({1}) で終了しました。",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "無効な引数",
"Exit.EbootNotFound": "eboot が見つかりません",
"Exit.RuntimeException": "ランタイム例外",
"Exit.EmulationError": "エミュレーションエラー",
"Exit.Unknown": "不明",
"Status.EmulatorLocating": "エミュレータ: 位置を検索中…",
"Status.EmulatorPath": "エミュレータ: {0}",
"Status.EmulatorNotFound": "エミュレータ: SharpEmuの実行ファイルが見つかりません — 先に SharpEmu.CLI をビルドしてください。",
"Status.ScanningLibrary": "ライブラリをスキャン中…",
"Status.AddFolderPrompt": "ライブラリに表示するゲームフォルダーを追加してください。",
"Status.LibraryScanned": "ライブラリのスキャン完了: {1} 個のフォルダーから {0} 本のゲームを検出。",
"Status.CouldNotOpenFolder": "フォルダーを開けませんでした: {0}",
"Status.CopiedToClipboard": "「{0}」をクリップボードにコピーしました。",
"Status.RemovedFromLibrary": "「{0}」がライブラリから削除されました。復元するにはフォルダーを再追加してください。",
"Status.Running": "{0} を実行中",
"Status.Stopping": "停止中…",
"Status.Idle": "待機中",
"Clipboard.Path": "パス",
"Clipboard.TitleId": "ゲームID",
"Discord.Playing": "{0} をプレイ中",
"Discord.Browsing": "ライブラリを閲覧中",
"Dialog.ChooseGameFolder": "ゲームが含まれるフォルダーを選択",
"Dialog.OpenExecutable": "起動する実行ファイルを開く",
"Dialog.PsExecutables": "PlayStation 実行ファイル",
"Dialog.SaveLogFile": "ログファイルの保存先を選択",
"Dialog.PlainTextFiles": "プレーンテキストファイル",
"Dialog.LogFiles": "ログファイル"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "한국어",
"Page.Library": "라이브러리",
"Page.Options": "옵션",
"Page.GameCount.One": "게임 1개",
"Page.GameCount.Other": "게임 {0}개",
"Library.SearchWatermark": "라이브러리 검색…",
"Library.AddFolder": " 폴더 추가",
"Library.Rescan": "⟳ 다시 스캔",
"Library.OpenFile": "파일 열기…",
"Library.Context.Launch": "실행",
"Library.Context.OpenFolder": "게임 폴더 열기",
"Library.Context.CopyPath": "경로 복사",
"Library.Context.CopyTitleId": "게임 ID 복사",
"Library.Context.Remove": "라이브러리에서 제거",
"Library.Empty.Title": "라이브러리가 비어 있습니다",
"Library.Empty.Hint": "시작하려면 게임이 포함된 폴더를 추가하세요.",
"Library.Empty.SearchTitle": "검색 결과와 일치하는 게임이 없습니다",
"Library.Empty.SearchHint": "라이브러리에 '{0}'와(과) 일치하는 항목이 없습니다.",
"Library.Empty.AddFolder": " 게임 폴더 추가",
"Library.Loading": "라이브러리 불러오는 중…",
"Options.General": "일반",
"Options.Section.Emulation": "에뮬레이션",
"Options.Section.Logging": "로깅",
"Options.Section.Launcher": "런처",
"Options.CpuEngine.Label": "CPU 엔진",
"Options.CpuEngine.Desc": "게임 코드를 실행하는 데 사용되는 실행 엔진입니다.",
"Options.CpuEngine.Native": "네이티브",
"Options.Strict.Label": "엄격한 동적 라이브러리 해석",
"Options.Strict.Desc": "가져온 심볼을 해석할 수 없는 경우 실행을 중단합니다.",
"Options.LogLevel.Label": "로그 수준",
"Options.LogLevel.Desc": "에뮬레이터 콘솔에 표시할 메시지의 세부 정보 수준입니다.",
"Options.LogLevel.Trace": "트레이스",
"Options.LogLevel.Debug": "디버그",
"Options.LogLevel.Info": "정보",
"Options.LogLevel.Warning": "경고",
"Options.LogLevel.Error": "오류",
"Options.LogLevel.Critical": "치명적 오류",
"Options.TraceImports.Label": "가져오기 트레이스 한도",
"Options.TraceImports.Desc": "각 모듈의 처음 N개 가져오기를 트레이스합니다 (0 = 비활성화).",
"Options.LogToFile.Label": "파일로 저장",
"Options.LogToFile.Desc": "에뮬레이터 출력을 로그 파일에 복사합니다.",
"Options.LogFilePath.Label": "로그 파일 경로",
"Options.LogFilePath.Default": "사용자 지정 경로 없음 — 로그는 에뮬레이터 옆의 user/logs 폴더에 저장됩니다.",
"Options.LogFilePath.Select": "선택…",
"Options.OverrideLogFile.Label": "로그 파일 덮어쓰기",
"Options.OverrideLogFile.Desc": "게임 ID와 타임스탬프를 추가하는 대신 정확히 이 경로를 사용합니다.",
"Options.TitleMusic.Label": "게임 음악",
"Options.TitleMusic.Desc": "라이브러리에서 선택한 게임의 미리보기 음악을 반복 재생합니다.",
"Options.Discord.Label": "디스코드 상태 표시",
"Options.Discord.Desc": "디스코드 프로필에 현재 실행 중인 게임을 표시합니다.",
"Options.Language.Label": "에뮬레이터 언어",
"Options.Language.Desc": "런처 전체에 사용되는 언어입니다. 변경 사항은 즉시 적용됩니다.",
"Common.On": "켬",
"Common.Off": "끔",
"Console.Title": "콘솔",
"Console.SearchWatermark": "검색…",
"Console.AutoScroll": "자동 스크롤",
"Console.Split": "창 분리",
"Console.Copy": "복사",
"Console.Clear": "지우기",
"Console.WindowTitle": "SharpEmu 콘솔",
"Launch.NoGameSelected": "선택된 게임 없음",
"Launch.NoGameHint": "라이브러리에서 게임을 선택하거나 eboot.bin 파일을 직접 여세요.",
"Launch.Idle": "대기 중",
"Launch.Console": "≡ 콘솔",
"Launch.Launch": "▶ 실행",
"Launch.Stop": "■ 중지",
"Launch.Running": "실행 중 — {0}",
"Launch.Stopping": "중지 중…",
"Launch.Exited": "프로세스가 코드 {0} ({1})(으)로 종료되었습니다",
"Launch.ExeNotFound": "SharpEmu 실행 파일을 찾을 수 없습니다. 먼저 SharpEmu.CLI 프로젝트를 컴파일하세요 (dotnet build).",
"Launch.LogFile": "로그 파일: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "에뮬레이터를 시작할 수 없습니다: {0}",
"Launch.ProcessExited": "프로세스가 코드 {0} ({1})(으)로 종료되었습니다.",
"Exit.Ok": "확인",
"Exit.InvalidArguments": "잘못된 인수",
"Exit.EbootNotFound": "eboot을 찾을 수 없음",
"Exit.RuntimeException": "런타임 예외",
"Exit.EmulationError": "에뮬레이션 오류",
"Exit.Unknown": "알 수 없음",
"Status.EmulatorLocating": "에뮬레이터: 위치 검색 중…",
"Status.EmulatorPath": "에뮬레이터: {0}",
"Status.EmulatorNotFound": "에뮬레이터: SharpEmu 실행 파일을 찾을 수 없습니다 — 먼저 SharpEmu.CLI를 컴파일하세요.",
"Status.ScanningLibrary": "라이브러리 스캔 중…",
"Status.AddFolderPrompt": "라이브러리를 채우려면 게임 폴더를 추가하세요.",
"Status.LibraryScanned": "라이브러리 스캔 완료: {1}개 폴더에서 {0}개 게임 발견.",
"Status.CouldNotOpenFolder": "폴더를 열 수 없습니다: {0}",
"Status.CopiedToClipboard": "{0}이(가) 클립보드에 복사되었습니다.",
"Status.RemovedFromLibrary": "'{0}'이(가) 라이브러리에서 제거되었습니다. 복구하려면 폴더를 다시 추가하세요.",
"Status.Running": "{0} 실행 중",
"Status.Stopping": "중지 중…",
"Status.Idle": "대기 중",
"Clipboard.Path": "경로",
"Clipboard.TitleId": "게임 ID",
"Discord.Playing": "{0} 플레이 중",
"Discord.Browsing": "라이브러리 둘러보는 중",
"Dialog.ChooseGameFolder": "게임이 포함된 폴더 선택",
"Dialog.OpenExecutable": "실행할 파일 열기",
"Dialog.PsExecutables": "PlayStation 실행 파일",
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
"Dialog.PlainTextFiles": "일반 텍스트 파일",
"Dialog.LogFiles": "로그 파일"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "Nederlands",
"Page.Library": "Bibliotheek",
"Page.Options": "Opties",
"Page.GameCount.One": "1 game",
"Page.GameCount.Other": "{0} games",
"Library.SearchWatermark": "Zoeken in bibliotheek…",
"Library.AddFolder": " Map toevoegen",
"Library.Rescan": "⟳ Opnieuw scannen",
"Library.OpenFile": "Bestand openen…",
"Library.Context.Launch": "Starten",
"Library.Context.OpenFolder": "Gamemap openen",
"Library.Context.CopyPath": "Pad kopiëren",
"Library.Context.CopyTitleId": "Titel-ID kopiëren",
"Library.Context.Remove": "Verwijderen uit bibliotheek",
"Library.Empty.Title": "Je bibliotheek is leeg",
"Library.Empty.Hint": "Voeg een map met je games toe om te beginnen.",
"Library.Empty.SearchTitle": "Geen games komen overeen met je zoekopdracht",
"Library.Empty.SearchHint": "Niets in de bibliotheek komt overeen met “{0}”.",
"Library.Empty.AddFolder": " Gamemap toevoegen",
"Library.Loading": "Bibliotheek laden…",
"Options.General": "Algemeen",
"Options.Section.Emulation": "EMULATIE",
"Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER",
"Options.CpuEngine.Label": "CPU-engine",
"Options.CpuEngine.Desc": "Engine die wordt gebruikt om gamecode uit te voeren.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Strikte dynlib-resolutie",
"Options.Strict.Desc": "Laat het opstarten mislukken wanneer een geïmporteerd symbool niet kan worden opgelost.",
"Options.LogLevel.Label": "Logniveau",
"Options.LogLevel.Desc": "Uitgebreidheid van de console-uitvoer van de emulator.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Waarschuwing",
"Options.LogLevel.Error": "Fout",
"Options.LogLevel.Critical": "Kritiek",
"Options.TraceImports.Label": "Tracelimiet voor imports",
"Options.TraceImports.Desc": "Traceer de eerste N imports per module (0 = uit).",
"Options.LogToFile.Label": "Loggen naar bestand",
"Options.LogToFile.Desc": "Stuur de uitvoer van de emulator ook naar een logbestand.",
"Options.LogFilePath.Label": "Pad naar logbestand",
"Options.LogFilePath.Default": "Geen aangepast pad — logs komen terecht in user/logs naast de emulator.",
"Options.LogFilePath.Select": "Selecteren…",
"Options.OverrideLogFile.Label": "Logbestand overschrijven",
"Options.OverrideLogFile.Desc": "Gebruik het exacte bestandspad in plaats van de titel-ID en tijdstempel toe te voegen.",
"Options.TitleMusic.Label": "Titelmuziek",
"Options.TitleMusic.Desc": "Herhaal de voorbeeldmuziek van de geselecteerde game in de bibliotheek.",
"Options.Discord.Label": "Discord-aanwezigheid",
"Options.Discord.Desc": "Toon de actieve game op je Discord-profiel.",
"Options.Language.Label": "Taal van de emulator",
"Options.Language.Desc": "Taal die in de hele launcher wordt gebruikt. Wordt direct toegepast.",
"Common.On": "Aan",
"Common.Off": "Uit",
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Zoeken...",
"Console.AutoScroll": "Automatisch scrollen",
"Console.Split": "Splitsen",
"Console.Copy": "Kopiëren",
"Console.Clear": "Wissen",
"Console.WindowTitle": "SharpEmu-console",
"Launch.NoGameSelected": "Geen game geselecteerd",
"Launch.NoGameHint": "Kies een game uit de bibliotheek, of open direct een eboot.bin-bestand.",
"Launch.Idle": "Inactief",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Starten",
"Launch.Stop": "■ Stoppen",
"Launch.Running": "Actief — {0}",
"Launch.Stopping": "Stoppen…",
"Launch.Exited": "Afgesloten met code {0} ({1})",
"Launch.ExeNotFound": "SharpEmu-uitvoerbaar bestand niet gevonden. Bouw eerst het SharpEmu.CLI-project (dotnet build).",
"Launch.LogFile": "Logbestand: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Starten van de emulator mislukt: {0}",
"Launch.ProcessExited": "Proces afgesloten met code {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "ongeldige argumenten",
"Exit.EbootNotFound": "eboot niet gevonden",
"Exit.RuntimeException": "runtime-uitzondering",
"Exit.EmulationError": "emulatiefout",
"Exit.Unknown": "onbekend",
"Status.EmulatorLocating": "Emulator: zoeken…",
"Status.EmulatorPath": "Emulator: {0}",
"Status.EmulatorNotFound": "Emulator: SharpEmu-uitvoerbaar bestand niet gevonden — bouw eerst SharpEmu.CLI.",
"Status.ScanningLibrary": "Bibliotheek scannen…",
"Status.AddFolderPrompt": "Voeg een gamemap toe om de bibliotheek te vullen.",
"Status.LibraryScanned": "Bibliotheek gescand: {0} game(s) in {1} map(pen).",
"Status.CouldNotOpenFolder": "Kan map niet openen: {0}",
"Status.CopiedToClipboard": "{0} gekopieerd naar klembord.",
"Status.RemovedFromLibrary": "“{0}” verwijderd uit de bibliotheek. Voeg de map opnieuw toe om dit te herstellen.",
"Status.Running": "Actief {0}",
"Status.Stopping": "Stoppen…",
"Status.Idle": "Inactief",
"Clipboard.Path": "Pad",
"Clipboard.TitleId": "Titel-ID",
"Discord.Playing": "Speelt {0}",
"Discord.Browsing": "Bladert door de bibliotheek",
"Dialog.ChooseGameFolder": "Kies een map met games",
"Dialog.OpenExecutable": "Open een uitvoerbaar bestand om te starten",
"Dialog.PsExecutables": "PS-uitvoerbare bestanden",
"Dialog.SaveLogFile": "Selecteer waar het logbestand moet worden opgeslagen",
"Dialog.PlainTextFiles": "Platte tekstbestanden",
"Dialog.LogFiles": "Logbestanden"
}
+146
View File
@@ -0,0 +1,146 @@
{
"_languageName": "Português (Portugal)",
"Page.Library": "Biblioteca",
"Page.Options": "Opções",
"Page.GameCount.One": "1 jogo",
"Page.GameCount.Other": "{0} jogos",
"Library.SearchWatermark": "Pesquisar biblioteca…",
"Library.AddFolder": " Adicionar pasta",
"Library.Rescan": "⟳ Reanalisar",
"Library.OpenFile": "Abrir ficheiro…",
"Library.Context.Launch": "Iniciar",
"Library.Context.OpenFolder": "Abrir pasta do jogo",
"Library.Context.CopyPath": "Copiar caminho",
"Library.Context.CopyTitleId": "Copiar ID do título",
"Library.Context.Remove": "Remover da biblioteca",
"Library.Empty.Title": "A sua biblioteca está vazia",
"Library.Empty.Hint": "Adicione uma pasta com os seus jogos para começar.",
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua pesquisa",
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
"Library.Empty.AddFolder": " Adicionar pasta de jogos",
"Library.Loading": "A carregar biblioteca…",
"Options.General": "Geral",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"Options.Env.Desc": "Switches passados ao emulador como variáveis de ambiente no arranque.",
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica à espera indefinidamente.\nDeixe desativado normalmente. Alguns títulos bloqueiam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada durante demasiado tempo.\nExperimente isto quando um jogo fecha sozinho durante o carregamento.",
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração da GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "REGISTOS",
"Options.Section.Launcher": "LANÇADOR",
"Options.CpuEngine.Label": "Motor de CPU",
"Options.CpuEngine.Desc": "Motor de execução utilizado para correr o código do jogo.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Resolução estrita de dynlib",
"Options.Strict.Desc": "Falha o arranque quando um símbolo importado não pode ser resolvido.",
"Options.LogLevel.Label": "Nível de registo",
"Options.LogLevel.Desc": "Nível de detalhe da saída da consola do emulador.",
"Options.LogLevel.Trace": "Rastreio",
"Options.LogLevel.Debug": "Depuração",
"Options.LogLevel.Info": "Informação",
"Options.LogLevel.Warning": "Aviso",
"Options.LogLevel.Error": "Erro",
"Options.LogLevel.Critical": "Crítico",
"Options.TraceImports.Label": "Limite de rastreio de importações",
"Options.TraceImports.Desc": "Rastreia as primeiras N importações por módulo (0 = desativado).",
"Options.LogToFile.Label": "Registar para ficheiro",
"Options.LogToFile.Desc": "Duplicar a saída do emulador para um ficheiro de registo.",
"Options.LogFilePath.Label": "Caminho do ficheiro de registo",
"Options.LogFilePath.Default": "Sem caminho personalizado — os registos vão para user/logs junto ao emulador.",
"Options.LogFilePath.Select": "Selecionar…",
"Options.OverrideLogFile.Label": "Substituir ficheiro de registo",
"Options.OverrideLogFile.Desc": "Utilizar o caminho de ficheiro exato em vez de acrescentar o ID do título e a hora.",
"Options.TitleMusic.Label": "Música do título",
"Options.TitleMusic.Desc": "Repetir em loop a música de pré-visualização do jogo selecionado na biblioteca.",
"Options.Discord.Label": "Presença no Discord",
"Options.Discord.Desc": "Mostrar o jogo em execução no seu perfil do Discord.",
"Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
"Common.On": "Ativado",
"Common.Off": "Desativado",
"Console.Title": "CONSOLA",
"Console.SearchWatermark": "Pesquisar...",
"Console.AutoScroll": "Deslocamento automático",
"Console.Split": "Dividir",
"Console.Copy": "Copiar",
"Console.Clear": "Limpar",
"Console.WindowTitle": "Consola do SharpEmu",
"Launch.NoGameSelected": "Nenhum jogo selecionado",
"Launch.NoGameHint": "Escolha um jogo da biblioteca ou abra um eboot.bin diretamente.",
"Launch.Idle": "Inativo",
"Launch.Console": "≡ Consola",
"Launch.Launch": "▶ Iniciar",
"Launch.Stop": "■ Parar",
"Launch.Running": "Em execução — {0}",
"Launch.Stopping": "A parar…",
"Launch.Exited": "Terminou com o código {0} ({1})",
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Ficheiro de registo: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
"Launch.ProcessExited": "O processo terminou com o código {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argumentos inválidos",
"Exit.EbootNotFound": "eboot não encontrado",
"Exit.RuntimeException": "exceção em tempo de execução",
"Exit.EmulationError": "erro de emulação",
"Exit.Unknown": "desconhecido",
"Status.EmulatorLocating": "Emulador: a localizar…",
"Status.EmulatorPath": "Emulador: {0}",
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile primeiro o SharpEmu.CLI.",
"Status.ScanningLibrary": "A analisar biblioteca…",
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
"Status.LibraryScanned": "Biblioteca analisada: {0} jogo(s) em {1} pasta(s).",
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente a pasta para o restaurar.",
"Status.Running": "A executar {0}",
"Status.Stopping": "A parar…",
"Status.Idle": "Inativo",
"Clipboard.Path": "Caminho",
"Clipboard.TitleId": "ID do título",
"Discord.Playing": "A jogar {0}",
"Discord.Browsing": "A navegar na biblioteca",
"Dialog.ChooseGameFolder": "Escolha uma pasta com jogos",
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
"Dialog.PsExecutables": "Executáveis PS",
"Dialog.SaveLogFile": "Selecione onde guardar o ficheiro de registo",
"Dialog.PlainTextFiles": "Ficheiros de Texto Simples",
"Dialog.LogFiles": "Ficheiros de Registo",
"Options.About" : "Sobre",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Código-fonte, problemas e desenvolvimento do projeto.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Junte-se ao nosso Discord!"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "Русский",
"Page.Library": "Библиотека",
"Page.Options": "Настройки",
"Page.GameCount.One": "1 игра",
"Page.GameCount.Other": "Игр: {0}",
"Library.SearchWatermark": "Поиск…",
"Library.AddFolder": "+ Добавить папку",
"Library.Rescan": "⟳ Сканировать",
"Library.OpenFile": "Открыть файл…",
"Library.Context.Launch": "Запустить",
"Library.Context.OpenFolder": "Открыть папку с игрой",
"Library.Context.CopyPath": "Скопировать путь",
"Library.Context.CopyTitleId": "Скопировать ID игры",
"Library.Context.Remove": "Удалить из библиотеки",
"Library.Empty.Title": "Ваша библиотека пуста",
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
"Library.Empty.AddFolder": "+ Добавить папку с играми",
"Library.Loading": "Загрузка библиотеки…",
"Options.General": "Основные",
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
"Options.Section.Launcher": "ЛАУНЧЕР",
"Options.CpuEngine.Label": "Движок ЦП",
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
"Options.CpuEngine.Native": "Нативный",
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
"Options.LogLevel.Label": "Уровень логгирования",
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Лимит трассировки импортов",
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
"Options.LogToFile.Label": "Запись лога в файл",
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
"Options.LogFilePath.Label": "Путь к файлу лога",
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
"Options.LogFilePath.Select": "Выбрать…",
"Options.OverrideLogFile.Label": "Переопределить файл лога",
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
"Options.TitleMusic.Label": "Музыка игры",
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
"Options.Discord.Label": "Статус Discord",
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
"Options.Language.Label": "Язык эмулятора",
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
"Common.On": "Включено",
"Common.Off": "Выключено",
"Console.Title": "КОНСОЛЬ",
"Console.SearchWatermark": "Поиск...",
"Console.AutoScroll": "Авто-прокрутка",
"Console.Split": "Разделить",
"Console.Copy": "Скопировать",
"Console.Clear": "Очистить",
"Console.WindowTitle": "Консоль SharpEmu",
"Launch.NoGameSelected": "Ничего не выбрано",
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
"Launch.Idle": "Ожидание",
"Launch.Console": "≡ Консоль",
"Launch.Launch": "▶ Запустить",
"Launch.Stop": "■ Остановить",
"Launch.Running": "Запущено - {0}",
"Launch.Stopping": "Остановка…",
"Launch.Exited": "Завершено с кодом {0} ({1})",
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Лог: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "некорректные аргументы",
"Exit.EbootNotFound": "eboot не найден",
"Exit.RuntimeException": "ошибка выполнения",
"Exit.EmulationError": "ошибка эмуляции",
"Exit.Unknown": "неизвестная ошибка",
"Status.EmulatorLocating": "Эмулятор: поиск…",
"Status.EmulatorPath": "Эмулятор: {0}",
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
"Status.ScanningLibrary": "Сканирование библиотеки…",
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
"Status.Running": "Запущено: {0}",
"Status.Stopping": "Остановка…",
"Status.Idle": "Ожидание",
"Clipboard.Path": "Путь",
"Clipboard.TitleId": "ID игры",
"Discord.Playing": "Играет в {0}",
"Discord.Browsing": "Просматривает библиотеку",
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
"Dialog.PsExecutables": "Исполняемые файлы PS",
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
"Dialog.PlainTextFiles": "Текстовые файлы",
"Dialog.LogFiles": "Логи"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_languageName": "Türkçe",
"Page.Library": "Kütüphane",
"Page.Options": "Ayarlar",
"Page.GameCount.One": "1 oyun",
"Page.GameCount.Other": "{0} oyun",
"Library.SearchWatermark": "Kütüphanede ara…",
"Library.AddFolder": " Klasör ekle",
"Library.Rescan": "⟳ Yeniden tara",
"Library.OpenFile": "Dosya aç…",
"Library.Context.Launch": "Başlat",
"Library.Context.OpenFolder": "Oyun klasörünü aç",
"Library.Context.CopyPath": "Yolu kopyala",
"Library.Context.CopyTitleId": "Title ID'yi kopyala",
"Library.Context.Remove": "Kütüphaneden kaldır",
"Library.Empty.Title": "Kütüphaneniz boş",
"Library.Empty.Hint": "Başlamak için oyunlarınızı içeren bir klasör ekleyin.",
"Library.Empty.SearchTitle": "Aramanızla eşleşen oyun yok",
"Library.Empty.SearchHint": "Kütüphanede “{0}” ile eşleşen bir şey yok.",
"Library.Empty.AddFolder": " Oyun klasörü ekle",
"Library.Loading": "Kütüphane yükleniyor…",
"Options.General": "Genel",
"Options.Section.Emulation": "EMÜLASYON",
"Options.Section.Logging": "GÜNLÜKLEME",
"Options.Section.Launcher": "BAŞLATICI",
"Options.CpuEngine.Label": "CPU motoru",
"Options.CpuEngine.Desc": "Oyun kodunu çalıştırmak için kullanılan yürütme motoru.",
"Options.CpuEngine.Native": "Native",
"Options.Strict.Label": "Katı dynlib çözümü",
"Options.Strict.Desc": "İçe aktarılan bir sembol çözümlenemediğinde başlatmayı başarısız kılar.",
"Options.LogLevel.Label": "Günlük seviyesi",
"Options.LogLevel.Desc": "Emülatör konsol çıktısının ayrıntı düzeyi.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Info",
"Options.LogLevel.Warning": "Warning",
"Options.LogLevel.Error": "Error",
"Options.LogLevel.Critical": "Critical",
"Options.TraceImports.Label": "Import izleme sınırı",
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
"Options.LogToFile.Label": "Dosyaya günlükle",
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
"Options.LogFilePath.Label": "Günlük dosyası yolu",
"Options.LogFilePath.Default": "Özel yol yok — günlükler emülatörün yanındaki user/logs klasörüne yazılır.",
"Options.LogFilePath.Select": "Seç…",
"Options.OverrideLogFile.Label": "Günlük dosyasının üzerine yaz",
"Options.OverrideLogFile.Desc": "Title ID ve zaman damgası eklemek yerine tam dosya yolunu kullan.",
"Options.TitleMusic.Label": "Oyun müziği",
"Options.TitleMusic.Desc": "Kütüphanede seçili oyunun önizleme müziğini döngüye al.",
"Options.Discord.Label": "Discord durumu",
"Options.Discord.Desc": "Discord profilinde çalışan oyunu göster.",
"Options.Language.Label": "Emülatör dili",
"Options.Language.Desc": "Başlatıcı genelinde kullanılan dil. Hemen uygulanır.",
"Common.On": "Açık",
"Common.Off": "Kapalı",
"Console.Title": "KONSOL",
"Console.SearchWatermark": "Ara...",
"Console.AutoScroll": "Otomatik kaydır",
"Console.Split": "Ayır",
"Console.Copy": "Kopyala",
"Console.Clear": "Temizle",
"Console.WindowTitle": "SharpEmu Konsol",
"Launch.NoGameSelected": "Oyun seçilmedi",
"Launch.NoGameHint": "Kütüphaneden bir oyun seçin veya doğrudan bir eboot.bin açın.",
"Launch.Idle": "Boşta",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Başlat",
"Launch.Stop": "■ Durdur",
"Launch.Running": "Çalışıyor — {0}",
"Launch.Stopping": "Durduruluyor…",
"Launch.Exited": "Çıkış kodu {0} ({1})",
"Launch.ExeNotFound": "SharpEmu çalıştırılabilir dosyası bulunamadı. Önce SharpEmu.CLI projesini derleyin (dotnet build).",
"Launch.LogFile": "Günlük dosyası: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emülatör başlatılamadı: {0}",
"Launch.ProcessExited": "İşlem {0} kodla sonlandı ({1}).",
"Exit.Ok": "Tamam",
"Exit.InvalidArguments": "geçersiz argümanlar",
"Exit.EbootNotFound": "eboot bulunamadı",
"Exit.RuntimeException": "çalışma zamanı hatası",
"Exit.EmulationError": "emülasyon hatası",
"Exit.Unknown": "bilinmiyor",
"Status.EmulatorLocating": "Emülatör: bulunuyor…",
"Status.EmulatorPath": "Emülatör: {0}",
"Status.EmulatorNotFound": "Emülatör: SharpEmu çalıştırılabilir dosyası bulunamadı — önce SharpEmu.CLI'yi derleyin.",
"Status.ScanningLibrary": "Kütüphane taranıyor…",
"Status.AddFolderPrompt": "Kütüphaneyi doldurmak için bir oyun klasörü ekleyin.",
"Status.LibraryScanned": "Kütüphane tarandı: {1} klasörde {0} oyun.",
"Status.CouldNotOpenFolder": "Klasör açılamadı: {0}",
"Status.CopiedToClipboard": "{0} panoya kopyalandı.",
"Status.RemovedFromLibrary": "“{0}” kütüphaneden kaldırıldı. Geri getirmek için klasörünü yeniden ekleyin.",
"Status.Running": "{0} çalışıyor",
"Status.Stopping": "Durduruluyor…",
"Status.Idle": "Boşta",
"Clipboard.Path": "Yol",
"Clipboard.TitleId": "Title ID",
"Discord.Playing": "{0} oynuyor",
"Discord.Browsing": "Kütüphaneye göz atıyor",
"Dialog.ChooseGameFolder": "Oyunlarınızı içeren bir klasör seçin",
"Dialog.OpenExecutable": "Başlatılacak bir çalıştırılabilir dosya aç",
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
"Dialog.LogFiles": "Günlük Dosyaları"
}
+256
View File
@@ -0,0 +1,256 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.GUI;
/// <summary>
/// Loads UI strings for the launcher. Every language ships embedded in the
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
/// self-contained; an optional Languages/&lt;code&gt;.json file next to the
/// executable overrides the embedded copy for that code, so a translation
/// fix or a brand-new language never needs a rebuild.
/// </summary>
public sealed class Localization
{
public static Localization Instance { get; } = new();
public sealed record LanguageInfo(string Code, string NativeName);
private const string EmbeddedResourcePrefix = "Languages.";
private const string EmbeddedResourceSuffix = ".json";
private Dictionary<string, string> _strings = new();
private Dictionary<string, string> _fallbackStrings = new();
private Localization()
{
}
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
public string CurrentCode { get; private set; } = "en";
public string Get(string key)
{
if (_strings.TryGetValue(key, out var value))
return value;
if (_fallbackStrings.TryGetValue(key, out var fallbackValue))
return fallbackValue;
return key;
}
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
/// <summary>
/// Languages available either embedded in the binary or as a loose
/// override file, sorted by code. A loose file's declared name wins when
/// the same code exists in both places.
/// </summary>
public List<LanguageInfo> DiscoverLanguages()
{
var languages = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var code in EmbeddedLanguageCodes())
{
using var stream = OpenEmbeddedLanguageStream(code);
if (stream is not null)
{
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
}
}
try
{
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
{
var code = Path.GetFileNameWithoutExtension(file);
using var stream = File.OpenRead(file);
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
}
}
catch (Exception)
{
// No loose Languages directory: the embedded languages still stand.
}
var result = languages.Values.ToList();
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
return result;
}
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
/// english is the fallback language
public void Load(string code)
{
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
{
if (!TryLoadLooseFile("en", out var fallback) && !TryLoadEmbedded("en", out fallback))
{
fallback = new Dictionary<string, string>();
}
_fallbackStrings = fallback;
}
else if (string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
{
if (TryLoadLooseFile("en", out var enDict) || TryLoadEmbedded("en", out enDict))
{
_strings = enDict;
_fallbackStrings = enDict;
}
else
{
_strings = new Dictionary<string, string>();
_fallbackStrings = new Dictionary<string, string>();
}
CurrentCode = "en";
return;
}
// Load the requested language
if (TryLoadLooseFile(code, out var loaded) || TryLoadEmbedded(code, out loaded))
{
_strings = loaded;
}
else
{
if (_fallbackStrings.Count > 0)
_strings = new Dictionary<string, string>(_fallbackStrings);
else
_strings = new Dictionary<string, string>();
}
CurrentCode = code;
}
private static IEnumerable<string> EmbeddedLanguageCodes()
{
foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames())
{
if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) &&
name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal))
{
yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length];
}
}
}
private static Stream? OpenEmbeddedLanguageStream(string code) =>
typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}");
private static string? ReadLanguageName(Stream stream)
{
try
{
using var document = JsonDocument.Parse(stream);
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
name.ValueKind == JsonValueKind.String)
{
return name.GetString();
}
}
catch (Exception)
{
// Malformed file: fall back to the code as its own display name.
}
return null;
}
private bool TryLoadLooseFile(string code)
{
try
{
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
}
catch (Exception)
{
return false;
}
}
private bool TryLoadEmbedded(string code)
{
try
{
using var stream = OpenEmbeddedLanguageStream(code);
if (stream is null)
{
return false;
}
using var reader = new StreamReader(stream);
return TryLoad(code, reader.ReadToEnd());
}
catch (Exception)
{
return false;
}
}
private bool TryLoadLooseFile(string code, out Dictionary<string, string> result)
{
result = new Dictionary<string, string>();
try
{
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
if (!File.Exists(path))
return false;
var json = File.ReadAllText(path);
return TryLoad(json, out result);
}
catch (Exception)
{
return false;
}
}
private bool TryLoadEmbedded(string code, out Dictionary<string, string> result)
{
result = new Dictionary<string, string>();
try
{
using var stream = OpenEmbeddedLanguageStream(code);
if (stream is null)
return false;
using var reader = new StreamReader(stream);
var json = reader.ReadToEnd();
return TryLoad(json, out result);
}
catch (Exception)
{
return false;
}
}
private static bool TryLoad(string json, out Dictionary<string, string> result)
{
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
if (loaded is null)
{
result = new Dictionary<string, string>();
return false;
}
result = loaded;
return true;
}
private bool TryLoad(string code, string json)
{
if (TryLoad(json, out var dict))
{
_strings = dict;
CurrentCode = code;
return true;
}
return false;
}
}
+457 -142
View File
@@ -9,14 +9,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
Title="SharpEmu"
Width="1280" Height="820"
MinWidth="980" MinHeight="640"
WindowState="Maximized"
WindowStartupLocation="CenterScreen"
Background="{StaticResource BgBrush}"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="PreferSystemChrome"
ExtendClientAreaTitleBarHeightHint="44"
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
KeyDown="OnKeyDown">
<Grid RowDefinitions="44,*,32">
<Grid RowDefinitions="Auto,*,Auto">
<!-- Selected-game backdrop: key art behind the main content, dimmed by
a scrim so tiles and text stay readable. Fades on selection. -->
@@ -40,8 +42,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Border>
</Panel>
<!-- Title bar -->
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
the game gets the whole screen. -->
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
RenderOptions.BitmapInterpolationMode="HighQuality" />
@@ -53,119 +56,438 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
<!-- Main content -->
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
<Grid Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto,Auto">
<!-- Library toolbar -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
</Border>
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
VerticalAlignment="Center" Margin="0,0,12,0" />
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content=" Add folder"
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
VerticalAlignment="Center" Margin="0,0,8,0" />
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
VerticalAlignment="Center" />
<!-- Library / Options page switcher, with the library toolbar sharing
the same row on the right. Plain buttons (not TabItem) so there is
no underline; LB/RB hint chips flank the pair and the gamepad's
shoulder buttons actually switch pages from anywhere. -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
<Border Classes="padHint" VerticalAlignment="Center">
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
</Border>
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
<Border Classes="padHint" VerticalAlignment="Center">
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
VerticalAlignment="Center">
<TextBox x:Name="SearchBox" Watermark="Search library…" Width="240" VerticalAlignment="Center" />
<Button x:Name="AddFolderButton" Classes="ghost" Content=" Add folder" VerticalAlignment="Center" />
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
</StackPanel>
</Grid>
<!-- Cover-art grid -->
<Panel Grid.Row="1">
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
SelectionMode="Single" Padding="0">
<ListBox.ContextMenu>
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
<MenuItem.Icon>
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
<MenuItem.Icon>
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxRemove" Header="Remove from library"
Foreground="{StaticResource DangerHoverBrush}">
<MenuItem.Icon>
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="128" Height="186" Spacing="7">
<Border Classes="coverShadow" Width="128" Height="128">
<Border Classes="coverClip">
<Panel>
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
</Panel>
</Border>
</Border>
<StackPanel Spacing="2">
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Empty state -->
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
HorizontalAlignment="Center" />
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content=" Add game folder"
HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
<!-- Library page. The tile row gets extra top margin so it sits
closer to eye level (PS5 home-screen style) instead of hugging
the Library/Options switcher above it. -->
<Panel x:Name="LibraryPage" Margin="0,64,0,0">
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
SelectionMode="Single" Padding="0">
<ListBox.ContextMenu>
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
<MenuItem.Icon>
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
<MenuItem.Icon>
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
<MenuItem.Icon>
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem x:Name="CtxRemove" Header="Remove from library"
Foreground="{StaticResource DangerHoverBrush}">
<MenuItem.Icon>
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="128" Height="172" Spacing="7">
<Border Classes="coverShadow" Width="128" Height="128">
<Border Classes="coverClip">
<Panel>
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
Foreground="#E8ECF4" Opacity="0.85"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
</Panel>
</Border>
</Border>
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
TextAlignment="Center" HorizontalAlignment="Center" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Empty state -->
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
HorizontalAlignment="Center" />
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content=" Add game folder"
HorizontalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
<!-- Loading state: covers the grid while a scan (initial load,
rescan, or a just-added folder) is in flight, so the screen
never looks blank mid-scan. -->
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="False">
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
</StackPanel>
</Panel>
<!-- Options page: sub-tabs so future categories (Graphics, …) slot
in next to General. Card-grouped sections match the visual
language used by the console panel and launch bar below. -->
<Grid x:Name="OptionsPage" IsVisible="False">
<TabControl>
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="CpuEngineLabel" Text="CPU engine" FontSize="13" />
<TextBlock x:Name="CpuEngineDesc" Text="Execution engine used to run game code."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
<TextBlock x:Name="StrictDesc" Text="Fail the launch when an imported symbol cannot be resolved."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="StrictToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogLevelLabel" Text="Log level" FontSize="13" />
<TextBlock x:Name="LogLevelDesc" Text="Verbosity of the emulator console output."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="LogLevelBox" Width="160" SelectedIndex="2"
VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
</ComboBox>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="TraceImportsLabel" Text="Import trace limit" FontSize="13" />
<TextBlock x:Name="TraceImportsDesc" Text="Trace the first N imports per module (0 = off)."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<NumericUpDown Grid.Column="1" x:Name="TraceImportsBox" Width="160" Minimum="0"
Maximum="4096" Increment="16" Value="0" FormatString="0"
VerticalAlignment="Center" CornerRadius="8" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogToFileLabel" Text="Log to file" FontSize="13" />
<TextBlock x:Name="LogToFileDesc" Text="Mirror emulator output to a log file."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LogFilePathLabel" Text="Log file path" FontSize="13" />
<TextBlock x:Name="LogFilePathText" Text="No custom path"
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<Button Grid.Column="1" x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="OverrideLogFileLabel" Text="Override log file" FontSize="13" />
<TextBlock x:Name="OverrideLogFileDesc"
Text="Use the exact file path instead of appending title ID and timestamp."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="LanguageLabel" Text="Emulator language" FontSize="13" />
<TextBlock x:Name="LanguageDesc"
Text="Language used throughout the launcher. Applies immediately."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ComboBox Grid.Column="1" x:Name="LanguageBox" Width="160"
VerticalAlignment="Center" CornerRadius="8"
DisplayMemberBinding="{Binding NativeName}" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="TitleMusicLabel" Text="Title music" FontSize="13" />
<TextBlock x:Name="TitleMusicDesc" Text="Loop the selected game's preview music in the library."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
IsChecked="True" VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="DiscordLabel" Text="Discord presence" FontSize="13" />
<TextBlock x:Name="DiscordDesc" Text="Show the running game on your Discord profile."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="AboutSectionTitle"
Classes="sectionTitle"
Text="ABOUT" />
<!--Github-->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/github.png"
Width="20"
Height="20"
VerticalAlignment="Center" />
<StackPanel VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="GithubLabel"
Text="GitHub"
FontSize="13" />
<TextBlock x:Name="GithubDesc"
Text="Source code, issues and project development."
FontSize="11"
Foreground="{StaticResource MutedBrush}"
TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Button Grid.Column="1"
x:Name="GithubButton"
Classes="ghost"
Content="Open"
VerticalAlignment="Center" />
</Grid>
<!--Discord-->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/discord.png"
Width="20"
Height="20"
VerticalAlignment="Center" />
<StackPanel VerticalAlignment="Center"
Spacing="2">
<TextBlock x:Name="DiscordServerLabel"
Text="Discord"
FontSize="13" />
<TextBlock x:Name="DiscordServerDesc"
Text="Join the community, get support and follow development."
FontSize="11"
Foreground="{StaticResource MutedBrush}"
TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Button Grid.Column="1"
x:Name="DiscordButton"
Classes="ghost"
Content="Join"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle" Text="ENVIRONMENT VARIABLES" />
<TextBlock x:Name="EnvDesc"
Text="Switches passed to the emulator as environment variables at launch."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_BTHID_UNAVAILABLE" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvBthidDesc"
Text="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.&#10;Leave off normally. Some titles freeze when init fails."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLoopGuardDesc"
Text="Do not force quit titles that repeat the same call for too long.&#10;Try this when a game exits on its own while loading."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvVkValidationDesc"
Text="Enable Vulkan validation layers for GPU debugging.&#10;Slow. Requires the Vulkan SDK to be installed."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_DUMP_SPIRV" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvDumpSpirvDesc"
Text="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.&#10;Use when reporting shader or rendering bugs."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_DIRECT_MEMORY" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogDirectMemoryDesc"
Text="Log direct memory allocations and failures to the console.&#10;Use when a game aborts or exits during boot."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogNpDesc"
Text="Log NP (PlayStation Network) library calls to the console."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl>
</Grid>
</Panel>
<!-- Console (collapsible) -->
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
Watermark="Search..." Width="320" />
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
FontSize="12" Margin="0,0,12,0" />
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
Padding="10,4" Margin="0,0,8,0" />
<Button Grid.Column="5" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
Padding="10,4" />
</Grid>
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
@@ -179,8 +501,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
</Border>
<!-- Launch bar -->
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
<!-- Launch bar; capped so it does not sprawl on a maximized window. -->
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14" MaxWidth="1280">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="Auto,*,Auto">
@@ -199,9 +521,32 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Panel>
</Border>
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
TextTrimming="CharacterEllipsis" />
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center" Margin="14,0,14,0">
<Grid ColumnDefinitions="Auto,Auto,*">
<TextBlock Grid.Column="0" x:Name="SelectedGameTitle" Text="No game selected" FontSize="16"
FontWeight="Bold" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
MaxWidth="340" Margin="0,0,10,0" />
<!-- Title id / version / size badges, right next to the
title. The title's own MaxWidth (not a "*" column) is
what keeps them from drifting to the far right. -->
<StackPanel Grid.Column="1" x:Name="SelectedBadgesRow" Orientation="Horizontal" Spacing="6"
IsVisible="False" VerticalAlignment="Center">
<Border Classes="pill" IsVisible="{Binding HasTitleId, FallbackValue=False}">
<TextBlock Text="{Binding TitleId}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill" IsVisible="{Binding HasVersion, FallbackValue=False}">
<TextBlock Text="{Binding VersionText}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill">
<TextBlock Text="{Binding SizeText, FallbackValue=''}" FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
</Grid>
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" Spacing="7">
@@ -213,48 +558,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
</StackPanel>
</Grid>
<!-- Launch options (collapsible) -->
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="CPU engine" />
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
<ComboBoxItem Content="Native" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Log level" />
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
<ComboBoxItem Content="Trace" />
<ComboBoxItem Content="Debug" />
<ComboBoxItem Content="Info" />
<ComboBoxItem Content="Warning" />
<ComboBoxItem Content="Error" />
<ComboBoxItem Content="Critical" />
</ComboBox>
</StackPanel>
<StackPanel Margin="0,0,24,0">
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
Value="0" FormatString="0" CornerRadius="8" />
</StackPanel>
<StackPanel>
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
</Grid>
<!-- Status bar -->
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
<Grid x:Name="StatusBar" Grid.Row="2" Height="32" Background="{StaticResource ChromeBrush}"
ColumnDefinitions="*,Auto">
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
TextTrimming="CharacterEllipsis" />
File diff suppressed because it is too large Load Diff
+27 -5
View File
@@ -10,8 +10,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version>
<!-- Required by the source-generated LibraryImport stubs in the linked
controller readers below. -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</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" />
@@ -22,14 +31,27 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ItemGroup>
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
</ItemGroup>
<!-- The DualSense HID reader is shared with the emulator's pad HLE. It is
dependency-free, so it is compiled in directly rather than pulling a
reference to all of SharpEmu.Libs into the launcher. -->
<ItemGroup>
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
<EmbeddedResource Include="Languages\*.json">
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
with the emulator's host input backend. They are dependency-free, so
they are compiled in directly rather than pulling a reference to all
of SharpEmu.HLE into the launcher. -->
<ItemGroup>
<Compile Include="..\SharpEmu.HLE\Host\HostGamepadState.cs" Link="Input/HostGamepadState.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsHidNative.cs" Link="Input/WindowsHidNative.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsDualSenseReader.cs" Link="Input/WindowsDualSenseReader.cs" />
<Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsXInputReader.cs" Link="Input/WindowsXInputReader.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(120).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);
}
+3
View File
@@ -180,6 +180,9 @@
"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);
}
}
Binary file not shown.
+51 -10
View File
@@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
@@ -238,23 +239,63 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
return false;
}
var bytes = new byte[capacity];
for (var index = 0; index < bytes.Length; index++)
const int StackBufferLength = 512;
const int ReadChunkLength = 128;
var rented = capacity > StackBufferLength ? ArrayPool<byte>.Shared.Rent(capacity) : null;
Span<byte> bytes = rented is null ? stackalloc byte[StackBufferLength] : rented;
try
{
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
var length = 0;
while (length < capacity)
{
return false;
// Bulk-read in bounded chunks rather than the full capacity: the string
// may end just before unmapped memory, and overreading past the
// terminator by more than a chunk could fault where the old
// byte-by-byte loop succeeded.
var chunk = Math.Min(ReadChunkLength, capacity - length);
var span = bytes.Slice(length, chunk);
if (Memory.TryRead(address + (ulong)length, span))
{
var terminator = span.IndexOf((byte)0);
if (terminator >= 0)
{
value = Encoding.UTF8.GetString(bytes[..(length + terminator)]);
return true;
}
length += chunk;
continue;
}
// The chunk touches an unreadable range; fall back to per-byte reads so a
// terminator sitting before the bad byte still yields the string.
for (var i = 0; i < chunk; i++)
{
if (!Memory.TryRead(address + (ulong)(length + i), bytes.Slice(length + i, 1)))
{
return false;
}
if (bytes[length + i] == 0)
{
value = Encoding.UTF8.GetString(bytes[..(length + i)]);
return true;
}
}
length += chunk;
}
if (bytes[index] == 0)
value = Encoding.UTF8.GetString(bytes[..capacity]);
return true;
}
finally
{
if (rented is not null)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
ArrayPool<byte>.Shared.Return(rented);
}
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
public bool PushUInt64(ulong value)
+13
View File
@@ -0,0 +1,13 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
[Flags]
public enum GuestPageProtection
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
}
+25 -25
View File
@@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot(
ulong LastReturnRip,
string? BlockReason);
/// <summary>
/// Continuation state for a blocked guest thread, replacing the closure pair a blocking
/// wait used to allocate. TryWake runs under the scheduler's guest-thread gate and
/// returns true when the waiter has a final result and the thread should be re-readied;
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
/// its return value becomes the guest's RAX for the resumed call.
/// </summary>
public interface IGuestThreadBlockWaiter
{
int Resume();
bool TryWake();
}
public interface IGuestThreadScheduler
{
bool SupportsGuestContextTransfer { get; }
@@ -106,10 +120,7 @@ public static class GuestThreadExecution
private static string? _pendingBlockWakeKey;
[ThreadStatic]
private static Func<int>? _pendingBlockResumeHandler;
[ThreadStatic]
private static Func<bool>? _pendingBlockWakeHandler;
private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
[ThreadStatic]
private static long _pendingBlockDeadlineTimestamp;
@@ -157,8 +168,7 @@ public static class GuestThreadExecution
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
@@ -179,8 +189,7 @@ public static class GuestThreadExecution
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitValue = 0;
@@ -211,8 +220,7 @@ public static class GuestThreadExecution
CpuContext? context,
string reason,
string? wakeKey = null,
Func<int>? resumeHandler = null,
Func<bool>? wakeHandler = null,
IGuestThreadBlockWaiter? waiter = null,
long blockDeadlineTimestamp = 0)
{
if (!IsGuestThread)
@@ -222,8 +230,7 @@ public static class GuestThreadExecution
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
_pendingBlockResumeHandler = resumeHandler;
_pendingBlockWakeHandler = wakeHandler;
_pendingBlockWaiter = waiter;
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
{
@@ -255,7 +262,6 @@ public static class GuestThreadExecution
out hasContinuation,
out _,
out _,
out _,
out _);
}
@@ -264,16 +270,14 @@ public static class GuestThreadExecution
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler)
out IGuestThreadBlockWaiter? waiter)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out resumeHandler,
out wakeHandler,
out waiter,
out _);
}
@@ -282,8 +286,7 @@ public static class GuestThreadExecution
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler,
out IGuestThreadBlockWaiter? waiter,
out long blockDeadlineTimestamp)
{
reason = _pendingBlockReason ?? string.Empty;
@@ -292,8 +295,7 @@ public static class GuestThreadExecution
continuation = default;
hasContinuation = false;
wakeKey = string.Empty;
resumeHandler = null;
wakeHandler = null;
waiter = null;
blockDeadlineTimestamp = 0;
return false;
}
@@ -301,15 +303,13 @@ public static class GuestThreadExecution
continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason;
resumeHandler = _pendingBlockResumeHandler;
wakeHandler = _pendingBlockWakeHandler;
waiter = _pendingBlockWaiter;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null;
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockWaiter = null;
_pendingBlockDeadlineTimestamp = 0;
return true;
}
@@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// General-purpose register snapshot of a suspended thread, produced by
/// <see cref="IHostThreading.TryCaptureThreadRegisters"/>. Registers are named
/// after the guest ISA (x86-64), which every supported host executes natively.
/// </summary>
public readonly record struct HostCapturedRegisters(
ulong Rip,
ulong Rsp,
ulong Rbp,
ulong Rax,
ulong Rbx,
ulong Rcx,
ulong Rdx);
+46
View File
@@ -0,0 +1,46 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host-neutral gamepad button flags. Named after the PlayStation layout the guest API
/// exposes, but the numeric values are the seam's own — the HLE pad exports translate
/// them to SCE_PAD_BUTTON bits, so guest ABI values never leak into host backends.
/// </summary>
[Flags]
public enum HostGamepadButtons : uint
{
None = 0,
Up = 1 << 0,
Down = 1 << 1,
Left = 1 << 2,
Right = 1 << 3,
Cross = 1 << 4,
Circle = 1 << 5,
Square = 1 << 6,
Triangle = 1 << 7,
L1 = 1 << 8,
R1 = 1 << 9,
L2 = 1 << 10,
R2 = 1 << 11,
L3 = 1 << 12,
R3 = 1 << 13,
Options = 1 << 14,
TouchPad = 1 << 15,
}
/// <summary>
/// Snapshot of one host gamepad: sticks are 0..255 with 128 centered and Y growing
/// downward; triggers 0..255. Unmanaged on purpose so per-frame polls can stackalloc
/// snapshot buffers.
/// </summary>
public readonly record struct HostGamepadState(
bool Connected,
HostGamepadButtons Buttons,
byte LeftX,
byte LeftY,
byte RightX,
byte RightY,
byte LeftTrigger,
byte RightTrigger);
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Platform-neutral page protection. Values intentionally enumerate the exact
/// combinations the emulator uses today so each maps 1:1 onto a single native
/// protection constant (PAGE_* on Windows, PROT_* elsewhere).
/// </summary>
public enum HostPageProtection
{
NoAccess,
ReadOnly,
ReadWrite,
Execute,
ReadExecute,
ReadWriteExecute,
ExecuteWriteCopy,
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE.Host.Windows;
namespace SharpEmu.HLE.Host;
/// <summary>
/// Process-wide access point for the host platform backend. Static HLE export
/// classes (which cannot receive constructor injection) resolve host primitives
/// through <see cref="Current"/>; injectable components should instead accept an
/// <see cref="IHostPlatform"/> and merely default to this.
/// </summary>
public static class HostPlatform
{
private static readonly Lazy<IHostPlatform> Instance = new(Create);
public static IHostPlatform Current => Instance.Value;
private static IHostPlatform Create()
{
// The Windows backend executes guest x86-64 natively and emits x86-64
// stubs, so a native ARM64 process must be rejected here rather than
// crash undefined later (x64 processes under emulation report X64).
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
return new WindowsHostPlatform();
}
throw new PlatformNotSupportedException(
"SharpEmu native guest execution requires a host platform backend and none exists for this OS/architecture yet (currently Windows x64 only).");
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Result of <see cref="IHostMemory.Query"/>. The Raw* fields carry the
/// untranslated OS values so call sites migrated from direct VirtualQuery use
/// keep comparing (and logging) the exact native words they did before;
/// <see cref="State"/> and <see cref="Protection"/> are neutral views.
/// </summary>
public readonly record struct HostRegionInfo(
ulong BaseAddress,
ulong AllocationBase,
ulong RegionSize,
HostRegionState State,
uint RawState,
HostPageProtection Protection,
uint RawProtection,
uint RawAllocationProtection);
+11
View File
@@ -0,0 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
public enum HostRegionState
{
Free,
Reserved,
Committed,
}
@@ -0,0 +1,21 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host functions whose addresses the execution engine bakes into emitted
/// stubs (spin-waits, worker run loops, TLS reads). Enum-keyed rather than a
/// free-form name lookup: each platform's emitters need their own specific
/// functions, and this set is exactly what the current emitters consume.
/// </summary>
public enum HostRuntimeFunction
{
TlsGetValue,
QueryPerformanceCounter,
SwitchToThread,
Sleep,
WaitForSingleObject,
SetEvent,
ExitThread,
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host audio-output device access. The HLE audio exports convert guest submissions to
/// interleaved stereo 16-bit PCM (the format every backend accepts) and feed them through
/// streams opened here; everything device-specific — queueing, backpressure, native
/// buffer lifetime — lives behind <see cref="IHostAudioStream"/>.
/// </summary>
public interface IHostAudioOutput
{
/// <summary>Backend identifier for diagnostics (e.g. "winmm").</summary>
string BackendName { get; }
/// <summary>
/// Opens an interleaved stereo 16-bit PCM output stream at the given sample rate.
/// Throws when the host has no usable output device; callers degrade to a silent
/// port and pace the guest instead.
/// </summary>
IHostAudioStream OpenStereoPcm16Stream(uint sampleRate);
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// One open host audio output stream. Submissions are interleaved stereo 16-bit PCM at
/// the sample rate the stream was opened with.
/// </summary>
public interface IHostAudioStream : IDisposable
{
/// <summary>
/// Submits one buffer. May block briefly while the device drains its queue (this is
/// what paces the guest's audio loop); returns false when the stream cannot accept
/// audio, in which case the caller paces the guest itself.
/// </summary>
bool Submit(ReadOnlySpan<byte> stereoPcm16);
}
@@ -0,0 +1,32 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Installation mechanics for the process-wide fault interception the execution
/// engine relies on to catch guest faults. Deliberately thin: the managed
/// handlers keep receiving the platform's raw exception data, and the emitted
/// pre-filter thunk is an opaque per-platform unit. Implementations live next
/// to the execution backend (SharpEmu.Core), not behind HostPlatform.Current.
/// </summary>
public interface IHostFaultHandling
{
/// <summary>
/// Emits the native thunk that wraps a managed fault handler: it pre-filters
/// exception codes that must never enter managed code and, when the fault
/// happened on a guest stack, switches to the host stack saved in
/// <paramref name="hostRspSwitchTlsSlot"/> before the call. Returns 0 on failure.
/// </summary>
nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress);
void FreeThunk(nint thunk);
/// <summary>Installs a first-chance handler ahead of existing ones; returns a removal handle (0 on failure).</summary>
nint AddFirstChanceHandler(nint thunk);
void RemoveHandler(nint handle);
/// <summary>Installs the last-resort filter; pass 0 to clear.</summary>
void SetUnhandledFilter(nint thunk);
}

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