Compare commits

...

43 Commits

Author SHA1 Message Date
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
123 changed files with 16450 additions and 1590 deletions
+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}"
+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>
+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

+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
+16
View File
@@ -5,6 +5,7 @@ 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;
@@ -111,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}");
@@ -123,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))
+3
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'">
+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,7 @@ 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;
@@ -134,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;
@@ -145,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)
@@ -350,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;
}
@@ -359,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;
}
@@ -391,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);
@@ -415,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,8 +102,8 @@ 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);
// Thread-mode probe: a hardware exception raised while this thread is inside
// the managed import gateway means the VEH->managed reentry happened from
@@ -112,7 +114,7 @@ public sealed partial class DirectExecutionBackend
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
}
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
}
@@ -127,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;
@@ -140,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!");
@@ -185,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];
@@ -198,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}");
}
}
@@ -248,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");
@@ -295,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;
}
@@ -332,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(
@@ -479,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;
}
@@ -491,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);
}
@@ -798,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;
}
@@ -916,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;
}
@@ -943,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;
@@ -955,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;
@@ -966,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;
@@ -985,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}");
@@ -993,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;
@@ -1008,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;
@@ -1019,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;
@@ -1044,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}");
@@ -1085,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)
@@ -8,8 +8,10 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Cpu.Native.Windows;
using SharpEmu.Core.Loader;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -69,23 +71,23 @@ public sealed partial class DirectExecutionBackend
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
if (exceptionRecord->ExceptionCode != 3221225477u)
if (exceptionRecord->ExceptionCode != WindowsFaultCodes.AccessViolation)
{
return 0;
}
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
ulong value = ReadCtxU64(contextRecord, 248);
ulong value = ReadCtxU64(contextRecord, CTX_RIP);
ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
{
return 0;
}
ulong rsp = ReadCtxU64(contextRecord, 152);
WriteCtxU64(contextRecord, 120, 0uL);
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
WriteCtxU64(contextRecord, CTX_RAX, 0uL);
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
{
WriteCtxU64(contextRecord, 152, nextRsp);
WriteCtxU64(contextRecord, 248, returnRip);
WriteCtxU64(contextRecord, CTX_RSP, nextRsp);
WriteCtxU64(contextRecord, CTX_RIP, returnRip);
Interlocked.Increment(ref _rawSentinelRecoveries);
if (LogThreadMode)
{
@@ -161,7 +163,7 @@ public sealed partial class DirectExecutionBackend
*(ulong*)(xmmSlot + 8));
}
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
{
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
@@ -238,6 +240,22 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 0uL;
return 0uL;
}
if (_hostShutdownRequested)
{
if (isGuestWorker &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "host shutdown"))
{
cpuContext[CpuRegister.Rax] = 0uL;
return 0uL;
}
if (!isGuestWorker &&
TryAbortGuestForHostShutdown(argPackPtr, num, num7))
{
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
}
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
@@ -245,12 +263,13 @@ public sealed partial class DirectExecutionBackend
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
bool flag5 = false;
ExportedFunction? matchedExport = importStubEntry.Export;
var traceFlags = importStubEntry.TraceFlags;
bool periodicTrace = num <= 128 ||
(num >= 240 && num <= 400) ||
(num >= 900 && num <= 1300) ||
num % 100000 == 0L ||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery1000) != 0 && (num <= 256 || num % 1000 == 0L)) ||
((traceFlags & ImportStubTraceFlags.PeriodicEvery128) != 0 && (num <= 256 || num % 128 == 0)) ||
flag ||
flag2 ||
flag3;
@@ -311,15 +330,15 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rsi],
cpuContext[CpuRegister.Rdx]);
}
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
if ((traceFlags & ImportStubTraceFlags.Memset) != 0 && num <= 256)
{
Console.Error.WriteLine($"[LOADER][TRACE] memset#{num}: dst=0x{cpuContext[CpuRegister.Rdi]:X16} val=0x{cpuContext[CpuRegister.Rsi] & 0xFF:X2} len=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if (importStubEntry.Nid == "tsvEmnenz48" && num <= 64)
if ((traceFlags & ImportStubTraceFlags.CxaAtexit) != 0 && num <= 64)
{
Console.Error.WriteLine($"[LOADER][TRACE] __cxa_atexit#{num}: func=0x{cpuContext[CpuRegister.Rdi]:X16} arg=0x{cpuContext[CpuRegister.Rsi]:X16} dso=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
if (importStubEntry.Nid == "bzQExy189ZI" || importStubEntry.Nid == "8G2LB+A3rzg")
if ((traceFlags & ImportStubTraceFlags.RawArgs) != 0)
{
Console.Error.WriteLine($"[LOADER][TRACE] {importStubEntry.Nid}#{num}: rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16} ret=0x{num7:X16}");
}
@@ -349,7 +368,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush();
}
}
if (importStubEntry.Nid == "Ou3iL1abvng")
if ((traceFlags & ImportStubTraceFlags.StackChkFail) != 0)
{
if (_logStackCheck)
{
@@ -381,7 +400,7 @@ public sealed partial class DirectExecutionBackend
ActiveGuestReturnSlotAddress);
try
{
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
if (importStubEntry.Kind == ImportStubKind.BootstrapBridge)
{
if (_logBootstrap)
{
@@ -390,12 +409,11 @@ public sealed partial class DirectExecutionBackend
orbisGen2Result = DispatchBootstrapBridge();
}
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
else if (importStubEntry.Kind == ImportStubKind.KernelDynlibDlsym)
{
orbisGen2Result = DispatchKernelDynlibDlsym();
}
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
else if (importStubEntry.Kind == ImportStubKind.Il2CppApiLookupSymbol)
{
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
}
@@ -736,6 +754,9 @@ public sealed partial class DirectExecutionBackend
var expectedEqueueTimeout =
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedEventFlagTimeout =
string.Equals(nid, "JTvBflhYazQ", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
var expectedMutexTrylockBusy =
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
@@ -748,6 +769,7 @@ public sealed partial class DirectExecutionBackend
if (!expectedFileProbeMiss &&
!expectedTimedWaitTimeout &&
!expectedEqueueTimeout &&
!expectedEventFlagTimeout &&
!expectedMutexTrylockBusy &&
!expectedUserServiceNoEvent &&
!expectedPrivacyInvalidParameter)
@@ -810,14 +832,15 @@ public sealed partial class DirectExecutionBackend
return !_logUsleep;
}
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
// slows guest spinlocks enough to livelock (Demon's Souls).
// Mutex lock uses this block-capable leaf path. Keep it out of the no-block subset.
return nid is
"9UK1vLZQft4" or // scePthreadMutexLock
"7H0iTOciTLo" or // pthread_mutex_lock
"tn3VlD0hG60" or // scePthreadMutexUnlock
"2Z+PpY6CaJg" or // pthread_mutex_unlock
"EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock
"q1cHNfGycLI" or // scePadRead
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
@@ -876,7 +899,7 @@ public sealed partial class DirectExecutionBackend
"Q2V+iqvjgC0" or // vsnprintf
"j4ViWNHEgww" or // strlen
"5jNubw4vlAA" or // strnlen
"LHMrG7e8G78" or // wcslen
"LHMrG7e8G78" or // wcsmisc
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
@@ -970,6 +993,31 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe bool TryAbortGuestForHostShutdown(nint argPackPtr, long dispatchIndex, ulong returnRip)
{
ulong hostExit = ActiveEntryReturnSentinelRip;
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
{
return false;
}
try
{
*(ulong*)(argPackPtr + 96) = hostExit;
}
catch
{
return false;
}
ActiveForcedGuestExit = true;
if (string.IsNullOrWhiteSpace(LastError))
{
LastError = "Host shutdown requested.";
}
Console.Error.WriteLine(
$"[LOADER][INFO] Guest unwind for host shutdown at import#{dispatchIndex} ret=0x{returnRip:X16} -> host_exit=0x{hostExit:X16}");
return true;
}
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
{
ulong hostExit = ActiveEntryReturnSentinelRip;
@@ -1076,7 +1124,15 @@ public sealed partial class DirectExecutionBackend
}
private static bool IsImportLoopGuardBoundary(string nid) =>
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
nid switch
{
"1jfXLRVzisc" => true, // sceKernelUsleep
"QcteRwbsnV0" => true, // usleep
"n88vx3C5nW8" => true, // gettimeofday
"Zxa0VhQVTsk" => true, // sceKernelWaitSema
"T72hz6ffq08" => true, // scePthreadYield
_ => false
};
private void ResetImportLoopPattern()
{
@@ -1671,9 +1727,9 @@ public sealed partial class DirectExecutionBackend
{
var candidateBase = ImportStubRegionCanonicalBase -
(ulong)candidateIndex * ImportStubRegionAddressStride;
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
if (!_hostMemory.Query(candidateBase, out var memoryInfo) ||
memoryInfo.RegionSize == 0 ||
memoryInfo.State != 4096)
memoryInfo.State != HostRegionState.Committed)
{
continue;
}
@@ -2012,7 +2068,7 @@ public sealed partial class DirectExecutionBackend
uint flNewProtect = default(uint);
try
{
if (Marshal.ReadByte(num2) != 232 || !VirtualProtect((void*)num, 5u, 64u, &flNewProtect))
if (Marshal.ReadByte(num2) != 232 || !_hostMemory.Protect((ulong)(void*)num, 5u, HostPageProtection.ReadWriteExecute, out flNewProtect))
{
return;
}
@@ -2020,7 +2076,7 @@ public sealed partial class DirectExecutionBackend
{
Marshal.WriteByte(num2 + i, 144);
}
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u);
_hostMemory.FlushInstructionCache((ulong)(void*)num, 5u);
_patchedEa020eLookupCall = true;
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
}
@@ -2031,7 +2087,7 @@ public sealed partial class DirectExecutionBackend
{
if (flNewProtect != 0)
{
VirtualProtect((void*)num, 5u, flNewProtect, &flNewProtect);
_hostMemory.ProtectRaw((ulong)(void*)num, 5u, flNewProtect, out flNewProtect);
}
}
}
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -35,20 +36,6 @@ public sealed partial class DirectExecutionBackend
private bool _nativeWorkersDisposed;
private int _nativeWorkerCreationFailedLogged;
private const uint StackSizeParamIsAReservation = 0x00010000u;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint CreateThread(
nint lpThreadAttributes,
nuint dwStackSize,
nint lpStartAddress,
nint lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
// thread; falls back to the historical inline calli (guest frames above this
// thread's managed frames) when workers are disabled or unavailable.
@@ -61,7 +48,7 @@ public sealed partial class DirectExecutionBackend
var worker = RentNativeGuestExecutor();
if (worker is null)
{
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
return CallNativeEntry(entryStub);
}
try
@@ -229,7 +216,7 @@ public sealed partial class DirectExecutionBackend
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
{
if (!EnsureKernel32Exports())
if (!EnsureHostRuntimeExports(backend._hostSymbols))
{
return null;
}
@@ -242,32 +229,27 @@ public sealed partial class DirectExecutionBackend
return executor;
}
private static bool EnsureKernel32Exports()
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
{
if (_exitThreadAddress != 0)
{
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
}
nint kernel32 = GetModuleHandle("kernel32.dll");
if (kernel32 == 0)
{
return false;
}
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
_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 = VirtualAlloc(null, 4096u, 12288u, 4u);
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
if (_controlBlock == null)
{
return false;
}
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
if (_loopStub == null)
{
return false;
@@ -349,17 +331,15 @@ public sealed partial class DirectExecutionBackend
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
uint oldProtect = 0;
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
{
return false;
}
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
_threadHandle = CreateThread(
0,
WorkerStackReservation,
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
_threadHandle = _backend._hostThreading.CreateNativeThread(
(nint)_loopStub,
0,
StackSizeParamIsAReservation,
WorkerStackReservation,
out _nativeThreadId);
if (_threadHandle == 0)
{
@@ -465,7 +445,7 @@ public sealed partial class DirectExecutionBackend
_prevYieldRequested = _activeGuestThreadYieldRequested;
_prevYieldReason = _activeGuestThreadYieldReason;
_prevState = _activeGuestThreadState;
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
_entered = true;
_activeExecutionBackend = backend;
@@ -477,11 +457,11 @@ public sealed partial class DirectExecutionBackend
_activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!);
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (_runState is { } state)
{
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
}
if (_runAffinityMask != 0)
{
@@ -511,7 +491,7 @@ public sealed partial class DirectExecutionBackend
{
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
}
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
_activeExecutionBackend = _prevBackend;
_activeCpuContext = _prevContext;
@@ -548,8 +528,8 @@ public sealed partial class DirectExecutionBackend
var exited = _threadHandle == 0;
if (_threadHandle != 0)
{
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
CloseHandle(_threadHandle);
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
_backend._hostThreading.CloseThreadHandle(_threadHandle);
_threadHandle = 0;
}
if (!exited)
@@ -563,12 +543,12 @@ public sealed partial class DirectExecutionBackend
}
if (_loopStub != null)
{
VirtualFree(_loopStub, 0u, 32768u);
_backend._hostMemory.Free((ulong)_loopStub);
_loopStub = null;
}
if (_controlBlock != null)
{
VirtualFree(_controlBlock, 0u, 32768u);
_backend._hostMemory.Free((ulong)_controlBlock);
_controlBlock = null;
}
if (_selfHandle.IsAllocated)
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);
}
+1 -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;
+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;
}
+19 -6
View File
@@ -17,7 +17,9 @@ namespace SharpEmu.Core.Loader;
public sealed class SelfLoader : ISelfLoader
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
private const uint SelfMagic = 0x4F153D1D;
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;
@@ -323,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)
@@ -345,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>());
}
@@ -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 &&
+134 -85
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,26 @@ 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 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 +60,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 +105,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 +139,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 +177,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;
}
@@ -336,6 +321,41 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
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 +365,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();
@@ -430,35 +450,35 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
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 +570,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 +750,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 +764,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);
}
}
@@ -932,12 +993,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 +1012,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 +1026,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 +1059,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 +1076,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 +1137,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;
}
}
+8 -2
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;
@@ -86,14 +87,19 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
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,
+3
View File
@@ -48,6 +48,9 @@ public sealed class GuiSettings
/// <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
+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": "ملفات السجل"
}
+129
View File
@@ -0,0 +1,129 @@
{
"_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.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"
}
+18 -1
View File
@@ -26,6 +26,15 @@
"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",
@@ -125,5 +134,13 @@
"Dialog.PsExecutables": "PS executables",
"Dialog.SaveLogFile": "Select where to save the Log file",
"Dialog.PlainTextFiles": "Plain Text Files",
"Dialog.LogFiles": "Log 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"
}
+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"
}
+100 -8
View File
@@ -22,6 +22,8 @@ public sealed class Localization
private const string EmbeddedResourceSuffix = ".json";
private Dictionary<string, string> _strings = new();
private Dictionary<string, string> _fallbackStrings = new();
private Localization()
{
@@ -32,7 +34,16 @@ public sealed class Localization
public string CurrentCode { get; private set; } = "en";
public string Get(string key) => _strings.TryGetValue(key, out var value) ? value : key;
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);
@@ -74,13 +85,46 @@ public sealed class Localization
}
/// <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 (!TryLoadLooseFile(code) && !TryLoadEmbedded(code) &&
!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
{
_ = TryLoadLooseFile("en") || TryLoadEmbedded("en");
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()
@@ -149,16 +193,64 @@ public sealed class Localization
}
}
private bool TryLoad(string code, string json)
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;
}
_strings = loaded;
CurrentCode = code;
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;
}
}
+148
View File
@@ -317,6 +317,154 @@ SPDX-License-Identifier: GPL-2.0-or-later
</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>
+95 -1
View File
@@ -25,6 +25,7 @@ namespace SharpEmu.GUI;
public partial class MainWindow : Window
{
private const int MaxConsoleLines = 4000;
private const int MaxConsoleLinesPerFlush = 500;
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
@@ -124,6 +125,18 @@ public partial class MainWindow : Window
UpdateDiscordPresence();
};
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
EnvBthidToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_DUMP_SPIRV", EnvDumpSpirvToggle.IsChecked == true);
EnvLogDirectMemoryToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_DIRECT_MEMORY", EnvLogDirectMemoryToggle.IsChecked == true);
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
@@ -146,6 +159,25 @@ public partial class MainWindow : Window
};
_gamepadTimer.Tick += (_, _) => PollGamepad();
_gamepadTimer.Start();
GithubButton.Click += (_, _) =>
{
Process.Start(new ProcessStartInfo
{
FileName = "https://github.com/par274/sharpemu",
UseShellExecute = true
});
};
DiscordButton.Click += (_, _) =>
{
Process.Start(new ProcessStartInfo
{
FileName = "https://discord.com/invite/6GejPEDqpc",
UseShellExecute = true
});
};
}
/// <summary>
@@ -383,6 +415,15 @@ public partial class MainWindow : Window
LoadingStateText.Text = loc.Get("Library.Loading");
GeneralTabItem.Header = loc.Get("Options.General");
EnvTabItem.Header = loc.Get("Options.Env.Tab");
EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
EnvDesc.Text = loc.Get("Options.Env.Desc");
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
@@ -442,6 +483,14 @@ public partial class MainWindow : Window
LaunchButton.Content = loc.Get("Launch.Launch");
StopButton.Content = loc.Get("Launch.Stop");
AboutSectionTitle.Text = loc.Get("Options.About");
GithubLabel.Text = loc.Get("About.Github.Label");
GithubDesc.Text = loc.Get("About.Github.Desc");
DiscordServerLabel.Text = loc.Get("About.Discord.Label");
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
GithubButton.Content = loc.Get("About.GithubButton");
DiscordButton.Content = loc.Get("About.DiscordButton");
UpdateEmptyStateTexts();
UpdateSelectedGameTexts();
}
@@ -556,9 +605,34 @@ public partial class MainWindow : Window
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
UpdateLogFilePathText();
}
// Environment variables set on this process at the previous launch; children
// inherit the process environment, so stale names must be cleared explicitly.
private readonly HashSet<string> _appliedEnvironmentVariables = new(StringComparer.OrdinalIgnoreCase);
private void SetEnvironmentToggle(string name, bool enabled)
{
if (enabled)
{
if (!_settings.EnvironmentToggles.Contains(name))
{
_settings.EnvironmentToggles.Add(name);
}
}
else
{
_settings.EnvironmentToggles.Remove(name);
}
}
private string SelectedLogLevel()
{
return LogLevelBox.SelectedIndex switch
@@ -1374,6 +1448,24 @@ public partial class MainWindow : Window
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
DimLineBrush);
// Apply the enabled switches to this process; both emulator launch paths
// (CreateProcessW and Process.Start) inherit it. Clear switches turned
// off since the previous launch.
foreach (var staleName in _appliedEnvironmentVariables)
{
if (!_settings.EnvironmentToggles.Contains(staleName))
{
Environment.SetEnvironmentVariable(staleName, null);
}
}
_appliedEnvironmentVariables.Clear();
foreach (var name in _settings.EnvironmentToggles)
{
Environment.SetEnvironmentVariable(name, "1");
_appliedEnvironmentVariables.Add(name);
}
var emulator = new EmulatorProcess();
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
@@ -1471,6 +1563,7 @@ public partial class MainWindow : Window
2 => "Exit.EbootNotFound",
3 => "Exit.RuntimeException",
4 => "Exit.EmulationError",
-1073741819 => "Exit.EmulationError",
_ => "Exit.Unknown",
};
var meaning = Localization.Instance.Get(meaningKey);
@@ -1504,7 +1597,8 @@ public partial class MainWindow : Window
}
var incoming = new List<LogLine>();
while (_pendingLines.TryDequeue(out var pending))
while (incoming.Count < MaxConsoleLinesPerFlush &&
_pendingLines.TryDequeue(out var pending))
{
WriteFileLog(pending.Line);
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
+2
View File
@@ -28,6 +28,8 @@ 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>
<ItemGroup>
Binary file not shown.
+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,
}
@@ -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);
@@ -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,
}
@@ -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);
}
+48
View File
@@ -0,0 +1,48 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host page-allocation primitives used by the native execution engine.
/// Allocate/Reserve/Commit are deliberately separate members (rather than a
/// flags parameter) so every call site maps 1:1 onto the exact native call it
/// replaced, keeping the Windows behavior byte-for-byte identical.
/// </summary>
public interface IHostMemory
{
/// <summary>
/// Reserves and commits pages in one step. <paramref name="desiredAddress"/> of 0
/// lets the OS choose the address. Returns the base address, or 0 on failure.
/// The OS may satisfy the request at a different address than desired; callers
/// that require an exact placement must check the result themselves.
/// </summary>
ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection);
/// <summary>Reserves address space without committing pages (lazy regions).</summary>
ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection);
/// <summary>Commits pages inside a previously reserved range (fault-path lazy commit).</summary>
bool Commit(ulong address, ulong size, HostPageProtection protection);
/// <summary>Releases an entire allocation or reservation by its base address.</summary>
bool Free(ulong address);
/// <summary>
/// Changes protection on committed pages. <paramref name="rawOldProtection"/> is the
/// untranslated previous OS protection value (see <see cref="HostRegionInfo.RawProtection"/>).
/// </summary>
bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection);
/// <summary>
/// Restores a raw protection value previously returned by <see cref="Protect"/> or
/// <see cref="Query"/> on this same platform. Raw values are opaque to callers and
/// must never cross platforms; this exists so save/restore protection sequences
/// round-trip OS-specific modifier bits the neutral enum cannot represent.
/// </summary>
bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection);
bool Query(ulong address, out HostRegionInfo info);
void FlushInstructionCache(ulong address, ulong size);
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Aggregates the host-OS primitives the native execution engine depends on.
/// Each supported platform provides one implementation; consumers reach the
/// process-wide instance through <see cref="HostPlatform.Current"/> or accept
/// one by injection.
/// </summary>
public interface IHostPlatform
{
IHostMemory Memory { get; }
IHostThreading Threading { get; }
IHostSymbolResolver Symbols { get; }
}
@@ -0,0 +1,10 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
public interface IHostSymbolResolver
{
/// <summary>Returns the native address of the function, or 0 if unavailable.</summary>
nint GetAddress(HostRuntimeFunction function);
}
+45
View File
@@ -0,0 +1,45 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Raw host thread and native-TLS primitives for the execution engine. Guest
/// code must run on threads the CLR did not create (no managed frames below
/// guest frames), so thread creation takes a native entry point and is not
/// expressible with managed threads.
/// </summary>
public interface IHostThreading
{
/// <summary>Allocates a native TLS slot; returns <see cref="uint.MaxValue"/> on failure.</summary>
uint AllocateTlsSlot();
bool FreeTlsSlot(uint slot);
bool SetTlsValue(uint slot, nint value);
nint GetTlsValue(uint slot);
uint CurrentThreadId { get; }
bool TrySetCurrentThreadAffinity(nuint affinityMask);
/// <summary>
/// Creates a raw OS thread executing native code at <paramref name="entry"/> with
/// <paramref name="stackReserveBytes"/> of reserved (not committed) stack.
/// Returns the thread handle, or 0 on failure.
/// </summary>
nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId);
/// <summary>Waits for the thread to exit; true when it did within the timeout.</summary>
bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds);
void CloseThreadHandle(nint threadHandle);
/// <summary>
/// Suspends the thread, snapshots its general-purpose registers, and resumes it —
/// one indivisible operation (diagnostics only). The caller must not pass the
/// current thread. Returns false if the thread cannot be opened or suspended.
/// </summary>
bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers);
}
@@ -0,0 +1,153 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
/// <summary>
/// Windows implementation over VirtualAlloc/VirtualFree/VirtualProtect/VirtualQuery.
/// Sealed so the JIT can devirtualize interface calls on fault-handling hot paths.
/// </summary>
internal sealed unsafe partial class WindowsHostMemory : IHostMemory
{
private const uint MEM_COMMIT = 0x1000;
private const uint MEM_RESERVE = 0x2000;
private const uint MEM_RELEASE = 0x8000;
private const uint MEM_FREE = 0x10000;
private const uint PAGE_NOACCESS = 0x01;
private const uint PAGE_READONLY = 0x02;
private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_WRITECOPY = 0x08;
private const uint PAGE_EXECUTE = 0x10;
private const uint PAGE_EXECUTE_READ = 0x20;
private const uint PAGE_EXECUTE_READWRITE = 0x40;
private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
{
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_COMMIT | MEM_RESERVE, ToNativeProtection(protection));
}
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection)
{
return (ulong)VirtualAlloc((void*)desiredAddress, (nuint)size, MEM_RESERVE, ToNativeProtection(protection));
}
public bool Commit(ulong address, ulong size, HostPageProtection protection)
{
return VirtualAlloc((void*)address, (nuint)size, MEM_COMMIT, ToNativeProtection(protection)) != null;
}
public bool Free(ulong address)
{
return VirtualFree((void*)address, 0, MEM_RELEASE);
}
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
return VirtualProtect((void*)address, (nuint)size, ToNativeProtection(protection), out rawOldProtection);
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
return VirtualProtect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
}
public bool Query(ulong address, out HostRegionInfo info)
{
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{
info = default;
return false;
}
info = new HostRegionInfo(
mbi.BaseAddress,
mbi.AllocationBase,
mbi.RegionSize,
ToRegionState(mbi.State),
mbi.State,
ToHostProtection(mbi.Protect),
mbi.Protect,
mbi.AllocationProtect);
return true;
}
public void FlushInstructionCache(ulong address, ulong size)
{
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
}
private static uint ToNativeProtection(HostPageProtection protection) => protection switch
{
HostPageProtection.NoAccess => PAGE_NOACCESS,
HostPageProtection.ReadOnly => PAGE_READONLY,
HostPageProtection.ReadWrite => PAGE_READWRITE,
HostPageProtection.Execute => PAGE_EXECUTE,
HostPageProtection.ReadExecute => PAGE_EXECUTE_READ,
HostPageProtection.ReadWriteExecute => PAGE_EXECUTE_READWRITE,
HostPageProtection.ExecuteWriteCopy => PAGE_EXECUTE_WRITECOPY,
_ => throw new ArgumentOutOfRangeException(nameof(protection), protection, null),
};
private static HostRegionState ToRegionState(uint state) => state switch
{
MEM_COMMIT => HostRegionState.Committed,
MEM_RESERVE => HostRegionState.Reserved,
MEM_FREE => HostRegionState.Free,
_ => HostRegionState.Free,
};
private static HostPageProtection ToHostProtection(uint rawProtection)
{
// Strip PAGE_GUARD/PAGE_NOCACHE/PAGE_WRITECOMBINE modifiers; callers needing
// them compare HostRegionInfo.RawProtection directly.
return (rawProtection & 0xFF) switch
{
PAGE_READONLY => HostPageProtection.ReadOnly,
PAGE_READWRITE => HostPageProtection.ReadWrite,
PAGE_WRITECOPY => HostPageProtection.ReadWrite,
PAGE_EXECUTE => HostPageProtection.Execute,
PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
PAGE_EXECUTE_WRITECOPY => HostPageProtection.ExecuteWriteCopy,
_ => HostPageProtection.NoAccess,
};
}
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[LibraryImport("kernel32.dll")]
private static partial nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[LibraryImport("kernel32.dll")]
private static partial void* GetCurrentProcess();
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
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;
}
}
@@ -0,0 +1,13 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host.Windows;
internal sealed class WindowsHostPlatform : IHostPlatform
{
public IHostMemory Memory { get; } = new WindowsHostMemory();
public IHostThreading Threading { get; } = new WindowsHostThreading();
public IHostSymbolResolver Symbols { get; } = new WindowsHostSymbolResolver();
}
@@ -0,0 +1,39 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
internal sealed partial class WindowsHostSymbolResolver : IHostSymbolResolver
{
public nint GetAddress(HostRuntimeFunction function)
{
var kernel32 = GetModuleHandle("kernel32.dll");
if (kernel32 == 0)
{
return 0;
}
return GetProcAddress(kernel32, function switch
{
HostRuntimeFunction.TlsGetValue => "TlsGetValue",
HostRuntimeFunction.QueryPerformanceCounter => "QueryPerformanceCounter",
HostRuntimeFunction.SwitchToThread => "SwitchToThread",
HostRuntimeFunction.Sleep => "Sleep",
HostRuntimeFunction.WaitForSingleObject => "WaitForSingleObject",
HostRuntimeFunction.SetEvent => "SetEvent",
HostRuntimeFunction.ExitThread => "ExitThread",
_ => throw new ArgumentOutOfRangeException(nameof(function), function, null),
});
}
// Utf16 marshalling pins the managed string and passes its address directly
// (no copy); Utf8 stack-allocates the transient buffer for these short
// ASCII export names. LibraryImport is exact-spelling, hence the W entry point.
[LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)]
private static partial nint GetModuleHandle(string lpModuleName);
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf8)]
private static partial nint GetProcAddress(nint hModule, string procName);
}
@@ -0,0 +1,165 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host.Windows;
internal sealed unsafe partial class WindowsHostThreading : IHostThreading
{
private const uint StackSizeParamIsAReservation = 0x00010000u;
private const uint ThreadGetContext = 0x0008u;
private const uint ThreadSuspendResume = 0x0002u;
// Win64 CONTEXT layout (CONTROL | INTEGER only — no XMM state is requested).
private const int Win64ContextSize = 0x4D0;
private const int Win64ContextFlagsOffset = 0x30;
private const uint ContextAmd64ControlInteger = 0x00100003u;
private const int CtxRax = 120;
private const int CtxRcx = 128;
private const int CtxRdx = 136;
private const int CtxRbx = 144;
private const int CtxRsp = 152;
private const int CtxRbp = 160;
private const int CtxRip = 248;
public uint AllocateTlsSlot() => TlsAlloc();
public bool FreeTlsSlot(uint slot) => TlsFree(slot);
public bool SetTlsValue(uint slot, nint value) => TlsSetValue(slot, value);
public nint GetTlsValue(uint slot) => TlsGetValue(slot);
public uint CurrentThreadId => GetCurrentThreadId();
public bool TrySetCurrentThreadAffinity(nuint affinityMask)
{
return SetThreadAffinityMask(GetCurrentThread(), affinityMask) != 0;
}
public nint CreateNativeThread(nint entry, nint parameter, nuint stackReserveBytes, out uint threadId)
{
return CreateThread(0, stackReserveBytes, entry, parameter, StackSizeParamIsAReservation, out threadId);
}
public bool WaitForThreadExit(nint threadHandle, uint timeoutMilliseconds)
{
return WaitForSingleObject(threadHandle, timeoutMilliseconds) == 0u;
}
public void CloseThreadHandle(nint threadHandle)
{
_ = CloseHandle(threadHandle);
}
public bool TryCaptureThreadRegisters(uint threadId, out HostCapturedRegisters registers)
{
registers = default;
var threadHandle = OpenThread(ThreadGetContext | ThreadSuspendResume, false, threadId);
if (threadHandle == 0)
{
return false;
}
void* contextRecord = null;
var suspended = false;
try
{
if (SuspendThread(threadHandle) == uint.MaxValue)
{
return false;
}
suspended = true;
// CONTEXT requires 16-byte alignment (it embeds M128A fields);
// NativeMemory.AllocZeroed guarantees max_align_t, stackalloc only
// pointer-size — so this stays a native allocation.
contextRecord = NativeMemory.AllocZeroed((nuint)Win64ContextSize);
*(uint*)((byte*)contextRecord + Win64ContextFlagsOffset) = ContextAmd64ControlInteger;
if (!GetThreadContext(threadHandle, contextRecord))
{
return false;
}
registers = new HostCapturedRegisters(
ReadU64(contextRecord, CtxRip),
ReadU64(contextRecord, CtxRsp),
ReadU64(contextRecord, CtxRbp),
ReadU64(contextRecord, CtxRax),
ReadU64(contextRecord, CtxRbx),
ReadU64(contextRecord, CtxRcx),
ReadU64(contextRecord, CtxRdx));
return true;
}
finally
{
if (contextRecord != null)
{
NativeMemory.Free(contextRecord);
}
if (suspended)
{
_ = ResumeThread(threadHandle);
}
_ = CloseHandle(threadHandle);
}
}
private static ulong ReadU64(void* contextRecord, int offset)
{
return *(ulong*)((byte*)contextRecord + offset);
}
[LibraryImport("kernel32.dll")]
private static partial uint TlsAlloc();
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool TlsFree(uint dwTlsIndex);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool TlsSetValue(uint dwTlsIndex, nint lpTlsValue);
[LibraryImport("kernel32.dll")]
private static partial nint TlsGetValue(uint dwTlsIndex);
[LibraryImport("kernel32.dll")]
private static partial uint GetCurrentThreadId();
[LibraryImport("kernel32.dll")]
private static partial nint GetCurrentThread();
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial nuint SetThreadAffinityMask(nint hThread, nuint dwThreadAffinityMask);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial nint CreateThread(
nint lpThreadAttributes,
nuint dwStackSize,
nint lpStartAddress,
nint lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial uint SuspendThread(nint hThread);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial uint ResumeThread(nint hThread);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetThreadContext(nint hThread, void* lpContext);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(nint hObject);
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Lets host-facing libraries (VideoOut, AudioOut) request cooperative guest
/// shutdown without taking a dependency on SharpEmu.Core.
/// </summary>
public static class HostSessionControl
{
private static Action<string>? _shutdownHandler;
public static void SetShutdownHandler(Action<string>? handler)
{
Volatile.Write(ref _shutdownHandler, handler);
}
public static void RequestShutdown(string reason)
{
try
{
Volatile.Read(ref _shutdownHandler)?.Invoke(reason);
}
catch (Exception exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Host shutdown handler failed: {exception.Message}");
}
}
}
+2
View File
@@ -8,4 +8,6 @@ public interface ICpuMemory
bool TryRead(ulong virtualAddress, Span<byte> destination);
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Implemented by memories that decorate another <see cref="ICpuMemory"/>
/// (e.g. access trackers) so capability lookups can unwrap to the real
/// implementation without reflection.
/// </summary>
public interface ICpuMemoryWrapper
{
ICpuMemory Inner { get; }
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Guest address-space manipulation beyond plain allocation: fixed-address
/// mapping and page-protection changes. Guest addresses are identity-mapped
/// onto host pages by the implementing memory, so HLE exports (mmap, mprotect)
/// reach these operations through <c>ctx.Memory</c> instead of calling host
/// APIs directly. Member signatures deliberately mirror the implementation in
/// SharpEmu.Core so existing call sites migrate call-for-call.
/// </summary>
public interface IGuestAddressSpace : IGuestMemoryAllocator
{
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
}
+578 -82
View File
@@ -29,6 +29,7 @@ public static class AgcExports
private const uint ItDispatchDirect = 0x15;
private const uint ItDispatchIndirect = 0x16;
private const uint ItWaitRegMem = 0x3C;
private const uint ItIndirectBuffer = 0x3F;
private const uint ItEventWrite = 0x46;
private const uint ItDmaData = 0x50;
private const uint ItSetContextReg = 0x69;
@@ -120,6 +121,9 @@ public static class AgcExports
private const uint RegisterDefaultsVersion13 = 13;
private const int RegisterDefaultsSize = 0x40;
private const int RegisterDefaultBlockSize = 16 * 8;
private const ulong ResourceRegistrationBytesPerResource = 0x118;
private const ulong ResourceRegistrationBytesPerOwner = 0x1E0;
private const int ResourceRegistrationMaxNameLength = 256;
private const ulong ShaderUserDataOffset = 0x08;
private const ulong ShaderCodeOffset = 0x10;
@@ -154,7 +158,7 @@ public static class AgcExports
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new();
private static readonly Dictionary<(ulong Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
private static readonly Dictionary<
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output),
(ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
private static readonly Dictionary<
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
@@ -388,9 +392,26 @@ public static class AgcExports
public SubmittedDcbState Graphics { get; } = new();
public Dictionary<uint, SubmittedDcbState> ComputeQueues { get; } = new();
public Dictionary<ulong, ComputeImageWriter> ComputeImageWriters { get; } = new();
public Dictionary<uint, string> ResourceOwners { get; } = new();
public Dictionary<uint, RegisteredAgcResource> RegisteredResources { get; } = new();
public bool ResourceRegistrationInitialized { get; set; }
public ulong ResourceRegistrationMemory { get; set; }
public ulong ResourceRegistrationMemorySize { get; set; }
public uint ResourceRegistrationMaxOwners { get; set; }
public uint DefaultOwner { get; set; } = DefaultAgcOwner;
public uint NextOwner { get; set; } = 1;
public uint NextResource { get; set; } = 1;
public ulong WorkSequence { get; set; }
}
private readonly record struct RegisteredAgcResource(
uint Owner,
ulong Address,
ulong Size,
string Name,
uint Type,
uint Flags);
private readonly record struct RegisterDefaultValue(uint Offset, uint Value);
private readonly record struct RegisterDefaultGroup(
@@ -1923,6 +1944,96 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress);
}
// Guest draw-command builders: emit valid (skippable) packets and return a live
// command pointer so the guest's command-buffer build succeeds; full draw processing is TODO.
[SysAbiExport(
Nid = "8N2tmT3jmC8",
ExportName = "sceAgcDcbSetIndexCount",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbSetIndexCount(CpuContext ctx)
{
var dcb = ctx[CpuRegister.Rdi];
var indexCount = (uint)ctx[CpuRegister.Rsi];
if (dcb == 0)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, dcb, 2, out var cmd) ||
!ctx.TryWriteUInt32(cmd, Pm4(2, ItNop, RZero)) ||
!ctx.TryWriteUInt32(cmd + 4, indexCount))
{
return ReturnPointer(ctx, 0);
}
return ReturnPointer(ctx, cmd);
}
[SysAbiExport(
Nid = "xSAR0LTcRKM",
ExportName = "sceAgcDcbJump",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbJump(CpuContext ctx)
{
var dcb = ctx[CpuRegister.Rdi];
var target = ctx[CpuRegister.Rsi];
var sizeDwords = (uint)ctx[CpuRegister.Rdx];
if (dcb == 0)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, dcb, 4, out var cmd) ||
!ctx.TryWriteUInt32(cmd, Pm4(4, ItIndirectBuffer, RZero)) ||
!ctx.TryWriteUInt32(cmd + 4, (uint)(target & 0xFFFF_FFFFUL)) ||
!ctx.TryWriteUInt32(cmd + 8, (uint)((target >> 32) & 0xFFFFUL)) ||
!ctx.TryWriteUInt32(cmd + 12, sizeDwords & 0xFFFFF))
{
return ReturnPointer(ctx, 0);
}
return ReturnPointer(ctx, cmd);
}
[SysAbiExport(
Nid = "bbFueFP+J4k",
ExportName = "sceAgcDcbSetPredication",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DcbSetPredication(CpuContext ctx)
{
var dcb = ctx[CpuRegister.Rdi];
var address = ctx[CpuRegister.Rsi];
if (dcb == 0)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, dcb, 3, out var cmd) ||
!ctx.TryWriteUInt32(cmd, Pm4(3, ItNop, RZero)) ||
!ctx.TryWriteUInt32(cmd + 4, (uint)(address & 0xFFFF_FFFFUL)) ||
!ctx.TryWriteUInt32(cmd + 8, (uint)(address >> 32)))
{
return ReturnPointer(ctx, 0);
}
return ReturnPointer(ctx, cmd);
}
[SysAbiExport(
Nid = "w6Dj1VJt5qY",
ExportName = "sceAgcSetPacketPredication",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int SetPacketPredication(CpuContext ctx)
{
// Global predication toggle on a packet; a no-op is safe for rendering.
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "w2rJhmD+dsE",
ExportName = "sceAgcDriverAddEqEvent",
@@ -1999,8 +2110,16 @@ public static class AgcExports
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (gpuState.Gate)
{
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
try
{
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
finally
{
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
}
}
ctx[CpuRegister.Rax] = 0;
@@ -2030,27 +2149,35 @@ public static class AgcExports
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (gpuState.Gate)
{
for (uint i = 0; i < bufferCount; i++)
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
try
{
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
commandAddress == 0 ||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
dwordCount == 0)
for (uint i = 0; i < bufferCount; i++)
{
continue;
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
commandAddress == 0 ||
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
dwordCount == 0)
{
continue;
}
if (tracePackets)
{
TraceAgc(
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
}
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
}
if (tracePackets)
{
TraceAgc(
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
}
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
finally
{
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
}
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
ctx[CpuRegister.Rax] = 0;
@@ -2098,8 +2225,16 @@ public static class AgcExports
gpuState.ComputeQueues.Add(ownerHandle, queueState);
}
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
Gen5ShaderScalarEvaluator.BeginGlobalMemoryReadScope();
try
{
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
DrainResumableDcbs(ctx, gpuState, tracePackets);
}
finally
{
Gen5ShaderScalarEvaluator.EndGlobalMemoryReadScope();
}
}
ctx[CpuRegister.Rax] = 0;
@@ -2120,12 +2255,87 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryWriteUInt32(outAddress, 256))
if (!ctx.TryWriteUInt32(outAddress, ResourceRegistrationMaxNameLength))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc($"agc.driver_get_resource_registration_max_name_length out=0x{outAddress:X16} value=256");
TraceAgc(
$"agc.driver_get_resource_registration_max_name_length " +
$"out=0x{outAddress:X16} value={ResourceRegistrationMaxNameLength}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "AOLcoIkQDgM",
ExportName = "sceAgcDriverQueryResourceRegistrationUserMemoryRequirements",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverQueryResourceRegistrationUserMemoryRequirements(CpuContext ctx)
{
var sizeAddress = ctx[CpuRegister.Rdi];
var resourceCount = ctx[CpuRegister.Rsi];
var ownerCount = ctx[CpuRegister.Rdx];
if (sizeAddress == 0 || resourceCount == 0 || ownerCount == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ulong requiredSize;
try
{
requiredSize = checked(
resourceCount * ResourceRegistrationBytesPerResource +
ownerCount * ResourceRegistrationBytesPerOwner);
}
catch (OverflowException)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryWriteUInt64(sizeAddress, requiredSize))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_query_resource_registration_memory resources={resourceCount} " +
$"owners={ownerCount} bytes=0x{requiredSize:X}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "F0Y42t-3e18",
ExportName = "sceAgcDriverInitResourceRegistration",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverInitResourceRegistration(CpuContext ctx)
{
var memoryAddress = ctx[CpuRegister.Rdi];
var memorySize = ctx[CpuRegister.Rsi];
var ownerCount = ctx[CpuRegister.Rdx];
if (memoryAddress == 0 || memorySize == 0 || ownerCount == 0 || ownerCount > uint.MaxValue)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
state.ResourceRegistrationInitialized = true;
state.ResourceRegistrationMemory = memoryAddress;
state.ResourceRegistrationMemorySize = memorySize;
state.ResourceRegistrationMaxOwners = (uint)ownerCount;
state.ResourceOwners.Clear();
state.RegisteredResources.Clear();
state.DefaultOwner = DefaultAgcOwner;
state.NextOwner = 1;
state.NextResource = 1;
}
TraceAgc(
$"agc.driver_init_resource_registration memory=0x{memoryAddress:X16} " +
$"bytes=0x{memorySize:X} owners={ownerCount}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -2144,12 +2354,97 @@ public static class AgcExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryWriteUInt32(ownerAddress, DefaultAgcOwner))
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint owner;
lock (state.Gate)
{
owner = state.DefaultOwner;
}
if (!ctx.TryWriteUInt32(ownerAddress, owner))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={DefaultAgcOwner}");
TraceAgc($"agc.driver_get_default_owner out=0x{ownerAddress:X16} owner={owner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "U9ueyEhSkF4",
ExportName = "sceAgcDriverRegisterDefaultOwner",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverRegisterDefaultOwner(CpuContext ctx)
{
var owner = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
state.DefaultOwner = owner;
}
TraceAgc($"agc.driver_register_default_owner owner={owner}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "X-Nm5KLREeg",
ExportName = "sceAgcDriverRegisterOwner",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverRegisterOwner(CpuContext ctx)
{
var ownerAddress = ctx[CpuRegister.Rdi];
var nameAddress = ctx[CpuRegister.Rsi];
if (ownerAddress == 0 || nameAddress == 0 ||
!TryReadGuestCString(
ctx,
nameAddress,
ResourceRegistrationMaxNameLength,
out var nameBytes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint owner;
lock (state.Gate)
{
if (!state.ResourceRegistrationInitialized ||
state.ResourceRegistrationMaxOwners != 0 &&
state.ResourceOwners.Count >= state.ResourceRegistrationMaxOwners)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
owner = state.NextOwner;
while (owner == state.DefaultOwner || state.ResourceOwners.ContainsKey(owner))
{
owner++;
if (owner == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
}
state.NextOwner = owner + 1;
state.ResourceOwners.Add(owner, System.Text.Encoding.UTF8.GetString(nameBytes));
}
if (!ctx.TryWriteUInt32(ownerAddress, owner))
{
lock (state.Gate)
{
state.ResourceOwners.Remove(owner);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_register_owner out=0x{ownerAddress:X16} owner={owner} " +
$"name={System.Text.Encoding.UTF8.GetString(nameBytes)}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -2160,19 +2455,90 @@ public static class AgcExports
LibraryName = "libSceAgc")]
public static int DriverRegisterResource(CpuContext ctx)
{
var resourceAddress = ctx[CpuRegister.Rdi];
var resourceHandleAddress = ctx[CpuRegister.Rdi];
var owner = (uint)ctx[CpuRegister.Rsi];
var nameAddress = ctx[CpuRegister.Rdx];
var type = (uint)ctx[CpuRegister.R8];
var flags = (uint)ctx[CpuRegister.R9];
var resourceAddress = ctx[CpuRegister.Rdx];
var resourceSize = ctx[CpuRegister.Rcx];
var nameAddress = ctx[CpuRegister.R8];
var type = (uint)ctx[CpuRegister.R9];
if (resourceHandleAddress == 0 || resourceAddress == 0 || resourceSize == 0 ||
!ctx.TryReadUInt32(ctx[CpuRegister.Rsp] + sizeof(ulong), out var flags) ||
!TryReadGuestCString(
ctx,
nameAddress,
ResourceRegistrationMaxNameLength,
out var nameBytes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
uint resourceHandle;
lock (state.Gate)
{
if (!state.ResourceRegistrationInitialized ||
owner != state.DefaultOwner &&
!state.ResourceOwners.ContainsKey(owner))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
resourceHandle = state.NextResource++;
if (resourceHandle == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
state.RegisteredResources.Add(
resourceHandle,
new RegisteredAgcResource(
owner,
resourceAddress,
resourceSize,
System.Text.Encoding.UTF8.GetString(nameBytes),
type,
flags));
}
if (!ctx.TryWriteUInt32(resourceHandleAddress, resourceHandle))
{
lock (state.Gate)
{
state.RegisteredResources.Remove(resourceHandle);
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc(
$"agc.driver_register_resource resource=0x{resourceAddress:X16} owner={owner} " +
$"name=0x{nameAddress:X16} type={type} flags={flags}");
$"agc.driver_register_resource handle={resourceHandle} owner={owner} " +
$"resource=0x{resourceAddress:X16} bytes=0x{resourceSize:X} " +
$"name={System.Text.Encoding.UTF8.GetString(nameBytes)} type={type} flags={flags}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "pWLG7WOpVcw",
ExportName = "sceAgcDriverUnregisterResource",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int DriverUnregisterResource(CpuContext ctx)
{
var resourceHandle = (uint)ctx[CpuRegister.Rdi];
var state = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (state.Gate)
{
if (!state.RegisteredResources.Remove(resourceHandle))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
}
TraceAgc($"agc.driver_unregister_resource handle={resourceHandle}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "-KRzWekV120",
ExportName = "sceAgcDriverUnknown_KRzWekV120",
@@ -3054,12 +3420,13 @@ public static class AgcExports
textures,
globalMemoryBuffers,
translatedDraw.AttributeCount,
new VulkanGuestRenderTarget(
firstTarget.Address,
firstTarget.Width,
firstTarget.Height,
firstTarget.Format,
firstTarget.NumberType),
translatedDraw.RenderTargets.Select(target =>
new VulkanGuestRenderTarget(
target.Address,
target.Width,
target.Height,
target.Format,
target.NumberType)).ToArray(),
translatedDraw.VertexSpirv,
translatedDraw.VertexCount,
translatedDraw.InstanceCount,
@@ -3167,7 +3534,8 @@ public static class AgcExports
exportState,
out var exportEvaluation,
out error,
resolveVertexInputs: true) ||
resolveVertexInputs: true,
vertexRecordLimit: indexed ? null : vertexCount) ||
!Gen5ShaderTranslator.TryCreateState(
ctx,
pixelShaderAddress,
@@ -3186,19 +3554,47 @@ public static class AgcExports
}
var renderTargets = GetRenderTargets(state.CxRegisters)
.Where(target =>
target.Slot == 0 &&
HasPixelColorExport(pixelState, target.Slot))
.Where(target => HasPixelColorExport(pixelState, target.Slot))
.OrderBy(target => target.Slot)
.ToArray();
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType);
var exportStateFingerprint = ComputeShaderStateFingerprint(exportEvaluation);
var pixelStateFingerprint = ComputeShaderStateFingerprint(pixelEvaluation);
var renderTargetFormats = new VulkanRenderTargetFormat[renderTargets.Length];
for (var index = 0; index < renderTargets.Length; index++)
{
var target = renderTargets[index];
if (!VulkanVideoPresenter.TryDecodeRenderTargetFormat(
target.Format,
target.NumberType,
out renderTargetFormats[index]))
{
error =
$"unsupported color target format={target.Format} number_type={target.NumberType}";
return false;
}
}
var pixelOutputs = renderTargets
.Select((target, location) => new Gen5PixelOutputBinding(
target.Slot,
(uint)location,
renderTargetFormats[location].OutputKind))
.ToArray();
var outputLayout = string.Join(
';',
pixelOutputs.Select(output =>
$"{output.GuestSlot}:{output.HostLocation}:{(int)output.Kind}"));
var attributeCount = GetInterpolatedAttributeCount(pixelState);
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
var shaderKey = (
exportShaderAddress,
exportStateFingerprint,
pixelShaderAddress,
pixelStateFingerprint,
outputKind);
outputLayout,
attributeCount);
var totalGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count +
exportEvaluation.GlobalMemoryBindings.Count;
(byte[] Vertex, byte[] Pixel) compiled;
lock (_submitTraceGate)
{
@@ -3207,26 +3603,25 @@ public static class AgcExports
if (compiled.Vertex is null || compiled.Pixel is null)
{
var totalGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count +
exportEvaluation.GlobalMemoryBindings.Count;
if (!Gen5SpirvTranslator.TryCompilePixelShader(
pixelState,
pixelEvaluation,
outputKind,
pixelOutputs,
out var pixelShader,
out error,
globalBufferBase: 0,
totalGlobalBufferCount: totalGlobalBuffers,
imageBindingBase: 0) ||
totalGlobalBufferCount: totalGlobalBuffers + 2,
imageBindingBase: 0,
scalarRegisterBufferIndex: totalGlobalBuffers) ||
!Gen5SpirvTranslator.TryCompileVertexShader(
exportState,
exportEvaluation,
out var vertexShader,
out error,
globalBufferBase: pixelEvaluation.GlobalMemoryBindings.Count,
totalGlobalBufferCount: totalGlobalBuffers,
imageBindingBase: pixelEvaluation.ImageBindings.Count))
totalGlobalBufferCount: totalGlobalBuffers + 2,
imageBindingBase: pixelEvaluation.ImageBindings.Count,
scalarRegisterBufferIndex: totalGlobalBuffers + 1))
{
return false;
}
@@ -3278,6 +3673,8 @@ public static class AgcExports
var globalMemoryBindings = pixelEvaluation.GlobalMemoryBindings
.Concat(exportEvaluation.GlobalMemoryBindings)
.Append(CreateScalarRegisterBinding(pixelEvaluation))
.Append(CreateScalarRegisterBinding(exportEvaluation))
.ToArray();
IReadOnlyList<Gen5VertexInputBinding> vertexInputs =
exportEvaluation.VertexInputs ?? [];
@@ -3288,7 +3685,7 @@ public static class AgcExports
primitiveType,
compiled.Vertex,
compiled.Pixel,
GetInterpolatedAttributeCount(pixelState),
attributeCount,
vertexCount,
state.InstanceCount,
indexed ? CreateVulkanIndexBuffer(ctx, state, vertexCount) : null,
@@ -3297,7 +3694,7 @@ public static class AgcExports
vertexInputs,
renderTargets,
ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
CreateRenderState(state.CxRegisters, renderTargets, pixelState),
textures,
vertexInputs,
pixelEvaluation.InitialScalarRegisters));
@@ -3313,7 +3710,8 @@ public static class AgcExports
/// Chowdren resets its effect layers with an untextured transparent-black
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
/// Treat precisely that draw shape as an overwrite.
/// Treat precisely that draw shape as an overwrite only when every MRT
/// attachment uses the same premultiplied blend pattern.
/// </summary>
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
VulkanGuestRenderState renderState,
@@ -3325,13 +3723,7 @@ public static class AgcExports
textures.Count != 0 ||
vertexInputs.Count != 0 ||
pixelUserData.Count < 4 ||
renderState.Blend is not
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
})
!renderState.Blends.All(IsTransparentPremultipliedFillBlend))
{
return renderState;
}
@@ -3346,10 +3738,21 @@ public static class AgcExports
return renderState with
{
Blend = renderState.Blend with { Enable = false },
Blends = renderState.Blends
.Select(blend => blend with { Enable = false })
.ToArray(),
};
}
private static bool IsTransparentPremultipliedFillBlend(VulkanGuestBlendState blend) =>
blend is
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
};
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
CpuContext ctx,
SubmittedDcbState state,
@@ -3372,19 +3775,15 @@ public static class AgcExports
: null;
}
private static Gen5PixelOutputKind GetPixelOutputKind(uint numberType) =>
numberType switch
{
4 => Gen5PixelOutputKind.Uint,
5 => Gen5PixelOutputKind.Sint,
_ => Gen5PixelOutputKind.Float,
};
private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
state.Program.Instructions.Any(instruction =>
instruction.Control is Gen5ExportControl export &&
export.Target == target &&
export.EnableMask != 0);
GetPixelColorExportMask(state, target) != 0;
private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
state.Program.Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5ExportControl>()
.Where(export => export.Target == target)
.Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu));
private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
{
@@ -3400,6 +3799,88 @@ public static class AgcExports
return (uint)(maxAttribute + 1);
}
private const int ShaderScalarRegisterCount = 256;
private static Gen5GlobalMemoryBinding CreateScalarRegisterBinding(
Gen5ShaderEvaluation evaluation)
{
var data = new byte[ShaderScalarRegisterCount * sizeof(uint)];
var registers = evaluation.InitialScalarRegisters;
var count = Math.Min(registers.Count, ShaderScalarRegisterCount);
for (var index = 0; index < count; index++)
{
var value = registers[index];
var offset = index * sizeof(uint);
data[offset] = (byte)value;
data[offset + 1] = (byte)(value >> 8);
data[offset + 2] = (byte)(value >> 16);
data[offset + 3] = (byte)(value >> 24);
}
return new Gen5GlobalMemoryBinding(0, 0, [], data);
}
private static ulong ComputeShaderStructureFingerprint(Gen5ShaderEvaluation evaluation)
{
const ulong offsetBasis = 14695981039346656037UL;
const ulong prime = 1099511628211UL;
var hash = offsetBasis;
void Mix(ulong value) => hash = (hash ^ value) * prime;
Mix((ulong)evaluation.GlobalMemoryBindings.Count);
foreach (var binding in evaluation.GlobalMemoryBindings)
{
Mix(binding.ScalarAddress);
Mix((ulong)binding.InstructionPcs.Count);
foreach (var pc in binding.InstructionPcs)
{
Mix(pc);
}
}
Mix((ulong)evaluation.ImageBindings.Count);
foreach (var image in evaluation.ImageBindings)
{
Mix(image.Pc);
Mix((ulong)(uint)image.Opcode.GetHashCode());
foreach (var word in image.ResourceDescriptor)
{
Mix(word);
}
foreach (var word in image.SamplerDescriptor)
{
Mix(word);
}
Mix(image.MipLevel ?? uint.MaxValue);
}
if (evaluation.VertexInputs is { } vertexInputs)
{
Mix((ulong)vertexInputs.Count);
foreach (var input in vertexInputs)
{
Mix(input.Pc);
Mix(input.Location);
Mix(input.ComponentCount);
Mix(input.DataFormat);
Mix(input.NumberFormat);
Mix(input.Stride);
}
}
if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters)
{
Mix(computeSystemRegisters.WorkGroupXRegister ?? uint.MaxValue);
Mix(computeSystemRegisters.WorkGroupYRegister ?? uint.MaxValue);
Mix(computeSystemRegisters.WorkGroupZRegister ?? uint.MaxValue);
Mix(computeSystemRegisters.ThreadGroupSizeRegister ?? uint.MaxValue);
}
return hash;
}
private static ulong ComputeShaderStateFingerprint(Gen5ShaderEvaluation evaluation)
{
const ulong offsetBasis = 14695981039346656037UL;
@@ -3460,11 +3941,25 @@ public static class AgcExports
private static VulkanGuestRenderState CreateRenderState(
IReadOnlyDictionary<uint, uint> registers,
RenderTargetDescriptor target)
IReadOnlyList<RenderTargetDescriptor> targets,
Gen5ShaderState pixelState)
{
if (targets.Count == 0)
{
return VulkanGuestRenderState.Default;
}
var target = targets[0];
var scissor = DecodeScissor(registers, target.Width, target.Height);
return new VulkanGuestRenderState(
DecodeBlendState(registers, target.Slot),
targets.Select(target =>
{
var blend = DecodeBlendState(registers, target.Slot);
return blend with
{
WriteMask = blend.WriteMask & GetPixelColorExportMask(pixelState, target.Slot),
};
}).ToArray(),
scissor,
DecodeViewport(registers, target.Width, target.Height, scissor));
}
@@ -3489,7 +3984,7 @@ public static class AgcExports
(control >> 24) & 0x1Fu,
(control >> 21) & 0x7u,
((control >> 29) & 1u) != 0,
writeMask == 0 ? 0xFu : writeMask);
writeMask);
}
private static VulkanGuestRect? DecodeScissor(
@@ -4250,7 +4745,7 @@ public static class AgcExports
var localSizeZ = GetComputeLocalSize(state.ShRegisters, ComputeNumThreadZ);
var gpuDispatch = false;
var computeError = string.Empty;
if (hasStorageBinding &&
if ((hasStorageBinding || evaluation.GlobalMemoryBindings.Count != 0) &&
(ulong)localSizeX * localSizeY * localSizeZ <= 1024)
{
var shaderKey = (
@@ -4320,7 +4815,8 @@ public static class AgcExports
$"groups={dispatch.GroupCountX}x{dispatch.GroupCountY}x{dispatch.GroupCountZ} " +
$"local={localSizeX}x{localSizeY}x{localSizeZ} " +
$"sys={DescribeComputeSystemRegisters(computeSystemRegisters)} " +
$"gpu={gpuDispatch} blits={blitCount}" +
$"gpu={gpuDispatch} blits={blitCount} " +
$"globals={evaluation.GlobalMemoryBindings.Count}" +
(computeError.Length == 0 ? string.Empty : $" error={computeError}") +
$" bindings=[{string.Join(',', descriptions)}]");
}
@@ -4826,7 +5322,7 @@ public static class AgcExports
if (Gen5SpirvTranslator.TryCompilePixelShader(
pixelState,
evaluation,
Gen5PixelOutputKind.Float,
[new(0, 0, Gen5PixelOutputKind.Float)],
out var compiledPixel,
out var compileError))
{
+5
View File
@@ -49,6 +49,11 @@ internal enum Gen5PixelOutputKind
Sint,
}
internal readonly record struct Gen5PixelOutputBinding(
uint GuestSlot,
uint HostLocation,
Gen5PixelOutputKind Kind);
internal enum Gen5SpirvStage
{
Vertex,
@@ -13,6 +13,24 @@ internal static class Gen5ShaderScalarEvaluator
private const int ImageDescriptorDwords = 8;
private const int SamplerDescriptorDwords = 4;
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
private static readonly int DefaultGlobalMemoryBindingBytes =
int.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_GLOBAL_BINDING_BYTES"),
out var configured) && configured >= sizeof(uint)
? Math.Min(configured, MaxGlobalMemoryBindingBytes)
: 1 * 1024 * 1024;
internal static long GlobalMemoryReadCount;
internal static long GlobalMemoryReadBytes;
internal static long GlobalMemoryReadCacheHits;
internal static long GlobalMemoryReadPvmBytes;
internal static long GlobalMemoryReadLibcBytes;
internal static long GlobalMemoryReadReuses;
private const long CrossFrameReadCacheMaxBytes = 1024L * 1024 * 1024;
private static readonly object _crossFrameReadGate = new();
private static readonly Dictionary<(ulong BaseAddress, int SizeBytes), byte[]> _crossFrameReadCache = new();
private static long _crossFrameReadCacheBytes;
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
private readonly record struct BufferDescriptor(
@@ -44,7 +62,8 @@ internal static class Gen5ShaderScalarEvaluator
Gen5ShaderState state,
out Gen5ShaderEvaluation evaluation,
out string error,
bool resolveVertexInputs = false)
bool resolveVertexInputs = false,
uint? vertexRecordLimit = null)
{
evaluation = default!;
error = string.Empty;
@@ -255,10 +274,28 @@ internal static class Gen5ShaderScalarEvaluator
if (resolveVertexInputs &&
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
{
var vertexReadSize = bufferDescriptor.SizeBytes;
if (vertexRecordLimit is { } recordLimit &&
instruction.Sources.Count > 2 &&
TryEvaluateScalarOperand(
instruction.Sources[2],
scalarRegisters,
out var scalarOffset))
{
var bindingOffset = unchecked((uint)bufferMemory.OffsetBytes + scalarOffset);
var requiredBytes =
(ulong)bindingOffset +
(ulong)(Math.Max(recordLimit, 1u) - 1u) * bufferDescriptor.Stride +
(ulong)bufferMemory.DwordCount * sizeof(uint);
vertexReadSize = Math.Min(
bufferDescriptor.SizeBytes,
Math.Max(requiredBytes, sizeof(uint)));
}
if (!TryReadGlobalMemory(
ctx,
bufferDescriptor.BaseAddress,
bufferDescriptor.SizeBytes,
vertexReadSize,
out var vertexData))
{
error =
@@ -533,22 +570,29 @@ internal static class Gen5ShaderScalarEvaluator
return false;
}
[ThreadStatic]
private static Dictionary<(ulong BaseAddress, int SizeBytes), byte[]>? _globalMemoryReadCache;
internal static void BeginGlobalMemoryReadScope()
{
_globalMemoryReadCache = new Dictionary<(ulong, int), byte[]>();
}
internal static void EndGlobalMemoryReadScope()
{
_globalMemoryReadCache = null;
}
private static bool TryReadGlobalMemory(
CpuContext ctx,
ulong baseAddress,
out byte[] data)
{
for (var size = MaxGlobalMemoryBindingBytes; size >= 4096; size >>= 1)
{
data = GC.AllocateUninitializedArray<byte>(size);
if (ctx.Memory.TryRead(baseAddress, data))
{
return true;
}
}
data = [];
return false;
return TryReadGlobalMemory(
ctx,
baseAddress,
(ulong)DefaultGlobalMemoryBindingBytes,
out data);
}
private static bool TryReadGlobalMemory(
@@ -570,13 +614,74 @@ internal static class Gen5ShaderScalarEvaluator
return false;
}
var cache = _globalMemoryReadCache;
var cacheKey = (baseAddress, (int)cappedSize);
if (cache is not null && cache.TryGetValue(cacheKey, out var cached))
{
Interlocked.Increment(ref GlobalMemoryReadCacheHits);
data = cached;
return true;
}
byte[]? previous;
lock (_crossFrameReadGate)
{
_crossFrameReadCache.TryGetValue(cacheKey, out previous);
}
if (previous is not null && ctx.Memory.TryCompare(baseAddress, previous))
{
Interlocked.Increment(ref GlobalMemoryReadReuses);
if (cache is not null)
{
cache[cacheKey] = previous;
}
data = previous;
return true;
}
var candidateSize = (int)cappedSize;
while (candidateSize >= sizeof(uint))
{
data = GC.AllocateUninitializedArray<byte>(candidateSize);
if (ctx.Memory.TryRead(baseAddress, data) ||
var readFromPvm = ctx.Memory.TryRead(baseAddress, data);
if (readFromPvm ||
KernelMemoryCompatExports.TryReadTrackedLibcHeap(baseAddress, data))
{
Interlocked.Increment(ref GlobalMemoryReadCount);
Interlocked.Add(ref GlobalMemoryReadBytes, data.Length);
if (readFromPvm)
{
Interlocked.Add(ref GlobalMemoryReadPvmBytes, data.Length);
}
else
{
Interlocked.Add(ref GlobalMemoryReadLibcBytes, data.Length);
}
if (cache is not null)
{
cache[cacheKey] = data;
}
lock (_crossFrameReadGate)
{
if (_crossFrameReadCache.TryGetValue(cacheKey, out var replaced))
{
_crossFrameReadCacheBytes -= replaced.Length;
}
if (_crossFrameReadCacheBytes + data.Length > CrossFrameReadCacheMaxBytes)
{
_crossFrameReadCache.Clear();
_crossFrameReadCacheBytes = 0;
}
_crossFrameReadCache[cacheKey] = data;
_crossFrameReadCacheBytes += data.Length;
}
return true;
}
@@ -802,8 +802,15 @@ internal static class Gen5ShaderTranslator
0x14 => "VCmpxGtF32",
0x15 => "VCmpxLgF32",
0x16 => "VCmpxGeF32",
0x17 => "VCmpxOF32",
0x18 => "VCmpxUF32",
0x19 => "VCmpxNgeF32",
0x1A => "VCmpxNlgF32",
0x1B => "VCmpxNgtF32",
0x1C => "VCmpxNleF32",
0x1D => "VCmpxNeqF32",
0x1E => "VCmpxNltF32",
0x1F => "VCmpxTruF32",
0x80 => "VCmpFI32",
0x81 => "VCmpLtI32",
0x82 => "VCmpEqI32",
@@ -396,6 +396,14 @@ internal static partial class Gen5SpirvTranslator
_module.Constant64(_ulongType, 32)));
break;
}
case "VBcntU32B32":
result = IAdd(
_module.AddInstruction(
SpirvOp.BitCount,
_uintType,
GetRawSource(instruction, 0)),
GetRawSource(instruction, 1));
break;
case "VMadU32U24":
{
var left = BitwiseAnd(
@@ -593,6 +601,18 @@ internal static partial class Gen5SpirvTranslator
Ext(38, _uintType, high, right));
break;
}
case "VSadU32":
{
var left = GetRawSource(instruction, 0);
var right = GetRawSource(instruction, 1);
var difference = _module.AddInstruction(
SpirvOp.ISub,
_uintType,
Ext(41, _uintType, left, right),
Ext(38, _uintType, left, right));
result = IAdd(difference, GetRawSource(instruction, 2));
break;
}
case "VMed3I32":
{
var left = Bitcast(_intType, GetRawSource(instruction, 0));
@@ -702,6 +722,14 @@ internal static partial class Gen5SpirvTranslator
result = Ext(58, _uintType, vector);
break;
}
case "VCvtPkU16U32":
case "VCvtPkI16I32":
result = BitwiseOr(
BitwiseAnd(GetRawSource(instruction, 0), UInt(0xFFFF)),
ShiftLeftLogical(
BitwiseAnd(GetRawSource(instruction, 1), UInt(0xFFFF)),
UInt(16)));
break;
default:
error = $"unsupported vector opcode {instruction.Opcode}";
return false;
@@ -833,6 +861,25 @@ internal static partial class Gen5SpirvTranslator
{
condition = _module.ConstantBool(true);
}
else if (opcode is "VCmpOF32" or "VCmpxOF32" or "VCmpUF32" or "VCmpxUF32")
{
// The ordered/unordered predicates only test whether either
// operand is NaN. SPIR-V's OpOrdered/OpUnordered are Kernel-only,
// so build the same result from OpIsNan, which needs no extra
// capability: unordered = isnan(a) || isnan(b), ordered = !that.
var left = GetFloatSource(instruction, 0);
var right = GetFloatSource(instruction, 1);
var nanLeft = _module.AddInstruction(SpirvOp.IsNan, _boolType, left);
var nanRight = _module.AddInstruction(SpirvOp.IsNan, _boolType, right);
var unordered = _module.AddInstruction(
SpirvOp.LogicalOr,
_boolType,
nanLeft,
nanRight);
condition = opcode is "VCmpUF32" or "VCmpxUF32"
? unordered
: _module.AddInstruction(SpirvOp.LogicalNot, _boolType, unordered);
}
else if (opcode is not ("VCmpClassF32" or "VCmpxClassF32") &&
opcode.EndsWith("F32", StringComparison.Ordinal))
{
@@ -847,6 +894,7 @@ internal static partial class Gen5SpirvTranslator
"VCmpLgF32" or "VCmpxLgF32" => SpirvOp.FOrdNotEqual,
"VCmpGeF32" or "VCmpxGeF32" => SpirvOp.FOrdGreaterThanEqual,
"VCmpNeqF32" or "VCmpxNeqF32" => SpirvOp.FUnordNotEqual,
"VCmpNlgF32" or "VCmpxNlgF32" => SpirvOp.FUnordEqual,
"VCmpNltF32" or "VCmpxNltF32" => SpirvOp.FUnordGreaterThanEqual,
"VCmpNleF32" or "VCmpxNleF32" => SpirvOp.FUnordGreaterThan,
"VCmpNgtF32" or "VCmpxNgtF32" => SpirvOp.FUnordLessThanEqual,
@@ -897,7 +945,8 @@ internal static partial class Gen5SpirvTranslator
condition = _module.AddInstruction(operation, _boolType, left, right);
}
StoreWaveMask(106, condition);
// On gfx10, VCmpx writes EXEC only and preserves VCC; the sdst
// operand was removed from the cmpx encodings on this generation.
if (opcode.StartsWith("VCmpx", StringComparison.Ordinal))
{
var active = _module.AddInstruction(
@@ -907,6 +956,10 @@ internal static partial class Gen5SpirvTranslator
condition);
StoreWaveMask(126, active);
}
else
{
StoreWaveMask(106, condition);
}
return true;
}
@@ -1303,6 +1356,43 @@ internal static partial class Gen5SpirvTranslator
Store(_scc, IsNotZero(result));
break;
}
case "SAbsdiffI32":
{
var wideLeft = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, left));
var wideRight = _module.AddInstruction(
SpirvOp.SConvert,
_longType,
Bitcast(_intType, right));
var difference = _module.AddInstruction(
SpirvOp.ISub,
_longType,
wideLeft,
wideRight);
result = _module.AddInstruction(
SpirvOp.UConvert,
_uintType,
Ext(5, _longType, difference));
Store(_scc, IsNotZero(result));
break;
}
case "SPackLlB32B16":
result = BitwiseOr(
BitwiseAnd(left, UInt(0xFFFF)),
ShiftLeftLogical(right, UInt(16)));
break;
case "SPackLhB32B16":
result = BitwiseOr(
BitwiseAnd(left, UInt(0xFFFF)),
BitwiseAnd(right, UInt(0xFFFF0000)));
break;
case "SPackHhB32B16":
result = BitwiseOr(
ShiftRightLogical(left, UInt(16)),
BitwiseAnd(right, UInt(0xFFFF0000)));
break;
case "SCselectB32":
result = _module.AddInstruction(
SpirvOp.Select,
+129 -46
View File
@@ -18,19 +18,67 @@ internal static partial class Gen5SpirvTranslator
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0)
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1) =>
TryCompilePixelShader(
state,
evaluation,
[new Gen5PixelOutputBinding(0, 0, outputKind)],
out shader,
out error,
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase,
scalarRegisterBufferIndex);
public static bool TryCompilePixelShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
IReadOnlyList<Gen5PixelOutputBinding> outputs,
out Gen5SpirvShader shader,
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1)
{
if (outputs.Count > 8 || outputs.Any(output => output.GuestSlot > 7))
{
shader = default!;
error = "pixel outputs must contain at most eight guest slots in the 0..7 range";
return false;
}
if (outputs.Select(output => output.GuestSlot).Distinct().Count() != outputs.Count ||
outputs.Select(output => output.HostLocation).Distinct().Count() != outputs.Count)
{
shader = default!;
error = "pixel output guest slots and host locations must be unique";
return false;
}
if (!outputs
.OrderBy(output => output.HostLocation)
.Select((output, index) => output.HostLocation == (uint)index)
.All(isDense => isDense))
{
shader = default!;
error = "pixel output host locations must be dense in the 0..N-1 range";
return false;
}
var context = new CompilationContext(
Gen5SpirvStage.Pixel,
state,
evaluation,
outputKind,
outputs,
1,
1,
1,
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase);
imageBindingBase,
scalarRegisterBufferIndex);
return context.TryCompile(out shader, out error);
}
@@ -41,19 +89,21 @@ internal static partial class Gen5SpirvTranslator
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0)
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1)
{
var context = new CompilationContext(
Gen5SpirvStage.Vertex,
state,
evaluation,
Gen5PixelOutputKind.Float,
[],
1,
1,
1,
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase);
imageBindingBase,
scalarRegisterBufferIndex);
return context.TryCompile(out shader, out error);
}
@@ -70,13 +120,14 @@ internal static partial class Gen5SpirvTranslator
Gen5SpirvStage.Compute,
state,
evaluation,
Gen5PixelOutputKind.Float,
[],
Math.Max(localSizeX, 1),
Math.Max(localSizeY, 1),
Math.Max(localSizeZ, 1),
0,
-1,
0);
0,
-1);
return context.TryCompile(out shader, out error);
}
@@ -86,15 +137,17 @@ internal static partial class Gen5SpirvTranslator
private readonly Gen5SpirvStage _stage;
private readonly Gen5ShaderState _state;
private readonly Gen5ShaderEvaluation _evaluation;
private readonly Gen5PixelOutputKind _outputKind;
private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
private readonly uint _localSizeX;
private readonly uint _localSizeY;
private readonly uint _localSizeZ;
private readonly int _globalBufferBase;
private readonly int _totalGlobalBufferCount;
private readonly int _imageBindingBase;
private readonly int _scalarRegisterBufferIndex;
private readonly List<uint> _interfaces = [];
private readonly Dictionary<uint, uint> _pixelInputs = [];
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
private readonly Dictionary<uint, uint> _vertexOutputs = [];
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
private readonly List<SpirvImageResource> _imageResources = [];
@@ -127,7 +180,6 @@ internal static partial class Gen5SpirvTranslator
private uint _lds;
private uint _workgroupUintPointer;
private uint _positionOutput;
private uint _pixelOutput;
private uint _vertexIndexInput;
private uint _instanceIndexInput;
private uint _fragCoordInput;
@@ -157,22 +209,28 @@ internal static partial class Gen5SpirvTranslator
uint Type,
uint ComponentCount);
private readonly record struct SpirvPixelOutput(
uint Variable,
uint Type,
Gen5PixelOutputKind Kind);
public CompilationContext(
Gen5SpirvStage stage,
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
Gen5PixelOutputKind outputKind,
IReadOnlyList<Gen5PixelOutputBinding> pixelOutputBindings,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
int globalBufferBase,
int totalGlobalBufferCount,
int imageBindingBase)
int imageBindingBase,
int scalarRegisterBufferIndex)
{
_stage = stage;
_state = state;
_evaluation = evaluation;
_outputKind = outputKind;
_pixelOutputBindings = pixelOutputBindings;
_localSizeX = localSizeX;
_localSizeY = localSizeY;
_localSizeZ = localSizeZ;
@@ -181,6 +239,7 @@ internal static partial class Gen5SpirvTranslator
? evaluation.GlobalMemoryBindings.Count
: totalGlobalBufferCount;
_imageBindingBase = imageBindingBase;
_scalarRegisterBufferIndex = scalarRegisterBufferIndex;
}
public bool TryCompile(out Gen5SpirvShader shader, out string error)
@@ -692,19 +751,24 @@ internal static partial class Gen5SpirvTranslator
(uint)SpirvBuiltIn.FragCoord);
_interfaces.Add(_fragCoordInput);
var outputType = _outputKind switch
foreach (var binding in _pixelOutputBindings)
{
Gen5PixelOutputKind.Uint => _uvec4Type,
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
_ => _vec4Type,
};
var outputPointer =
_module.TypePointer(SpirvStorageClass.Output, outputType);
_pixelOutput = _module.AddGlobalVariable(
outputPointer,
SpirvStorageClass.Output);
_module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0);
_interfaces.Add(_pixelOutput);
var outputType = GetPixelOutputType(binding.Kind);
var outputPointer =
_module.TypePointer(SpirvStorageClass.Output, outputType);
var variable = _module.AddGlobalVariable(
outputPointer,
SpirvStorageClass.Output);
_module.AddName(variable, $"mrt{binding.GuestSlot}");
_module.AddDecoration(
variable,
SpirvDecoration.Location,
binding.HostLocation);
_pixelOutputs.Add(
binding.GuestSlot,
new SpirvPixelOutput(variable, outputType, binding.Kind));
_interfaces.Add(variable);
}
}
else
{
@@ -767,15 +831,25 @@ internal static partial class Gen5SpirvTranslator
private void EmitInitialState()
{
for (uint index = 0;
index < _evaluation.InitialScalarRegisters.Count &&
index < ScalarRegisterCount;
index++)
if (_scalarRegisterBufferIndex >= 0)
{
var value = _evaluation.InitialScalarRegisters[(int)index];
if (value != 0)
for (uint index = 0; index < ScalarRegisterCount; index++)
{
StoreS(index, UInt(value));
StoreS(index, LoadBufferWord(_scalarRegisterBufferIndex, UInt(index)));
}
}
else
{
for (uint index = 0;
index < _evaluation.InitialScalarRegisters.Count &&
index < ScalarRegisterCount;
index++)
{
var value = _evaluation.InitialScalarRegisters[(int)index];
if (value != 0)
{
StoreS(index, UInt(value));
}
}
}
@@ -813,7 +887,10 @@ internal static partial class Gen5SpirvTranslator
1);
StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
StoreV(3, Bitcast(_uintType, y), guardWithExec: false);
Store(_pixelOutput, _module.ConstantNull(GetPixelOutputType()));
foreach (var output in _pixelOutputs.Values)
{
Store(output.Variable, _module.ConstantNull(output.Type));
}
}
else
{
@@ -2066,21 +2143,27 @@ internal static partial class Gen5SpirvTranslator
if (_stage == Gen5SpirvStage.Pixel)
{
if (export.Target != 0)
if (!_pixelOutputs.TryGetValue(export.Target, out var output))
{
return true;
}
var outputType = GetPixelOutputType();
var values = new uint[4];
for (var component = 0; component < 4; component++)
{
var enabled = (export.EnableMask & (1u << component)) != 0;
if (!enabled)
{
values[component] = _outputKind == Gen5PixelOutputKind.Sint
? Bitcast(_intType, UInt(0))
: UInt(0);
values[component] = _module.AddInstruction(
SpirvOp.CompositeExtract,
output.Kind switch
{
Gen5PixelOutputKind.Uint => _uintType,
Gen5PixelOutputKind.Sint => _intType,
_ => _floatType,
},
Load(output.Type, output.Variable),
(uint)component);
continue;
}
@@ -2089,7 +2172,7 @@ internal static partial class Gen5SpirvTranslator
var value = LoadCompressedExportComponent(
instruction,
component);
values[component] = _outputKind switch
values[component] = output.Kind switch
{
Gen5PixelOutputKind.Uint => _module.AddInstruction(
SpirvOp.ConvertFToU,
@@ -2105,7 +2188,7 @@ internal static partial class Gen5SpirvTranslator
}
var raw = LoadV(instruction.Sources[component].Value);
values[component] = _outputKind switch
values[component] = output.Kind switch
{
Gen5PixelOutputKind.Uint => raw,
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
@@ -2115,15 +2198,15 @@ internal static partial class Gen5SpirvTranslator
var vector = _module.AddInstruction(
SpirvOp.CompositeConstruct,
outputType,
output.Type,
values);
vector = _module.AddInstruction(
SpirvOp.Select,
outputType,
output.Type,
Load(_boolType, _exec),
vector,
Load(outputType, _pixelOutput));
Store(_pixelOutput, vector);
Load(output.Type, output.Variable));
Store(output.Variable, vector);
return true;
}
@@ -2191,8 +2274,8 @@ internal static partial class Gen5SpirvTranslator
(uint)(component & 1));
}
private uint GetPixelOutputType() =>
_outputKind switch
private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
kind switch
{
Gen5PixelOutputKind.Uint => _uvec4Type,
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
+37
View File
@@ -0,0 +1,37 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Ajm;
public static class AjmExports
{
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
private static int _nextContextId;
[SysAbiExport(
Nid = "dl+4eHSzUu4",
ExportName = "sceAjmInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int Initialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outContextAddress = ctx[CpuRegister.Rsi];
// AJM requires a zero reserved argument.
if (reserved != 0 || outContextAddress == 0)
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
return ctx.SetReturn(0);
}
}
+46
View File
@@ -348,6 +348,18 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Zi3dBUjgyXI",
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueueOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWriteKernelEventQueueOnCompletion(CpuContext ctx)
{
TraceAmpr(ctx, "measure_write_equeue_complete", 0, KernelEventQueueRecordSize, 0);
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "C+IEj+BsAFM",
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
@@ -440,6 +452,40 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "o67gODLFpls",
ExportName = "sceAmprCommandBufferWriteKernelEventQueueOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWriteKernelEventQueueOnCompletion(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var equeue = ctx[CpuRegister.Rsi];
var ident = ctx[CpuRegister.Rdx];
var completionToken = ctx[CpuRegister.Rcx];
var userData = ctx[CpuRegister.R8];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!AppendKernelEventQueueRecord(
ctx,
commandBuffer,
equeue,
ident,
completionToken,
userData))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_equeue_complete", commandBuffer, ident, completionToken);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sJXyWHjP-F8",
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
+31 -10
View File
@@ -54,20 +54,41 @@ internal static class PakDirectoryTracker
if (state.Directory is { } entries)
{
// Several unrelated archived files can share a byte count (e.g. a head model and a bot
// navigation file both 0x3A34 bytes). Directory order alone then picks whichever comes
// first, which is wrong when the game reads them out of order. id archives cluster
// related assets, and the guest streams them with locality, so disambiguate collisions
// by choosing the unconsumed match nearest the running read cursor.
DirectoryEntry? best = null;
var bestDistance = ulong.MaxValue;
foreach (var entry in entries)
{
if (!entry.Consumed && entry.FileLen == requestedSize)
if (entry.Consumed || entry.FileLen != requestedSize)
{
entry.Consumed = true;
if (_trace)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
}
return entry.FilePos;
continue;
}
var distance = entry.FilePos >= state.NextOffset
? entry.FilePos - state.NextOffset
: state.NextOffset - entry.FilePos;
if (distance < bestDistance)
{
bestDistance = distance;
best = entry;
}
}
if (best is not null)
{
best.Consumed = true;
if (_trace)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{best.Name}' " +
$"filepos=0x{best.FilePos:X8} filelen=0x{best.FileLen:X8}");
}
return best.FilePos;
}
}
+1 -1
View File
@@ -202,7 +202,7 @@ public static class AudioOut2Exports
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
@@ -213,6 +213,17 @@ public static class AudioOutExports
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
public static void ShutdownAllPorts()
{
foreach (var handle in Ports.Keys)
{
if (Ports.TryRemove(handle, out var port))
{
port.Dispose();
}
}
}
private static bool TryGetFormat(
int rawFormat,
out int channels,
@@ -35,6 +35,17 @@ public static class AvPlayerExports
return unchecked((int)handle);
}
[SysAbiExport(
Nid = "KMcEa+rHsIo",
ExportName = "sceAvPlayerAddSource",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerAddSource(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JdksQu8pNdQ",
ExportName = "sceAvPlayerGetVideoDataEx",
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.CommonDialog;
@@ -10,10 +11,21 @@ public static class MsgDialogExports
{
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusRunning = 2;
private const int StatusFinished = 3;
private const int ResultSize = 0x20;
private static int _initialized;
private const int ErrorOk = 0;
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
private const int ErrorNotFinished = unchecked((int)0x80B80005);
private const int ErrorBusy = unchecked((int)0x80B80007);
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
private const int ErrorArgNull = unchecked((int)0x80B8000D);
// Result buffer layout follows the common dialog convention: mode at +0x00,
// result at +0x04, buttonId at +0x08. The affirmative button (OK/YES) is 1.
private const int ResultSize = 0x20;
private const int ButtonIdAffirmative = 1;
private static int _status;
[SysAbiExport(
@@ -24,11 +36,10 @@ public static class MsgDialogExports
public static int MsgDialogInitialize(CpuContext ctx)
{
// Treat repeated initialization as success. The dialog service is process-global in
// this HLE implementation and has no per-call resources to recreate.
Interlocked.Exchange(ref _initialized, 1);
Interlocked.Exchange(ref _status, StatusInitialized);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
// this HLE implementation and has no per-call resources to recreate. Only promote
// from NONE so re-initializing mid-flow cannot clobber a running/finished dialog.
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
return ctx.SetReturn(ErrorOk);
}
[SysAbiExport(
@@ -38,9 +49,12 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogTerminate(CpuContext ctx)
{
Interlocked.Exchange(ref _initialized, 0);
Interlocked.Exchange(ref _status, StatusNone);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
}
return ctx.SetReturn(ErrorOk);
}
[SysAbiExport(
@@ -50,13 +64,29 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogOpen(CpuContext ctx)
{
LogDialogMessage(ctx, ctx[CpuRegister.Rdi]);
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return ctx.SetReturn(ErrorArgNull);
}
// There is no host popup to actually show. Complete immediately with "finished" so a
// guest polling loop (GetStatus/UpdateStatus -> GetResult -> Close) sees a dismissed
// dialog on its very first poll instead of spinning forever waiting for user input.
Interlocked.Exchange(ref _status, StatusFinished);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
var status = Volatile.Read(ref _status);
if (status == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
}
if (status == StatusRunning)
{
return ctx.SetReturn(ErrorBusy);
}
LogDialogMessage(ctx, paramAddress);
// There is no host popup to actually show. Enter RUNNING so close/cancel paths see
// a live dialog; the guest's next status poll auto-dismisses it (see PollStatus).
Interlocked.Exchange(ref _status, StatusRunning);
return ctx.SetReturn(ErrorOk);
}
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
@@ -65,12 +95,6 @@ public static class MsgDialogExports
// level deep, then a second level for nested sub-param structs.
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
{
if (paramAddress == 0)
{
Console.Error.WriteLine("[LOADER][INFO] sceMsgDialogOpen: param=NULL");
return;
}
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
Span<byte> head = stackalloc byte[0xA0];
@@ -134,14 +158,24 @@ public static class MsgDialogExports
ExportName = "sceMsgDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
public static int MsgDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
[SysAbiExport(
Nid = "6fIC3XKt2k0",
ExportName = "sceMsgDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
public static int MsgDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
// With no host UI the dialog cannot wait for user input: the first status poll after
// Open observes the dialog as already dismissed. Advancing on both UpdateStatus and
// GetStatus keeps every guest polling pattern free of infinite RUNNING loops, while
// an Open -> Close sequence with no poll in between still exercises the close path.
private static int PollStatus()
{
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
return Volatile.Read(ref _status);
}
[SysAbiExport(
Nid = "Lr8ovHH9l6A",
@@ -153,17 +187,27 @@ public static class MsgDialogExports
var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return ctx.SetReturn(ErrorArgNull);
}
if (Volatile.Read(ref _status) != StatusFinished)
{
return ctx.SetReturn(ErrorNotFinished);
}
// Report the affirmative button so yes/no prompts take the confirming branch;
// buttonId 0 is the "invalid" sentinel and games may treat it as an error.
Span<byte> result = stackalloc byte[ResultSize];
result.Clear();
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], ButtonIdAffirmative);
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return ctx.SetReturn(ErrorOk);
}
[SysAbiExport(
@@ -173,13 +217,44 @@ public static class MsgDialogExports
LibraryName = "libSceMsgDialog")]
public static int MsgDialogClose(CpuContext ctx)
{
Interlocked.Exchange(ref _status, StatusFinished);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
{
return ctx.SetReturn(ErrorNotRunning);
}
return ctx.SetReturn(ErrorOk);
}
private static int SetReturn(CpuContext ctx, int result)
[SysAbiExport(
Nid = "wTpfglkmv34",
ExportName = "sceMsgDialogProgressBarSetValue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogProgressBarSetValue(CpuContext ctx) => ProgressBarNoOp(ctx);
[SysAbiExport(
Nid = "Gc5k1qcK4fs",
ExportName = "sceMsgDialogProgressBarInc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogProgressBarInc(CpuContext ctx) => ProgressBarNoOp(ctx);
[SysAbiExport(
Nid = "6H-71OdrpXM",
ExportName = "sceMsgDialogProgressBarSetMsg",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMsgDialog")]
public static int MsgDialogProgressBarSetMsg(CpuContext ctx) => ProgressBarNoOp(ctx);
// There is no visible bar to update. Accept the call whenever the service is alive so
// save/install loops that report progress do not abort on an unexpected error.
private static int ProgressBarNoOp(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
if (Volatile.Read(ref _status) == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
}
return ctx.SetReturn(ErrorOk);
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
namespace SharpEmu.Libs;
public static class HostTimerResolution
{
private const uint TargetPeriodMilliseconds = 1;
private static int _requested;
public static void Request()
{
if (Interlocked.Exchange(ref _requested, 1) != 0)
{
return;
}
if (!OperatingSystem.IsWindows())
{
return;
}
try
{
if (TimeBeginPeriod(TargetPeriodMilliseconds) != 0)
{
Console.Error.WriteLine(
"[LOADER][WARN] Host timer resolution request rejected; " +
"timed waits keep the default ~15.6 ms granularity.");
}
}
catch (DllNotFoundException exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Host timer resolution unavailable: {exception.Message}");
}
}
[SupportedOSPlatform("windows")]
[DllImport("winmm.dll", EntryPoint = "timeBeginPeriod", ExactSpelling = true)]
private static extern uint TimeBeginPeriod(uint uPeriod);
}
+76
View File
@@ -78,6 +78,82 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
[SysAbiExport(
Nid = "+drDFyAS6u4",
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerSetGlobalNullAccessCallback(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
JsonObjectHeap.GlobalNullAccessCallback = ctx[CpuRegister.Rsi];
JsonObjectHeap.GlobalNullAccessCallbackContext = ctx[CpuRegister.Rdx];
TraceJson("Initializer.setGlobalNullAccessCallback", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetAllocator(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int Initializer2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
+234
View File
@@ -0,0 +1,234 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Json;
// sce::Json::Value and sce::Json::String constructors, setters and destructors. Prospero titles
// (Quake among them) build a Value tree and populate it through these before serializing it for a
// web request; without them the imports resolve to nothing and the guest faults on the call. The
// payload is modelled in JsonObjectHeap; here we only translate the C++ ABI (registers in, `this`
// back out) and never write the guest object, so a stack-allocated Value/String is left intact.
//
// Only the complete-object variants (C1/D1) are bound — those are what standalone locals emit and
// what the observed Prospero imports use. The base-object variants (C2/D2) are left for a title
// that actually imports them.
public static class JsonValueExports
{
private const int MaxStringLength = 0x10000;
private static int ReturnThis(CpuContext ctx, ulong thisAddress)
{
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int ReturnVoid(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static double ReadDoubleArg(CpuContext ctx)
{
ctx.GetXmmRegister(0, out var low, out _);
return BitConverter.Int64BitsToDouble(unchecked((long)low));
}
private static string ReadCString(CpuContext ctx, ulong address) =>
ctx.TryReadNullTerminatedUtf8(address, MaxStringLength, out var text) ? text : string.Empty;
// ---- sce::Json::Value constructors ----
[SysAbiExport(Nid = "qBMjqyBn3OM", ExportName = "_ZN3sce4Json5ValueC1Ev",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueDefaultConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "UeuWT+yNdCQ", ExportName = "_ZN3sce4Json5ValueC1Eb",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueBooleanConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "0lLK8+kDqmE", ExportName = "_ZN3sce4Json5ValueC1El",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueIntegerConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "x4AUdbhpRB0", ExportName = "_ZN3sce4Json5ValueC1Em",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueUnsignedConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "sOmU4vnx3s0", ExportName = "_ZN3sce4Json5ValueC1Ed",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueRealConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "b9V6fmppLXY", ExportName = "_ZN3sce4Json5ValueC1EPKc",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueCStringConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "CbrT3dwDILo", ExportName = "_ZN3sce4Json5ValueC1ENS0_9ValueTypeE",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueTypeConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "sZIoMRGO+jk", ExportName = "_ZN3sce4Json5ValueC1ERKNS0_6StringE",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueStringConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "WTtYf+cNnXI", ExportName = "_ZN3sce4Json5ValueD1Ev",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueDestructor(CpuContext ctx)
{
JsonObjectHeap.RemoveValue(ctx[CpuRegister.Rdi]);
return ReturnVoid(ctx);
}
// ---- sce::Json::Value setters (return Value&, i.e. `this`) ----
[SysAbiExport(Nid = "5yHuiWXo2gg", ExportName = "_ZN3sce4Json5Value3setEb",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueSetBoolean(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromBoolean((ctx[CpuRegister.Rsi] & 0xFF) != 0));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "QxVVYhP-mvg", ExportName = "_ZN3sce4Json5Value3setEl",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueSetInteger(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromInteger(unchecked((long)ctx[CpuRegister.Rsi])));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "SIe1ZmW7e7s", ExportName = "_ZN3sce4Json5Value3setEm",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueSetUnsigned(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromUnsignedInteger(ctx[CpuRegister.Rsi]));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "BSmWDIkV4w4", ExportName = "_ZN3sce4Json5Value3setEd",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueSetReal(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromReal(ReadDoubleArg(ctx)));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "IKQimvG9Wqs", ExportName = "_ZN3sce4Json5Value3setENS0_9ValueTypeE",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueSetType(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromExplicitType(unchecked((uint)ctx[CpuRegister.Rsi])));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "n6FC+l9DU70", ExportName = "_ZN3sce4Json5Value3setEPKc",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueSetCString(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(ReadCString(ctx, ctx[CpuRegister.Rsi])));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "6l3Bv2gysNc", ExportName = "_ZN3sce4Json5Value3setERKNS0_6StringE",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueSetString(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.FromString(JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi])));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "FIjXN2TkuTs", ExportName = "_ZN3sce4Json5Value5clearEv",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int ValueClear(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetValue(thisAddress, JsonValueState.Null);
return ReturnThis(ctx, thisAddress);
}
// ---- sce::Json::String ----
[SysAbiExport(Nid = "9KUZFjI1IxA", ExportName = "_ZN3sce4Json6StringC1EPKc",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int StringCStringConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetString(thisAddress, ReadCString(ctx, ctx[CpuRegister.Rsi]));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "qSmqLXXCPas", ExportName = "_ZN3sce4Json6StringC1Ev",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int StringDefaultConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetString(thisAddress, string.Empty);
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "0CAesfH963Q", ExportName = "_ZN3sce4Json6StringC1ERKS1_",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int StringCopyConstructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
JsonObjectHeap.SetString(thisAddress, JsonObjectHeap.GetStringOrEmpty(ctx[CpuRegister.Rsi]));
return ReturnThis(ctx, thisAddress);
}
[SysAbiExport(Nid = "cG1VE2HMl6c", ExportName = "_ZN3sce4Json6StringD1Ev",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceJson")]
public static int StringDestructor(CpuContext ctx)
{
JsonObjectHeap.RemoveString(ctx[CpuRegister.Rdi]);
return ReturnVoid(ctx);
}
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Json;
// sce::Json::Value is an opaque variant type (null / bool / signed / unsigned / real / string /
// array / object). Games build one, populate it through set()/ctors and later serialize it. We
// model the payload host-side keyed by the guest `this` pointer instead of writing into the guest
// object: the object is often stack-allocated and its real byte layout is unknown, so writing a
// guessed layout risks smashing an adjacent stack canary (the same failure the AudioOut2 context
// param sizing note in this project already ran into). The guest reaches the payload only through
// libSceJson methods, so shadowing by address is enough for the build path.
internal enum JsonValueKind : byte
{
Null = 0,
Boolean = 1,
Integer = 2,
UInteger = 3,
Real = 4,
String = 5,
// set(ValueType) / Value(ValueType): the guest chose the type itself. We keep its raw enum
// value verbatim rather than mapping it, because the canonical ValueType constants are not
// known from clean-room evidence and round-tripping the guest's own value is what matters.
ExplicitType = 6,
}
internal readonly struct JsonValueState
{
private JsonValueState(
JsonValueKind kind,
bool boolean = false,
long integer = 0,
ulong unsignedInteger = 0,
double real = 0,
string? text = null,
uint explicitType = 0)
{
Kind = kind;
Boolean = boolean;
Integer = integer;
UnsignedInteger = unsignedInteger;
Real = real;
Text = text;
ExplicitType = explicitType;
}
public JsonValueKind Kind { get; }
public bool Boolean { get; }
public long Integer { get; }
public ulong UnsignedInteger { get; }
public double Real { get; }
public string? Text { get; }
public uint ExplicitType { get; }
public static JsonValueState Null { get; } = new(JsonValueKind.Null);
public static JsonValueState FromBoolean(bool value) => new(JsonValueKind.Boolean, boolean: value);
public static JsonValueState FromInteger(long value) => new(JsonValueKind.Integer, integer: value);
public static JsonValueState FromUnsignedInteger(ulong value) =>
new(JsonValueKind.UInteger, unsignedInteger: value);
public static JsonValueState FromReal(double value) => new(JsonValueKind.Real, real: value);
public static JsonValueState FromString(string value) => new(JsonValueKind.String, text: value);
public static JsonValueState FromExplicitType(uint value) =>
new(JsonValueKind.ExplicitType, explicitType: value);
}
// Shared host-side heap for the libSceJson object shadows. Keyed by the guest object address;
// constructors overwrite and destructors remove, so guest stack-address reuse stays correct.
internal static class JsonObjectHeap
{
public static ConcurrentDictionary<ulong, JsonValueState> Values { get; } = new();
public static ConcurrentDictionary<ulong, string> Strings { get; } = new();
// Guest function the library should call when a Value is read as the wrong type. This HLE
// never dereferences missing members (shadows degrade to defaults), so the hook is stored for
// fidelity but not invoked.
public static ulong GlobalNullAccessCallback;
public static ulong GlobalNullAccessCallbackContext;
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
public static void SetString(ulong address, string text) => Strings[address] = text;
public static void RemoveString(ulong address) => Strings.TryRemove(address, out _);
// A missing shadow (temporary the compiler built without an out-of-line ctor, or a copy we did
// not track) degrades to the empty string rather than faulting.
public static string GetStringOrEmpty(ulong address) =>
Strings.TryGetValue(address, out var text) ? text : string.Empty;
internal static void ResetForTests()
{
Values.Clear();
Strings.Clear();
GlobalNullAccessCallback = 0;
GlobalNullAccessCallbackContext = 0;
}
}
@@ -80,14 +80,8 @@ public static class KernelAprCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
{
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
// Completion output was written when the command was submitted.
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -169,27 +163,6 @@ public static class KernelAprCompatExports
return ctx.Memory.TryWrite(resultAddress, result);
}
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
{
if (waitArg2 == 0)
{
return submittedResultAddress;
}
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
{
return submittedResultAddress;
}
return waitArg2;
}
private static bool IsAmprCompletionToken(ulong value)
{
var tag = value >> 56;
return tag is 0x0C or 0x10;
}
private static void TraceApr(
CpuContext ctx,
string operation,
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
@@ -33,6 +34,11 @@ public static class KernelEventFlagCompatExports
public object Gate { get; } = new();
}
private sealed class EventFlagWaiter
{
public OrbisGen2Result? Result { get; set; }
}
[SysAbiExport(
Nid = "BpFoboUJoZU",
ExportName = "sceKernelCreateEventFlag",
@@ -233,9 +239,47 @@ public static class KernelEventFlagCompatExports
if (timeoutAddress != 0)
{
if (timeoutUsec == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
TimeSpan.FromTicks((long)timeoutUsec * 10L));
var timedWaiter = new EventFlagWaiter();
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
resumeHandler: () => CompleteBlockedTimedWait(
ctx,
state,
timedWaiter,
pattern,
waitMode,
resultAddress,
timeoutAddress,
deadline),
wakeHandler: () => TryCompleteBlockedTimedWait(
ctx,
state,
timedWaiter,
pattern,
waitMode,
resultAddress),
blockDeadlineTimestamp: deadline))
{
state.WaitingThreads++;
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
@@ -446,6 +490,92 @@ public static class KernelEventFlagCompatExports
}
}
private static bool TryCompleteBlockedTimedWait(
CpuContext ctx,
EventFlagState state,
EventFlagWaiter waiter,
ulong pattern,
uint waitMode,
ulong resultAddress)
{
lock (state.Gate)
{
if (waiter.Result is not null)
{
return true;
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_OK
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
ApplyClearMode(state, pattern, waitMode);
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
return true;
}
}
private static int CompleteBlockedTimedWait(
CpuContext ctx,
EventFlagState state,
EventFlagWaiter waiter,
ulong pattern,
uint waitMode,
ulong resultAddress,
ulong timeoutAddress,
long deadlineTimestamp)
{
lock (state.Gate)
{
if (waiter.Result is null)
{
if (IsSatisfied(state.Bits, pattern, waitMode))
{
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_OK
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
ApplyClearMode(state, pattern, waitMode);
}
}
else
{
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
}
}
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(
uint.MaxValue,
remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
}
return (int)waiter.Result.Value;
}
private static string GetEventFlagWakeKey(ulong handle) =>
$"event_flag:0x{handle:X16}";
@@ -11,6 +11,7 @@ public static class KernelEventQueueCompatExports
{
private const int KernelEventSize = 0x20;
public const short KernelEventFilterGraphics = -14;
public const short KernelEventFilterUser = -11;
public const short KernelEventFilterAmpr = -16;
public const short KernelEventFilterAmprSystem = -17;
@@ -89,8 +90,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddUserEventEdge(CpuContext ctx)
{
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
0);
TraceEventQueue(ctx, "add_user_edge", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -100,8 +109,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
0);
TraceEventQueue(ctx, "add_user", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -111,8 +128,15 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelDeleteUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var deleted = DeleteRegisteredEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser);
TraceEventQueue(ctx, "delete_user", handle);
return deleted
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -122,8 +146,18 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelTriggerUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var triggered = TriggerRegisteredEvent(
handle,
ctx[CpuRegister.Rsi],
KernelEventFilterUser,
flags: 0x21,
fflags: 0,
data: ctx[CpuRegister.Rdx]);
TraceEventQueue(ctx, "trigger_user", handle);
return triggered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -475,6 +509,43 @@ public static class KernelEventQueueCompatExports
return triggeredCount;
}
private static bool TriggerRegisteredEvent(
ulong handle,
ulong ident,
short filter,
ushort flags,
uint fflags,
ulong data)
{
lock (_eventQueueGate)
{
if (!_registeredEvents.TryGetValue(handle, out var registrations) ||
!registrations.TryGetValue((ident, filter), out var registration))
{
return false;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new LinkedList<KernelQueuedEvent>();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
flags,
fflags,
data,
registration.UserData));
}
WakeEventQueue(handle);
return true;
}
public static bool TriggerDisplayEvent(
ulong handle,
ulong ident,
@@ -656,6 +727,6 @@ public static class KernelEventQueueCompatExports
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
}
-56
View File
@@ -461,66 +461,10 @@ public static class KernelExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Unidentified kernel NIDs observed at runtime; stubbed to return 0 until they are
// resolved against scripts/ps5_names.txt.
[SysAbiExport(
Nid = "9T2pDF2Ryqg",
ExportName = "sceKernelUnknown9T2p",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknown9T2p(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "TU-d9PfIHPM",
ExportName = "sceKernelUnknownTU",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownTU(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "iWQWrwiSt8A",
ExportName = "sceKernelUnknowniWQW",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknowniWQW(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "KuOmgKoqCdY",
ExportName = "sceKernelUnknownKuOmg",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownKuOmg(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "RenI1lL1WFk",
ExportName = "sceKernelUnknownRenI",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownRenI(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "pQGpHYopAIY",
ExportName = "sceKernelUnknownpQGp",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownpQGp(CpuContext ctx) => SetZeroReturn(ctx);
[SysAbiExport(
Nid = "Rbvt+5Y2iEw",
ExportName = "sceKernelUnknownRbvt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnknownRbvt(CpuContext ctx) => SetZeroReturn(ctx);
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
// shadowing them breaks every C++ static-init guard in the game.
private static int SetZeroReturn(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
}
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Ampr;
using System.Buffers;
using System.Buffers.Binary;
@@ -52,14 +53,13 @@ public static class KernelMemoryCompatExports
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
private const int OrbisVirtualQueryInfoSize = 72;
private const int OrbisKernelMaximumNameLength = 32;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint MemRelease = 0x8000;
// Raw Windows PAGE_* values used only against HostRegionInfo.RawProtection,
// which by contract carries the untranslated protection word of the host
// platform in use (see IHostMemory).
private const uint HostPageNoAccess = 0x01;
private const uint HostPageReadOnly = 0x02;
private const uint HostPageReadWrite = 0x04;
private const uint HostPageWriteCopy = 0x08;
private const uint HostPageExecute = 0x10;
private const uint HostPageExecuteRead = 0x20;
private const uint HostPageExecuteReadWrite = 0x40;
private const uint HostPageExecuteWriteCopy = 0x80;
@@ -110,6 +110,15 @@ public static class KernelMemoryCompatExports
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
private static long _nextFileDescriptor = 2;
internal static int AllocateGuestFileDescriptor()
{
lock (_fdGate)
{
return (int)Interlocked.Increment(ref _nextFileDescriptor);
}
}
private static ulong _nextPhysicalAddress;
private static ulong _nextVirtualAddress;
private static ulong _mainDirectMemoryPoolBase = UnsetMainDirectMemoryPoolBase;
@@ -127,31 +136,9 @@ public static class KernelMemoryCompatExports
private static string? _cachedApp0Root;
private static string? _cachedDownload0Root;
[StructLayout(LayoutKind.Sequential)]
private struct MemoryBasicInformation
{
public nint BaseAddress;
public nint AllocationBase;
public uint AllocationProtect;
public nuint RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nuint VirtualQuery(nint lpAddress, out MemoryBasicInformation lpBuffer, nuint dwLength);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
// Property (not a cached field) so merely touching this type never resolves
// the platform backend; non-Windows hosts only throw if a call is reached.
private static IHostMemory HostMemory => HostPlatform.Current.Memory;
private sealed class OpenDirectory
{
@@ -191,6 +178,40 @@ public static class KernelMemoryCompatExports
}
}
public static bool TryUnregisterGuestPathMount(string guestMountPoint)
{
if (string.IsNullOrWhiteSpace(guestMountPoint))
{
return false;
}
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
if (normalizedMountPoint is null || normalizedMountPoint == "/")
{
return false;
}
var removed = false;
lock (_guestMountGate)
{
removed = _guestMounts.Remove(normalizedMountPoint);
}
if (!removed)
{
return false;
}
lock (_statCacheGate)
{
_negativeStatCache.RemoveWhere(path =>
string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) ||
path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase));
}
return true;
}
internal static bool TryAllocateHleData(
CpuContext ctx,
ulong length,
@@ -469,7 +490,7 @@ public static class KernelMemoryCompatExports
[SysAbiExport(
Nid = "LHMrG7e8G78",
ExportName = "wcslen",
ExportName = "wcsmisc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Wcslen(CpuContext ctx)
@@ -1479,6 +1500,214 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ob5xAW4ln-0",
ExportName = "strchr",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Strchr(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
if (address == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
// The terminator counts as part of the scanned range, so strchr(s, '\0')
// returns a pointer to the string's null byte just like a native libc.
Span<byte> current = stackalloc byte[1];
for (ulong index = 0; index < 1_048_576; index++)
{
if (!TryReadCompat(ctx, address + index, current))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (current[0] == needle)
{
ctx[CpuRegister.Rax] = address + index;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (current[0] == 0)
{
break;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "9yDWMxEFdJU",
ExportName = "strrchr",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Strrchr(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
if (address == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong match = 0;
var found = false;
Span<byte> current = stackalloc byte[1];
for (ulong index = 0; index < 1_048_576; index++)
{
if (!TryReadCompat(ctx, address + index, current))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (current[0] == needle)
{
match = address + index;
found = true;
}
if (current[0] == 0)
{
break;
}
}
ctx[CpuRegister.Rax] = found ? match : 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8u8lPzUEq+U",
ExportName = "memchr",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Memchr(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
var needle = unchecked((byte)ctx[CpuRegister.Rsi]);
var count = ctx[CpuRegister.Rdx];
Span<byte> current = stackalloc byte[1];
for (ulong index = 0; index < count; index++)
{
if (!TryReadCompat(ctx, address + index, current))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (current[0] == needle)
{
ctx[CpuRegister.Rax] = address + index;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ls4tzzhimqQ",
ExportName = "strcat",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Strcat(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var source = ctx[CpuRegister.Rsi];
if (!TryReadCString(ctx, source, 1_048_576, out var sourceBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryReadCString(ctx, destination, 1_048_576, out var destinationBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
// Overwrite the destination terminator and re-terminate after the copied bytes.
var appendAddress = destination + (ulong)destinationBytes.Length;
var payload = new byte[sourceBytes.Length + 1];
sourceBytes.CopyTo(payload.AsSpan());
if (!TryWriteCompat(ctx, appendAddress, payload))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kHg45qPC6f0",
ExportName = "strncat",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Strncat(CpuContext ctx)
{
var destination = ctx[CpuRegister.Rdi];
var source = ctx[CpuRegister.Rsi];
var limit = ctx[CpuRegister.Rdx];
// Bounding the source read by the count yields strncat's "at most n bytes"
// semantics while still stopping early at the source terminator.
if (!TryReadCString(ctx, source, limit, out var sourceBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryReadCString(ctx, destination, 1_048_576, out var destinationBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var appendAddress = destination + (ulong)destinationBytes.Length;
var payload = new byte[sourceBytes.Length + 1];
sourceBytes.CopyTo(payload.AsSpan());
if (!TryWriteCompat(ctx, appendAddress, payload))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "viiwFMaNamA",
ExportName = "strstr",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Strstr(CpuContext ctx)
{
var haystack = ctx[CpuRegister.Rdi];
var needle = ctx[CpuRegister.Rsi];
if (!TryReadCString(ctx, haystack, 1_048_576, out var haystackBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryReadCString(ctx, needle, 1_048_576, out var needleBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
// An empty needle matches at the start of the haystack.
if (needleBytes.Length == 0)
{
ctx[CpuRegister.Rax] = haystack;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var matchIndex = haystackBytes.AsSpan().IndexOf(needleBytes.AsSpan());
ctx[CpuRegister.Rax] = matchIndex >= 0 ? haystack + (ulong)matchIndex : 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "QrZZdJ8XsX0",
ExportName = "fputs",
@@ -1555,9 +1784,10 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var directoryFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
int directoryFd;
lock (_fdGate)
{
directoryFd = AllocateGuestFileDescriptor();
_openDirectories[directoryFd] = new OpenDirectory
{
Path = hostPath,
@@ -1578,9 +1808,10 @@ public static class KernelMemoryCompatExports
stream.Seek(0, SeekOrigin.End);
}
var fd = (int)Interlocked.Increment(ref _nextFileDescriptor);
int fd;
lock (_fdGate)
{
fd = AllocateGuestFileDescriptor();
_openFiles[fd] = stream;
}
@@ -1603,6 +1834,45 @@ public static class KernelMemoryCompatExports
}
}
[SysAbiExport(
Nid = "fgIsQ10xYVA",
ExportName = "sceKernelChmod",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelChmod(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var mode = unchecked((uint)ctx[CpuRegister.Rsi]);
if (pathAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var hostPath = ResolveGuestPath(guestPath);
if (IsReadOnlyGuestMutationPath(guestPath))
{
LogOpenTrace($"chmod readonly path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
if (!File.Exists(hostPath) && !Directory.Exists(hostPath))
{
AddNegativeStatCacheForGuestPath(guestPath);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
// POSIX permission bits have no host equivalent on Windows; accept the call
// so guests that chmod their freshly created files/directories can proceed.
LogOpenTrace($"chmod path='{guestPath}' host='{hostPath}' mode=0x{mode:X}");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "NNtFaKJbPt0",
ExportName = "_close",
@@ -1672,6 +1942,30 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "E6ao34wPw+U",
ExportName = "stat",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixStat(CpuContext ctx)
{
var result = KernelStat(ctx);
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return 0;
}
var errno = result switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => Einval,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => Efault,
_ => 2,
};
KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
ctx[CpuRegister.Rax] = ulong.MaxValue;
return -1;
}
[SysAbiExport(
Nid = "gEpBkcwxUjw",
ExportName = "sceKernelAprResolveFilepathsToIdsAndFileSizes",
@@ -1953,6 +2247,12 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (KernelSocketCompatExports.TryCloseSocketFd(fd))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
FileStream? stream;
lock (_fdGate)
{
@@ -1996,6 +2296,17 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (KernelSocketCompatExports.TryReadSocketFd(ctx, fd, bufferAddress, requested, out var socketBytesRead, out var socketError))
{
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
{
return (int)socketError;
}
ctx[CpuRegister.Rax] = socketBytesRead;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
FileStream? stream;
lock (_fdGate)
{
@@ -2225,6 +2536,17 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (KernelSocketCompatExports.TryWriteSocketFd(ctx, fd, payload, out var socketError))
{
if (socketError != OrbisGen2Result.ORBIS_GEN2_OK)
{
return (int)socketError;
}
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
FileStream? stream;
lock (_fdGate)
{
@@ -2974,7 +3296,7 @@ public static class KernelMemoryCompatExports
[SysAbiExport(
Nid = "4h6F1LLbTiw",
ExportName = "sceKernelMapFlexibleMemoryInternal",
ExportName = "sceKernelMapNamedFlexibleMemoryInternal",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
@@ -3258,7 +3580,7 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
if (!TryProtectHostRange(ctx, alignedAddress, alignedLength, protection))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -3292,7 +3614,7 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryProtectHostRange(alignedAddress, alignedLength, protection))
if (!TryProtectHostRange(ctx, alignedAddress, alignedLength, protection))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -3441,6 +3763,18 @@ public static class KernelMemoryCompatExports
return true;
}
internal static bool TryFormatStringFromVaList(CpuContext ctx, string format, ulong vaListAddress, out string rendered)
{
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
{
rendered = format;
return false;
}
rendered = FormatString(ctx, format, ref vaCursor);
return true;
}
private static int WriteSnprintfOutput(
CpuContext ctx,
ulong destination,
@@ -3914,7 +4248,7 @@ public static class KernelMemoryCompatExports
_ => unchecked((int)argumentSource.NextGpArg())
};
var formatted = value.ToString();
var formatted = value.ToString(CultureInfo.InvariantCulture);
if (showSign && value >= 0)
formatted = "+" + formatted;
else if (spaceForSign && value >= 0)
@@ -3938,7 +4272,7 @@ public static class KernelMemoryCompatExports
_ => (uint)argumentSource.NextGpArg()
};
var formatted = value.ToString();
var formatted = value.ToString(CultureInfo.InvariantCulture);
sb.Append(PadString(formatted, width, leftAlign, padWithZero && !leftAlign));
}
break;
@@ -3959,8 +4293,8 @@ public static class KernelMemoryCompatExports
};
var formatted = specifier == 'x'
? value.ToString("x")
: value.ToString("X");
? value.ToString("x", CultureInfo.InvariantCulture)
: value.ToString("X", CultureInfo.InvariantCulture);
if (alternateForm && value != 0)
formatted = specifier == 'x' ? "0x" + formatted : "0X" + formatted;
@@ -4068,7 +4402,10 @@ public static class KernelMemoryCompatExports
var formatStr = precision >= 0
? $"{{0:{specifier}{precision}}}"
: $"{{0:{specifier}}}";
var formatted = string.Format(formatStr, value);
var formatted = string.Format(
CultureInfo.InvariantCulture,
formatStr,
value);
if (showSign && value >= 0)
formatted = "+" + formatted;
@@ -5477,42 +5814,40 @@ public static class KernelMemoryCompatExports
return alignedLength != 0;
}
private static bool TryProtectHostRange(ulong address, ulong length, int orbisProtection)
private static bool TryProtectHostRange(CpuContext ctx, ulong address, ulong length, int orbisProtection)
{
if (length == 0 || length > nuint.MaxValue)
{
return false;
}
var hostProtection = ResolveHostProtection(orbisProtection);
if (!VirtualProtect((nint)address, (nuint)length, hostProtection, out _))
if (!KernelVirtualRangeAllocator.TryResolveAddressSpace(ctx.Memory, out var addressSpace))
{
return false;
}
return true;
return addressSpace.TryProtect(address, length, ResolveGuestProtection(orbisProtection));
}
private static uint ResolveHostProtection(int orbisProtection)
private static GuestPageProtection ResolveGuestProtection(int orbisProtection)
{
var read = (orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0;
var write = (orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0;
var execute = (orbisProtection & OrbisProtCpuExec) != 0;
if (execute)
var protection = GuestPageProtection.None;
if ((orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0)
{
return write
? HostPageExecuteReadWrite
: read
? HostPageExecuteRead
: HostPageExecute;
protection |= GuestPageProtection.Read;
}
return write
? HostPageReadWrite
: read
? HostPageReadOnly
: HostPageNoAccess;
if ((orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0)
{
protection |= GuestPageProtection.Write;
}
if ((orbisProtection & OrbisProtCpuExec) != 0)
{
protection |= GuestPageProtection.Execute;
}
return protection;
}
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
@@ -5898,7 +6233,7 @@ public static class KernelMemoryCompatExports
var effectiveAlignment = Math.Max(alignment, pageSize);
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
var baseAddress = unchecked((nint)HostMemory.Allocate(0, reservationSize, HostPageProtection.ReadWrite));
if (baseAddress == 0)
{
return false;
@@ -5906,9 +6241,9 @@ public static class KernelMemoryCompatExports
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
var guardAddress = alignedAddress + (ulong)usableSize;
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
if (!HostMemory.Protect(guardAddress, pageSize, HostPageProtection.NoAccess, out _))
{
_ = VirtualFree(baseAddress, 0, MemRelease);
_ = HostMemory.Free(unchecked((ulong)baseAddress));
return false;
}
@@ -6043,7 +6378,7 @@ public static class KernelMemoryCompatExports
if (allocation.IsGuarded)
{
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
_ = HostMemory.Free(unchecked((ulong)allocation.BaseAddress));
}
else
{
@@ -6139,7 +6474,7 @@ public static class KernelMemoryCompatExports
return false;
}
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess))
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.RawProtection, writeAccess))
{
return false;
}
@@ -6150,7 +6485,7 @@ public static class KernelMemoryCompatExports
return true;
}
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess))
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.RawProtection, writeAccess))
{
return false;
}
@@ -6158,16 +6493,14 @@ public static class KernelMemoryCompatExports
return true;
}
private static bool TryQueryHostPage(ulong address, out MemoryBasicInformation info)
private static bool TryQueryHostPage(ulong address, out HostRegionInfo info)
{
info = default;
var size = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
if (VirtualQuery((nint)address, out info, size) == 0)
if (!HostMemory.Query(address, out info))
{
return false;
}
return info.State == MemCommit;
return info.State == HostRegionState.Committed;
}
private static bool HasRequiredProtection(uint protect, bool writeAccess)
@@ -6661,15 +6994,4 @@ public static class KernelMemoryCompatExports
sum = left + right;
return sum >= left;
}
[SysAbiExport(
Nid = "KMcEa+rHsIo",
ExportName = "sceKernelMapMemory",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelMapMemory(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -3,6 +3,7 @@
using System.Collections.Concurrent;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -44,6 +45,8 @@ public static class KernelPthreadCompatExports
// type initializer that first runs on a guest thread and fail-fasts the CLR.
private static long _nextMutexWakeId;
private static long _nextCondWakeId;
private static readonly ConcurrentDictionary<ulong, long> _posixTimedWaitCounts = new();
private static readonly ConcurrentDictionary<ulong, byte> _reportedContendedMutexes = new();
private sealed class PthreadMutexState
{
@@ -137,7 +140,7 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PthreadYield(CpuContext ctx)
{
_ = ctx;
GuestThreadExecution.Scheduler?.Pump(ctx, "scePthreadYield");
Thread.Yield();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -351,29 +354,95 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PosixPthreadCondTimedwait(CpuContext ctx)
{
// POSIX passes `const struct timespec *abstime` (absolute deadline), not relative microseconds (#113).
var abstimeAddress = ctx[CpuRegister.Rdx];
if (abstimeAddress == 0)
var absoluteTimeAddress = ctx[CpuRegister.Rdx];
if (absoluteTimeAddress == 0 ||
!ctx.TryReadUInt64(absoluteTimeAddress, out var secondsRaw) ||
!ctx.TryReadUInt64(absoluteTimeAddress + sizeof(ulong), out var nanosecondsRaw))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadUInt64(abstimeAddress, out var deadlineSeconds) ||
!ctx.TryReadUInt64(abstimeAddress + 8, out var deadlineNanoseconds))
var deadlineSeconds = unchecked((long)secondsRaw);
var deadlineNanoseconds = unchecked((long)nanosecondsRaw);
if (deadlineSeconds < 0 || deadlineNanoseconds is < 0 or >= 1_000_000_000)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var nowMicroseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1000L;
var deadlineMicroseconds =
deadlineSeconds >= long.MaxValue / 1_000_000UL
? long.MaxValue
: (long)(deadlineSeconds * 1_000_000UL + Math.Min(deadlineNanoseconds, 999_999_999UL) / 1000UL);
var remainingMicroseconds = deadlineMicroseconds - nowMicroseconds;
var timeoutUsec = remainingMicroseconds <= 0
? 1u
: (uint)Math.Min(remainingMicroseconds, uint.MaxValue);
return PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: timeoutUsec);
long nowSeconds;
long nowNanoseconds;
if (deadlineSeconds >= 1_000_000_000)
{
var now = DateTimeOffset.UtcNow;
nowSeconds = now.ToUnixTimeSeconds();
nowNanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
}
else
{
// Engine condition variables use monotonic deadlines.
KernelRuntimeCompatExports.GetProcessMonotonicTime(out nowSeconds, out nowNanoseconds);
}
var remainingNanoseconds =
((Int128)deadlineSeconds * 1_000_000_000 + deadlineNanoseconds) -
((Int128)nowSeconds * 1_000_000_000 + nowNanoseconds);
var timeoutUsec = remainingNanoseconds <= 0
? 0u
: (uint)Int128.Min(uint.MaxValue, (remainingNanoseconds + 999) / 1_000);
TracePosixTimedWait(
ctx,
deadlineSeconds,
deadlineNanoseconds,
nowSeconds,
nowNanoseconds,
remainingNanoseconds,
timeoutUsec);
return PthreadCondWaitCore(
ctx,
ctx[CpuRegister.Rdi],
ctx[CpuRegister.Rsi],
timed: true,
timeoutUsec,
posixResult: true);
}
private static void TracePosixTimedWait(
CpuContext ctx,
long deadlineSeconds,
long deadlineNanoseconds,
long nowSeconds,
long nowNanoseconds,
Int128 remainingNanoseconds,
uint timeoutUsec)
{
var condAddress = ctx[CpuRegister.Rdi];
var count = _posixTimedWaitCounts.AddOrUpdate(condAddress, 1, static (_, current) => current + 1);
if (count != 1 && count % 1_000_000 != 0)
{
return;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_cond_timedwait#{count}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
$"cond=0x{condAddress:X16} mutex=0x{ctx[CpuRegister.Rsi]:X16} abstime=0x{ctx[CpuRegister.Rdx]:X16} " +
$"deadline={deadlineSeconds}.{deadlineNanoseconds:D9} now={nowSeconds}.{nowNanoseconds:D9} " +
$"remaining_ns={remainingNanoseconds} timeout_us={timeoutUsec} ret=0x{returnRip:X16}");
if (GuestThreadExecution.Scheduler is not { } scheduler)
{
return;
}
foreach (var snapshot in scheduler.SnapshotThreads())
{
Console.Error.WriteLine(
$"[LOADER][DIAG] guest_thread handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
}
}
[SysAbiExport(
@@ -654,11 +723,12 @@ public static class KernelPthreadCompatExports
var acquired = state.Semaphore.Wait(0);
if (!acquired)
{
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
// Cooperative deschedule on a contended lock corrupts fiber state
// (Demon's Souls sceFiberSwitch -> ESRCH). Off by default: fall through to the
// synchronous host-thread wait below.
if (_enableMutexLockBlocking &&
// Fibers retain the synchronous fallback to preserve switch state.
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
if (canCooperativelyBlock &&
!tryOnly &&
GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
@@ -697,6 +767,53 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceContendedMutex(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
ulong currentThreadId)
{
if (!_reportedContendedMutexes.TryAdd(resolvedAddress, 0))
{
return;
}
ulong ownerThreadId;
int recursionCount;
int type;
lock (state)
{
ownerThreadId = state.OwnerThreadId;
recursionCount = state.RecursionCount;
type = state.Type;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_mutex_contended: waiter=0x{currentThreadId:X16} owner=0x{ownerThreadId:X16} " +
$"mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} recursion={recursionCount} " +
$"type={type} semaphore_count={state.Semaphore.CurrentCount} ret=0x{returnRip:X16}");
if (GuestThreadExecution.Scheduler is not { } scheduler)
{
return;
}
foreach (var snapshot in scheduler.SnapshotThreads())
{
if (snapshot.ThreadHandle != currentThreadId && snapshot.ThreadHandle != ownerThreadId)
{
continue;
}
Console.Error.WriteLine(
$"[LOADER][DIAG] pthread_mutex_party handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
}
}
private static int PthreadMutexUnlockCore(CpuContext ctx, ulong mutexAddress, bool requireOwner)
{
if (mutexAddress == 0)
@@ -1076,6 +1193,13 @@ public static class KernelPthreadCompatExports
lock (_stateGate)
{
if (_condStates.TryGetValue(condAddress, out var raced))
{
resolvedAddress = condAddress;
state = raced;
return true;
}
_condStates[condAddress] = createdState;
_condStates[handle] = createdState;
}
@@ -1172,7 +1296,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int PthreadCondWaitCore(CpuContext ctx, ulong condAddress, ulong mutexAddress, bool timed, uint timeoutUsec = 0)
private static int PthreadCondWaitCore(
CpuContext ctx,
ulong condAddress,
ulong mutexAddress,
bool timed,
uint timeoutUsec = 0,
bool posixResult = false)
{
if (condAddress == 0 || mutexAddress == 0)
{
@@ -1210,9 +1340,10 @@ public static class KernelPthreadCompatExports
if (consumedPendingSignal)
{
// Leave SyncRoot before relocking the mutex to avoid lock-order inversion with cond-signal (#113).
state.Waiters = Math.Max(0, state.Waiters - 1);
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
// Relock outside SyncRoot to preserve lock ordering.
Monitor.Exit(state.SyncRoot);
try
{
@@ -1221,6 +1352,7 @@ public static class KernelPthreadCompatExports
}
finally
{
// Balance the surrounding lock statement.
Monitor.Enter(state.SyncRoot);
}
}
@@ -1231,7 +1363,7 @@ public static class KernelPthreadCompatExports
ctx,
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
state.WakeKey,
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion),
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult),
() => state.SignalEpoch != observedEpoch,
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
{
@@ -1315,7 +1447,7 @@ public static class KernelPthreadCompatExports
state,
timed,
waitResult);
return waitResult;
return TranslatePthreadResult(waitResult, posixResult);
}
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
@@ -1372,7 +1504,8 @@ public static class KernelPthreadCompatExports
PthreadCondState state,
ulong observedEpoch,
bool timed,
int releasedRecursion)
int releasedRecursion,
bool posixResult)
{
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
lock (state.SyncRoot)
@@ -1393,7 +1526,32 @@ public static class KernelPthreadCompatExports
}
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
return lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult;
return TranslatePthreadResult(
lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult,
posixResult);
}
private static int TranslatePthreadResult(int result, bool posixResult)
{
if (!posixResult || result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return result;
}
return result switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => 1, // EPERM
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND => 2, // ENOENT
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => 22, // EINVAL
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK => 11, // EDEADLK
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => 13, // EACCES
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY => 16, // EBUSY
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => 35, // EAGAIN
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => 60, // ETIMEDOUT
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => 85, // ECANCELED
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => 14, // EFAULT
_ => result
};
}
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
@@ -1559,7 +1717,10 @@ public static class KernelPthreadCompatExports
return TimeSpan.Zero;
}
return TimeSpan.FromTicks((long)timeoutUsec * 10L);
// Round positive sub-millisecond waits to the host sleep quantum.
return timeoutUsec < 1_000
? TimeSpan.FromMilliseconds(1)
: TimeSpan.FromTicks((long)timeoutUsec * 10L);
}
private static TimeSpan GetCondSpuriousWakeTimeout()
@@ -1701,7 +1862,8 @@ public static class KernelPthreadCompatExports
}
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_cond_{operation}: cond=0x{condAddress:X16} mutex=0x{mutexAddress:X16} " +
$"[LOADER][TRACE] pthread_cond_{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} " +
$"cond=0x{condAddress:X16} key={state?.WakeKey ?? "none"} mutex=0x{mutexAddress:X16} " +
$"waiters={(state?.Waiters ?? 0)} epoch=0x{(state?.SignalEpoch ?? 0):X} timed={timed} result=0x{unchecked((uint)result):X8}");
}
@@ -382,6 +382,47 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "oIRFTjoILbg",
ExportName = "scePthreadSetschedparam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSetschedparam(CpuContext ctx) => PosixPthreadSetschedparam(ctx);
[SysAbiExport(
Nid = "P41kTWUS3EI",
ExportName = "scePthreadGetschedparam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetschedparam(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var outPolicyAddress = ctx[CpuRegister.Rsi];
var outSchedParamAddress = ctx[CpuRegister.Rdx];
if (thread == 0 || outPolicyAddress == 0 || outSchedParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
int policy;
int priority;
lock (_stateGate)
{
var state = GetOrCreateThreadStateLocked(thread);
policy = state.Attributes.SchedPolicy;
priority = state.Priority;
}
if (!ctx.TryWriteInt32(outPolicyAddress, policy) ||
!ctx.TryWriteInt32(outSchedParamAddress, priority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nsYoNRywwNg",
ExportName = "scePthreadAttrInit",
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Fiber;
using System.Buffers.Binary;
using System.Diagnostics;
@@ -48,9 +49,6 @@ public static class KernelRuntimeCompatExports
private const int MapFlagFixed = 0x10;
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
private const int AioInitParamSize = 0x3C;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint PageExecuteReadWrite = 0x40;
private static readonly object _stateGate = new();
private static readonly long _processStartCounter = Stopwatch.GetTimestamp();
private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader();
@@ -85,22 +83,29 @@ public static class KernelRuntimeCompatExports
ExportName = "sceKernelNanosleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelNanosleep(CpuContext ctx)
public static int KernelNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: false);
[SysAbiExport(
Nid = "yS8U2TGCe1A",
ExportName = "nanosleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixNanosleep(CpuContext ctx) => NanosleepCore(ctx, posix: true);
private static int NanosleepCore(CpuContext ctx, bool posix)
{
var requestAddress = ctx[CpuRegister.Rdi];
var remainAddress = ctx[CpuRegister.Rsi];
if (requestAddress == 0)
{
ctx[CpuRegister.Rax] = Einval;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> timespecBuffer = stackalloc byte[16];
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
{
ctx[CpuRegister.Rax] = Efault;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return NanosleepFailure(ctx, posix, Efault, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
@@ -108,8 +113,7 @@ public static class KernelRuntimeCompatExports
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
{
ctx[CpuRegister.Rax] = Einval;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return NanosleepFailure(ctx, posix, Einval, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (tvSec == 0 && tvNsec == 0)
@@ -119,7 +123,7 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep");
GuestThreadExecution.Scheduler?.Pump(ctx, posix ? "nanosleep" : "sceKernelNanosleep");
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
// a single tick rather than collapsing to a zero-length (no-op) sleep.
@@ -138,6 +142,21 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int NanosleepFailure(CpuContext ctx, bool posix, int errnoValue, OrbisGen2Result sceResult)
{
if (posix)
{
TrySetErrno(ctx, errnoValue);
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
}
else
{
ctx[CpuRegister.Rax] = unchecked((ulong)errnoValue);
}
return (int)sceResult;
}
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
{
if (remainAddress == 0)
@@ -192,6 +211,13 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "QcteRwbsnV0",
ExportName = "usleep",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixUsleep(CpuContext ctx) => KernelUsleep(ctx);
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
{
if (!_traceUsleep)
@@ -403,6 +429,18 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "HoLVWNanBBc",
ExportName = "getpid",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int GetProcessId(CpuContext ctx)
{
var processId = Environment.ProcessId;
ctx[CpuRegister.Rax] = unchecked((uint)processId);
return processId;
}
[SysAbiExport(
Nid = "fgxnMeTNUtY",
ExportName = "sceKernelGetProcessTimeCounter",
@@ -1853,7 +1891,7 @@ public static class KernelRuntimeCompatExports
try
{
nint stubAddress = VirtualAlloc(nint.Zero, (nuint)16, MemCommit | MemReserve, PageExecuteReadWrite);
nint stubAddress = unchecked((nint)HostPlatform.Current.Memory.Allocate(0, 16, HostPageProtection.ReadWriteExecute));
if (stubAddress == 0)
{
return null;
@@ -1883,9 +1921,6 @@ public static class KernelRuntimeCompatExports
}
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
private static bool TryReserveVirtualRange(
CpuContext ctx,
ulong desiredAddress,
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Diagnostics;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
@@ -12,6 +13,9 @@ public static class KernelSemaphoreCompatExports
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
private static int _nextSemaphoreHandle = 1;
[ThreadStatic]
private static int _semaPollBackoffCount;
private sealed class KernelSemaphoreState
{
public required string Name { get; init; }
@@ -28,6 +32,7 @@ public static class KernelSemaphoreCompatExports
{
public required int NeedCount { get; init; }
public required int CancelEpochAtBlock { get; init; }
public bool Timed { get; init; }
// Written and read only under the owning semaphore's Gate.
public int? Result { get; set; }
@@ -120,6 +125,7 @@ public static class KernelSemaphoreCompatExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var pollTimedOut = false;
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
@@ -131,36 +137,82 @@ public static class KernelSemaphoreCompatExports
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt32(timeoutAddress, out _))
if (!ctx.TryReadUInt32(timeoutAddress, out var timeoutMicros))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
if (timeoutMicros == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
pollTimedOut = true;
}
else
{
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
var timedWaiter = new SemaphoreWaiter
{
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
Timed = true,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline),
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
blockDeadlineTimestamp: deadline))
{
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
// Host-owned threads cannot park in the guest scheduler; degrade to the
// immediate-timeout poll the callers already tolerate.
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
pollTimedOut = true;
}
}
var waiter = new SemaphoreWaiter
if (!pollTimedOut)
{
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
};
if (!GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
var waiter = new SemaphoreWaiter
{
NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch,
};
if (!GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
GetSemaphoreWakeKey(handle),
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelWaitSema");
if ((++_semaPollBackoffCount & 255) == 0)
{
Thread.Sleep(0);
}
else
{
Thread.Yield();
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
[SysAbiExport(
@@ -368,6 +420,42 @@ public static class KernelSemaphoreCompatExports
}
}
private static int CompleteBlockedTimedSemaWait(
CpuContext ctx,
KernelSemaphoreState semaphore,
SemaphoreWaiter waiter,
ulong timeoutAddress,
long deadlineTimestamp)
{
int result;
lock (semaphore.Gate)
{
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
{
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
result = waiter.Result!.Value;
}
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(uint.MaxValue, remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
}
return result;
}
private static void TraceSemaphore(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
@@ -0,0 +1,558 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
using System.Text;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
internal static class KernelSocketCompatExports
{
private sealed class EmulatedSocketState
{
public TcpClient? Client;
public NetworkStream? Stream;
public IPAddress BoundAddress = IPAddress.Any;
public int BoundPort;
public bool Bound;
public bool Connected;
}
private static readonly object Gate = new();
private static readonly Dictionary<int, EmulatedSocketState> Sockets = new();
internal static bool IsEmulatedSocketFd(int fd)
{
lock (Gate)
{
return Sockets.ContainsKey(fd);
}
}
internal static bool TryCloseSocketFd(int fd)
{
lock (Gate)
{
if (!Sockets.Remove(fd, out var state))
{
return false;
}
DisposeEmulatedSocket(state);
return true;
}
}
internal static bool TryReadSocketFd(
CpuContext ctx,
int fd,
ulong bufferAddress,
int requested,
out ulong bytesRead,
out OrbisGen2Result error)
{
bytesRead = 0;
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
if (!TryGetEmulatedSocketState(fd, out var state) ||
state is null ||
!state.Connected ||
state.Stream is null)
{
return false;
}
var socketBuffer = GC.AllocateUninitializedArray<byte>(requested);
int socketRead;
try
{
socketRead = state.Stream.Read(socketBuffer, 0, requested);
}
catch (IOException)
{
return false;
}
if (socketRead > 0 && !ctx.Memory.TryWrite(bufferAddress, socketBuffer.AsSpan(0, socketRead)))
{
error = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return true;
}
bytesRead = unchecked((ulong)socketRead);
error = OrbisGen2Result.ORBIS_GEN2_OK;
return true;
}
internal static bool TryWriteSocketFd(
CpuContext ctx,
int fd,
byte[] payload,
out OrbisGen2Result error)
{
error = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
if (!TryGetEmulatedSocketState(fd, out var state) ||
state is null ||
!state.Connected ||
state.Stream is null)
{
return false;
}
try
{
state.Stream.Write(payload, 0, payload.Length);
state.Stream.Flush();
}
catch (IOException)
{
return false;
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
Console.Out.Write(Encoding.UTF8.GetString(payload));
Console.Out.Flush();
}
error = OrbisGen2Result.ORBIS_GEN2_OK;
return true;
}
[SysAbiExport(
Nid = "TU-d9PfIHPM",
ExportName = "socket",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Socket(CpuContext ctx)
{
var fd = KernelMemoryCompatExports.AllocateGuestFileDescriptor();
lock (Gate)
{
Sockets[fd] = new EmulatedSocketState();
}
ctx[CpuRegister.Rax] = unchecked((ulong)fd);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "XVL8So3QJUk",
ExportName = "connect",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Connect(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
if (!TryGetEmulatedSocketState(fd, out _))
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
{
LogNet($"connect sockaddr parse failed: fd={fd} addr=0x{sockaddrAddress:X} len={addrlen}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var redirectApplied = TryApplyNetRedirect(ref ipAddress);
if (redirectApplied)
{
LogNet($"connect redirect: fd={fd} ip={ipAddress} port={port}");
}
if (!IsGuestTcpOutboundAllowed(ipAddress, redirectApplied))
{
LogNet($"connect denied by outbound policy: fd={fd} ip={ipAddress} port={port}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryEstablishHostTcpConnection(ipAddress, port, out var client, out var stream))
{
LogNet($"connect failed: fd={fd} ip={ipAddress} port={port}");
RemoveEmulatedSocketFd(fd);
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
LogNet($"connect ok: fd={fd} ip={ipAddress} port={port}");
lock (Gate)
{
if (!Sockets.TryGetValue(fd, out var state) || state is null)
{
try { stream.Dispose(); } catch (IOException) { }
try { client.Dispose(); } catch (IOException) { }
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
DisposeEmulatedSocket(state);
state.Client = client;
state.Stream = stream;
state.Connected = true;
state.BoundAddress = ipAddress;
state.BoundPort = port;
state.Bound = true;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "KuOmgKoqCdY",
ExportName = "bind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Bind(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlen = unchecked((int)ctx[CpuRegister.Rdx]);
if (!TryGetEmulatedSocketState(fd, out var state) || state is null)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryParseGuestSockaddrIn(sockaddrAddress, addrlen, ctx, out var ipAddress, out var port))
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
state.BoundAddress = ipAddress;
state.BoundPort = port;
state.Bound = true;
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "RenI1lL1WFk",
ExportName = "getsockname",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Getsockname(CpuContext ctx)
{
var fd = unchecked((int)ctx[CpuRegister.Rdi]);
var sockaddrAddress = ctx[CpuRegister.Rsi];
var addrlenAddress = ctx[CpuRegister.Rdx];
if (!TryGetEmulatedSocketState(fd, out var state) || state is null || !state.Bound)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> addrlenBuffer = stackalloc byte[4];
if (!ctx.Memory.TryRead(addrlenAddress, addrlenBuffer))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var addrlen = BinaryPrimitives.ReadInt32LittleEndian(addrlenBuffer);
if (addrlen < 8)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> sockaddr = stackalloc byte[16];
sockaddr[0] = 16;
sockaddr[1] = 2;
BinaryPrimitives.WriteUInt16BigEndian(sockaddr.Slice(2, 2), (ushort)state.BoundPort);
var addressBytes = state.BoundAddress.GetAddressBytes();
if (addressBytes.Length == 4)
{
addressBytes.CopyTo(sockaddr.Slice(4, 4));
}
var writeLength = Math.Min(addrlen, 16);
if (!ctx.Memory.TryWrite(sockaddrAddress, sockaddr.Slice(0, writeLength)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
BinaryPrimitives.WriteInt32LittleEndian(addrlenBuffer, writeLength);
if (!ctx.Memory.TryWrite(addrlenAddress, addrlenBuffer))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "9oiX1kyeedA",
ExportName = "bzero",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Bzero(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
var length = unchecked((int)ctx[CpuRegister.Rsi]);
if (length > 0 && address != 0)
{
var zeros = new byte[length];
if (!ctx.Memory.TryWrite(address, zeros))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4n51s0zEf0c",
ExportName = "inet_pton",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int InetPton(CpuContext ctx)
{
var af = unchecked((int)ctx[CpuRegister.Rdi]);
var srcAddress = ctx[CpuRegister.Rsi];
var dstAddress = ctx[CpuRegister.Rdx];
if (af != 2 || srcAddress == 0 || dstAddress == 0)
{
ctx[CpuRegister.Rax] = unchecked((ulong)0xFFFFFFFFFFFFFFFF);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadCString(srcAddress, ctx, out var text) ||
!TryParseIpv4Address(text, out var octets))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
Span<byte> packed = stackalloc byte[4];
packed[0] = octets[0];
packed[1] = octets[1];
packed[2] = octets[2];
packed[3] = octets[3];
if (!ctx.Memory.TryWrite(dstAddress, packed))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 1;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jogUIsOV3-U",
ExportName = "htons",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Htons(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
var swapped = (ushort)(((value & 0x00FF) << 8) | ((value >> 8) & 0x00FF));
ctx[CpuRegister.Rax] = swapped;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryGetEmulatedSocketState(int fd, out EmulatedSocketState? state)
{
lock (Gate)
{
return Sockets.TryGetValue(fd, out state);
}
}
private static bool TryParseGuestSockaddrIn(
ulong address,
int addrlen,
CpuContext ctx,
out IPAddress ipAddress,
out int port)
{
ipAddress = IPAddress.None;
port = 0;
if (address == 0 || addrlen < 8)
{
return false;
}
Span<byte> buffer = stackalloc byte[16];
var readLength = Math.Min(addrlen, buffer.Length);
if (!ctx.Memory.TryRead(address, buffer.Slice(0, readLength)))
{
return false;
}
if (buffer[1] != 2)
{
return false;
}
port = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2, 2));
ipAddress = new IPAddress(buffer.Slice(4, 4).ToArray());
return true;
}
private static void DisposeEmulatedSocket(EmulatedSocketState state)
{
try { state.Stream?.Dispose(); } catch (IOException) { }
try { state.Client?.Dispose(); } catch (IOException) { }
state.Stream = null;
state.Client = null;
state.Connected = false;
}
private static void RemoveEmulatedSocketFd(int fd)
{
lock (Gate)
{
if (Sockets.Remove(fd, out var socketState))
{
DisposeEmulatedSocket(socketState);
}
}
}
private static void LogNet(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][DEBUG] {message}");
}
}
private static bool TryApplyNetRedirect(ref IPAddress ipAddress)
{
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
if (string.IsNullOrWhiteSpace(redirect))
{
return false;
}
if (!IPAddress.TryParse(redirect.Trim(), out var redirectAddress))
{
return false;
}
ipAddress = redirectAddress;
return true;
}
private static bool IsNetRedirectConfigured()
{
var redirect = Environment.GetEnvironmentVariable("SHARPEMU_NET_REDIRECT");
return !string.IsNullOrWhiteSpace(redirect);
}
private static bool IsGuestTcpOutboundAllowed(IPAddress ipAddress, bool redirectApplied)
{
return redirectApplied || IsNetRedirectConfigured() || IPAddress.IsLoopback(ipAddress);
}
private static bool TryEstablishHostTcpConnection(
IPAddress ipAddress,
int port,
out TcpClient client,
out NetworkStream stream)
{
client = null!;
stream = null!;
if (!TryConnectTcpClient(ipAddress, port, out client))
{
return false;
}
stream = client.GetStream();
return true;
}
private static bool TryConnectTcpClient(IPAddress ipAddress, int port, out TcpClient client)
{
client = new TcpClient();
try
{
var connectTask = client.ConnectAsync(ipAddress, port);
if (!connectTask.Wait(TimeSpan.FromMilliseconds(500)))
{
client.Dispose();
client = null!;
return false;
}
return true;
}
catch (SocketException)
{
client.Dispose();
client = null!;
return false;
}
catch (IOException)
{
client.Dispose();
client = null!;
return false;
}
}
private static bool TryReadCString(ulong address, CpuContext ctx, out string text)
{
const int maxLength = 64;
var buffer = new byte[maxLength];
var length = 0;
for (; length < maxLength; length++)
{
if (!ctx.Memory.TryRead(address + (ulong)length, buffer.AsSpan(length, 1)))
{
text = string.Empty;
return false;
}
if (buffer[length] == 0)
{
break;
}
}
text = Encoding.ASCII.GetString(buffer, 0, length);
return true;
}
private static bool TryParseIpv4Address(string text, out byte[] octets)
{
octets = Array.Empty<byte>();
var parts = text.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length != 4)
{
return false;
}
var parsed = new byte[4];
for (var i = 0; i < 4; i++)
{
if (!byte.TryParse(parts[i], out parsed[i]))
{
return false;
}
}
octets = parsed;
return true;
}
}
@@ -3,15 +3,12 @@
using SharpEmu.HLE;
using System;
using System.Collections.Concurrent;
using System.Reflection;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
internal static class KernelVirtualRangeAllocator
{
private static readonly ConcurrentDictionary<Type, Accessor> _accessors = new();
public static bool TryReserve(
CpuContext ctx,
ulong desiredAddress,
@@ -31,38 +28,24 @@ internal static class KernelVirtualRangeAllocator
try
{
if (!TryResolveAccessor(ctx.Memory, out var target, out var accessor))
if (!TryResolveAddressSpace(ctx.Memory, out var addressSpace))
{
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
return false;
}
if (allowSearch && accessor.AllocateAtOrAbove is not null)
if (allowSearch &&
addressSpace.TryAllocateAtOrAbove(desiredAddress, length, executable, alignment, out var searchedAddress) &&
searchedAddress != 0)
{
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs);
if (searchResult is bool trueValue && trueValue &&
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
{
mappedAddress = searchedAddress;
return true;
}
mappedAddress = searchedAddress;
return true;
}
if (accessor.AllocateAt is null)
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
if (allocated == 0)
{
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {target.GetType().FullName}");
return false;
}
var invokeArgs = accessor.AllocateAtHasAllowAlternativeArg
? new object[] { desiredAddress, length, executable, allowAllocateAtAlternative }
: new object[] { desiredAddress, length, executable };
var result = accessor.AllocateAt.Invoke(target, invokeArgs);
if (result is not ulong allocated || allocated == 0)
{
var resultType = result?.GetType().FullName ?? "null";
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {resultType} value={result ?? "null"}");
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {typeof(ulong).FullName} value=0");
return false;
}
@@ -76,84 +59,36 @@ internal static class KernelVirtualRangeAllocator
}
}
private static bool TryResolveAccessor(object rootMemory, out object target, out Accessor accessor)
/// <summary>
/// Finds the <see cref="IGuestAddressSpace"/> behind <paramref name="rootMemory"/>,
/// unwrapping decorators (bounded, like the reflection walker this replaced).
/// </summary>
public static bool TryResolveAddressSpace(ICpuMemory rootMemory, [NotNullWhen(true)] out IGuestAddressSpace? addressSpace)
{
target = rootMemory;
accessor = default;
var target = rootMemory;
for (var depth = 0; depth < 4; depth++)
{
accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor);
if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null)
if (target is IGuestAddressSpace resolved)
{
addressSpace = resolved;
return true;
}
if (accessor.InnerProperty is null)
if (target is not ICpuMemoryWrapper wrapper)
{
break;
}
var innerValue = accessor.InnerProperty.GetValue(target);
if (innerValue is null || ReferenceEquals(innerValue, target))
var inner = wrapper.Inner;
if (inner is null || ReferenceEquals(inner, target))
{
break;
}
target = innerValue;
target = inner;
}
addressSpace = null;
return false;
}
private static Accessor DiscoverAccessor(Type type)
{
MethodInfo? allocateAt = null;
MethodInfo? allocateAtOrAbove = null;
var allocateAtHasAllowAlternativeArg = false;
foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
var parameters = candidate.GetParameters();
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
parameters.Length == 5 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool) &&
parameters[3].ParameterType == typeof(ulong) &&
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
{
allocateAtOrAbove = candidate;
}
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
{
if (parameters.Length == 3 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = false;
}
else if (parameters.Length == 4 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool) &&
parameters[3].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = true;
}
}
}
var innerProperty = type.GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
return new Accessor(allocateAt, allocateAtOrAbove, allocateAtHasAllowAlternativeArg, innerProperty);
}
private readonly record struct Accessor(
MethodInfo? AllocateAt,
MethodInfo? AllocateAtOrAbove,
bool AllocateAtHasAllowAlternativeArg,
PropertyInfo? InnerProperty);
}
+1 -1
View File
@@ -19,7 +19,7 @@ public static class LibcInternalExports
[SysAbiExport(
Nid = "NWtTN10cJzE",
ExportName = "LibcHeapGetTraceInfo",
ExportName = "sceLibcHeapGetTraceInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "LibcInternalExt")]
public static int LibcHeapGetTraceInfo(CpuContext ctx)
+353
View File
@@ -338,6 +338,359 @@ public static class LibcStdioExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8Q60JLJ6Rv4",
ExportName = "getc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Getc(CpuContext ctx) => FgetcCore(ctx);
[SysAbiExport(
Nid = "AEuF3F2f8TA",
ExportName = "fgetc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fgetc(CpuContext ctx) => FgetcCore(ctx);
private static int FgetcCore(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (!_fileHandles.TryGetValue(handle, out var stream))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
try
{
var value = stream.ReadByte();
ctx[CpuRegister.Rax] = value < 0 ? unchecked((ulong)(-1L)) : (ulong)value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "LxcEU+ICu8U",
ExportName = "feof",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Feof(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var atEnd = _fileHandles.TryGetValue(handle, out var stream) &&
stream.CanRead && stream.Position >= stream.Length;
ctx[CpuRegister.Rax] = atEnd ? 1UL : 0UL;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "AHxyhN96dy4",
ExportName = "ferror",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Ferror(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "3QIPIh-GDjw",
ExportName = "rewind",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Rewind(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.Seek(0, SeekOrigin.Begin);
}
catch (IOException)
{
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "aZK8lNei-Qw",
ExportName = "fputc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fputc(CpuContext ctx)
{
var character = unchecked((byte)ctx[CpuRegister.Rdi]);
var handle = ctx[CpuRegister.Rsi];
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.WriteByte(character);
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
else
{
// Unknown streams are the bundled libc's real stdout/stderr FILE objects;
// mirror the fputs HLE and forward them to the host console.
Console.Out.Write((char)character);
}
ctx[CpuRegister.Rax] = character;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MpxhMh8QFro",
ExportName = "fwrite",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fwrite(CpuContext ctx)
{
var source = ctx[CpuRegister.Rdi];
var elementSize = ctx[CpuRegister.Rsi];
var elementCount = ctx[CpuRegister.Rdx];
var handle = ctx[CpuRegister.Rcx];
if (elementSize == 0 || elementCount == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var knownHandle = _fileHandles.TryGetValue(handle, out var stream);
if (source == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var totalRequested = elementSize * elementCount;
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
ulong totalWritten = 0;
try
{
while (totalWritten < totalRequested)
{
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalWritten);
if (!ctx.Memory.TryRead(source + totalWritten, buffer.AsSpan(0, request)))
{
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (knownHandle)
{
stream!.Write(buffer, 0, request);
}
else
{
Console.Out.Write(System.Text.Encoding.UTF8.GetString(buffer, 0, request));
}
totalWritten += (ulong)request;
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
ctx[CpuRegister.Rax] = totalWritten / elementSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MUjC4lbHrK4",
ExportName = "fflush",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fflush(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
try
{
if (handle == 0)
{
foreach (var stream in _fileHandles.Values)
{
stream.Flush();
}
}
else if (_fileHandles.TryGetValue(handle, out var stream))
{
stream.Flush();
}
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fffwELXNVFA",
ExportName = "fprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Fprintf(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
return WriteRenderedText(ctx, handle, rendered);
}
[SysAbiExport(
Nid = "pDBDcY6uLSA",
ExportName = "vfprintf",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Vfprintf(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var formatAddress = ctx[CpuRegister.Rsi];
var vaListAddress = ctx[CpuRegister.Rdx];
if (!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, formatAddress, MaxPathLength, out var format))
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
KernelMemoryCompatExports.TryFormatStringFromVaList(ctx, format, vaListAddress, out var rendered);
return WriteRenderedText(ctx, handle, rendered);
}
private static int WriteRenderedText(CpuContext ctx, ulong handle, string rendered)
{
var payload = System.Text.Encoding.UTF8.GetBytes(rendered);
if (_fileHandles.TryGetValue(handle, out var stream))
{
try
{
stream.Write(payload, 0, payload.Length);
}
catch (IOException)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
else
{
Console.Out.Write(rendered);
}
ctx[CpuRegister.Rax] = (ulong)payload.Length;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "gkWgn0p1AfU",
ExportName = "freopen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int Freopen(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var modeAddress = ctx[CpuRegister.Rsi];
var handle = ctx[CpuRegister.Rdx];
if (pathAddress == 0 || modeAddress == 0 || handle == 0 ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (_fileHandles.TryRemove(handle, out var previousStream))
{
previousStream.Dispose();
}
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
try
{
if (fileAccess != FileAccess.Read)
{
var parentDirectory = Path.GetDirectoryName(hostPath);
if (!string.IsNullOrWhiteSpace(parentDirectory))
{
Directory.CreateDirectory(parentDirectory);
}
}
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
// freopen keeps the caller's FILE* identity, so rebind the same handle value.
_fileHandles[handle] = stream;
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X}");
}
ctx[CpuRegister.Rax] = handle;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
if (_traceStdio)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] freopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
}
ctx[CpuRegister.Rax] = 0;
return ex is UnauthorizedAccessException
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
}
[SysAbiExport(
Nid = "sUP1hBaouOw",
ExportName = "_Getpctype",
+11
View File
@@ -7,6 +7,17 @@ namespace SharpEmu.Libs.Mouse;
public static class MouseExports
{
[SysAbiExport(
Nid = "Qs0wWulgl7U",
ExportName = "sceMouseInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceMouse")]
public static int MouseInit(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
// as an sceNgs2VoiceGetState alias.
[SysAbiExport(
@@ -146,6 +146,13 @@ public static class NetCtlExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
}
[SysAbiExport(
Nid = "1NE9OWdBIww",
ExportName = "sceNetCtlRegisterCallbackV6",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlRegisterCallbackV6(CpuContext ctx) => NetCtlRegisterCallback(ctx);
[SysAbiExport(
Nid = "obuxdTiwkF8",
ExportName = "sceNetCtlGetInfo",
+48
View File
@@ -95,6 +95,54 @@ public static class NetExports
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "9T2pDF2Ryqg",
ExportName = "sceNetHtonl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetHtonl(CpuContext ctx)
{
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "iWQWrwiSt8A",
ExportName = "sceNetHtons",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetHtons(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "pQGpHYopAIY",
ExportName = "sceNetNtohl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetNtohl(CpuContext ctx)
{
var value = unchecked((uint)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "Rbvt+5Y2iEw",
ExportName = "sceNetNtohs",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetNtohs(CpuContext ctx)
{
var value = unchecked((ushort)ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = BinaryPrimitives.ReverseEndianness(value);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "C4UgDHHPvdw",
ExportName = "sceNetResolverCreate",
+21
View File
@@ -237,6 +237,27 @@ public static class Ngs2Exports
LibraryName = "libSceNgs2")]
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "1WsleK-MTkE",
ExportName = "sceNgs2GeomCalcListener",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomCalcListener(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "0lbbayqDNoE",
ExportName = "sceNgs2GeomResetSourceParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomResetSourceParam(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "7Lcfo8SmpsU",
ExportName = "sceNgs2GeomResetListenerParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNgs2")]
public static int Ngs2GeomResetListenerParam(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "i0VnXM-C9fc",
ExportName = "sceNgs2SystemRender",
@@ -0,0 +1,34 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Np;
// Stub for sce::Np::CppWebApi: titles abort PS5-component startup if
// Common::initialize returns a negative SCE error, so no-op success is required to boot.
public static class NpCppWebApiExports
{
[SysAbiExport(
Nid = "UYPxv8MIzGo",
ExportName = "_ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpCppWebApi")]
public static int CppWebApiCommonInitialize(CpuContext ctx)
{
// int Common::initialize(const InitParams&, LibContext&) — 0 on success.
TraceCppWebApi("common_initialize", ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi]);
return ctx.SetReturn(0);
}
private static void TraceCppWebApi(string operation, ulong arg0, ulong arg1)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] np_cppwebapi.{operation} arg0=0x{arg0:X16} arg1=0x{arg1:X16}");
}
}
+39 -2
View File
@@ -22,6 +22,17 @@ public static class NpManagerExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "S7QTn72PrDw",
ExportName = "sceNpDeleteRequest",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpDeleteRequest(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JELHf4xPufo",
ExportName = "sceNpCheckCallbackForLib",
@@ -42,8 +53,19 @@ public static class NpManagerExports
LibraryName = "libSceNpManager")]
public static int NpGetOnlineId(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
// Gen5 ABI: user ID, then output structure.
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
}
[SysAbiExport(
Nid = "rbknaUjpqWo",
ExportName = "sceNpGetAccountIdA",
Target = Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetOnlineIdA(CpuContext ctx)
{
// The A variant takes a user ID before OnlineId.
return WriteOfflineOnlineId(ctx, ctx[CpuRegister.Rsi]);
}
[SysAbiExport(
@@ -158,4 +180,19 @@ public static class NpManagerExports
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}");
}
private static int WriteOfflineOnlineId(CpuContext ctx, ulong address)
{
if (address == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// SceNpOnlineId is a 16-byte handle plus four trailing bytes.
Span<byte> onlineId = stackalloc byte[20];
"Player"u8.CopyTo(onlineId);
return ctx.Memory.TryWrite(address, onlineId)
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
@@ -9,7 +9,10 @@ namespace SharpEmu.Libs.Np;
public static class NpUniversalDataSystemExports
{
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
private static readonly object _eventGate = new();
private static readonly HashSet<int> _createdEvents = [];
private static int _nextHandle = 1;
private static int _nextEvent = 1;
[SysAbiExport(
Nid = "sjaobBgqeB4",
@@ -67,6 +70,114 @@ public static class NpUniversalDataSystemExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
[SysAbiExport(
Nid = "p+GcLqwpL9M",
ExportName = "sceNpUniversalDataSystemCreateEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemCreateEvent(CpuContext ctx)
{
var parameterAddress = ctx[CpuRegister.Rdi];
if (parameterAddress == 0)
{
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
}
var eventId = Interlocked.Increment(ref _nextEvent);
lock (_eventGate)
{
_createdEvents.Add(eventId);
}
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdx], eventId, checkNil: true) ||
ctx.TryWriteInt32(ctx[CpuRegister.Rcx], eventId, checkNil: true))
{
return ctx.SetReturn(0, typeof(long));
}
lock (_eventGate)
{
_createdEvents.Remove(eventId);
}
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
[SysAbiExport(
Nid = "wG+84pnNIuo",
ExportName = "sceNpUniversalDataSystemDestroyEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemDestroyEvent(CpuContext ctx)
{
var eventId = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_eventGate)
{
_createdEvents.Remove(eventId);
}
return ctx.SetReturn(0, typeof(long));
}
[SysAbiExport(
Nid = "MfDb+4Nln64",
ExportName = "sceNpUniversalDataSystemEventPropertyObjectSetString",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemEventPropertyObjectSetString(CpuContext ctx)
{
var propertyObjectAddress = ctx[CpuRegister.Rsi];
var valueAddress = ctx[CpuRegister.Rdx];
if (propertyObjectAddress == 0 || valueAddress == 0)
{
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
}
Span<byte> probe = stackalloc byte[1];
return ctx.Memory.TryRead(propertyObjectAddress, probe) &&
ctx.Memory.TryRead(valueAddress, probe)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
[SysAbiExport(
Nid = "Wxbg5x3pTXA",
ExportName = "sceNpUniversalDataSystemEventPropertyObjectSetArray",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemEventPropertyObjectSetArray(CpuContext ctx)
{
var propertyObjectAddress = ctx[CpuRegister.Rsi];
var valueAddress = ctx[CpuRegister.Rdx];
if (propertyObjectAddress == 0)
{
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
}
Span<byte> probe = stackalloc byte[1];
if (!ctx.Memory.TryRead(propertyObjectAddress, probe))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
if (valueAddress != 0 && !ctx.Memory.TryRead(valueAddress, probe))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
return ctx.SetReturn(0, typeof(long));
}
[SysAbiExport(
Nid = "CzkKf7ahIyU",
ExportName = "sceNpUniversalDataSystemPostEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemPostEvent(CpuContext ctx)
{
return ctx.SetReturn(0, typeof(long));
}
[SysAbiExport(
Nid = "tpFJ8LIKvPw",
ExportName = "sceNpUniversalDataSystemRegisterContext",
+12
View File
@@ -31,6 +31,18 @@ public static class NpWebApi2Exports
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "WV1GwM32NgY",
ExportName = "sceNpWebApi2PushEventCreateHandle",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpWebApi2")]
public static int NpWebApi2InitializeAlt(CpuContext ctx)
{
Interlocked.Exchange(ref _initialized, 1);
TraceNpWebApi2("init-alt", unchecked((int)ctx[CpuRegister.Rdi]), ctx[CpuRegister.Rsi]);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "bEvXpcEk200",
ExportName = "sceNpWebApi2Terminate",

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