Compare commits

...

12 Commits

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

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

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

* [Host] Abstract pad and keyboard input behind IHostInput

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* [Host] Route PhysicalVirtualMemory through IHostMemory

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* [Vulkan] Execute translated draws with multiple color attachments

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

* [ShaderDump] Add MRT edge-case coverage

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

---------

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

* [Json] Add Initializer::setGlobalNullAccessCallback

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 01:49:33 +03:00
Randomuser8219 4c35831cb8 Add code -1073741819 as an emulation error (#188)
There's currently games that crash on this code due to emulation errors, so it'd make sense to add this error code as an emulation error.
2026-07-15 01:47:44 +03:00
93 changed files with 6246 additions and 1942 deletions
+3
View File
@@ -100,6 +100,9 @@ jobs:
- name: Build solution - name: Build solution
run: dotnet build SharpEmu.slnx -c Release --no-restore 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 - 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}" 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.Fonts.Inter" Version="11.3.18" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" /> <PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
<PackageVersion Include="Iced" Version="1.21.0" /> <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" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" 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.Vulkan.Extensions.KHR" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" /> <PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 --> <!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" /> <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> </ItemGroup>
</Project> </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.Libs/SharpEmu.Libs.csproj" />
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" /> <Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
</Folder> </Folder>
<Folder Name="/tests/">
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
</Folder>
</Solution> </Solution>
+6 -2
View File
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory; using SharpEmu.Core.Memory;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging; using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu; namespace SharpEmu.Core.Cpu;
@@ -41,16 +42,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
]; ];
private readonly IVirtualMemory _virtualMemory; private readonly IVirtualMemory _virtualMemory;
private readonly IModuleManager _moduleManager; private readonly IModuleManager _moduleManager;
private readonly IHostPlatform? _hostPlatform;
private INativeCpuBackend? _nativeCpuBackend; private INativeCpuBackend? _nativeCpuBackend;
public CpuDispatcher( public CpuDispatcher(
IVirtualMemory virtualMemory, IVirtualMemory virtualMemory,
IModuleManager moduleManager, IModuleManager moduleManager,
INativeCpuBackend? nativeCpuBackend = null) INativeCpuBackend? nativeCpuBackend = null,
IHostPlatform? hostPlatform = null)
{ {
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory)); _virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager)); _moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
_nativeCpuBackend = nativeCpuBackend; _nativeCpuBackend = nativeCpuBackend;
_hostPlatform = hostPlatform;
} }
public ulong? LastEntryPoint { get; private set; } public ulong? LastEntryPoint { get; private set; }
@@ -266,7 +270,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
entryFrameDiagnostic, entryFrameDiagnostic,
Environment.NewLine, Environment.NewLine,
"CpuEngine: native-only"); "CpuEngine: native-only");
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager); _nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
if (_nativeCpuBackend.TryExecute( if (_nativeCpuBackend.TryExecute(
context, context,
entryPoint, entryPoint,
@@ -8,6 +8,7 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging; using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu.Native; namespace SharpEmu.Core.Cpu.Native;
@@ -134,8 +135,9 @@ public sealed partial class DirectExecutionBackend
int num2 = 0; int num2 = 0;
List<ulong> list = new List<ulong>(16); List<ulong> list = new List<ulong>(16);
ulong num3 = scanStart; ulong num3 = scanStart;
MEMORY_BASIC_INFORMATION64 lpBuffer; var hostMemory = ResolveDiagnosticsHostMemory();
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0) HostRegionInfo lpBuffer;
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
{ {
ulong baseAddress = lpBuffer.BaseAddress; ulong baseAddress = lpBuffer.BaseAddress;
ulong num4 = baseAddress + lpBuffer.RegionSize; ulong num4 = baseAddress + lpBuffer.RegionSize;
@@ -145,7 +147,7 @@ public sealed partial class DirectExecutionBackend
} }
ulong value = Math.Max(num3, baseAddress); ulong value = Math.Max(num3, baseAddress);
ulong num5 = Math.Min(num4, scanEnd); 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); ulong num6 = AlignUp(value, 8uL);
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8) for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
@@ -350,7 +352,7 @@ public sealed partial class DirectExecutionBackend
{ {
return false; return false;
} }
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0) if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
{ {
return false; return false;
} }
@@ -359,7 +361,7 @@ public sealed partial class DirectExecutionBackend
{ {
return false; return false;
} }
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect)) if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
{ {
return false; return false;
} }
@@ -391,12 +393,12 @@ public sealed partial class DirectExecutionBackend
return true; return true;
} }
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0) if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
{ {
return false; return false;
} }
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect); var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
if (executable) if (executable)
{ {
_knownExecutablePages.TryAdd(pageAddress, 0); _knownExecutablePages.TryAdd(pageAddress, 0);
@@ -415,6 +417,14 @@ public sealed partial class DirectExecutionBackend
return (value + num) & ~num; 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) private static bool IsReadableProtection(uint protect)
{ {
if ((protect & 0x100) != 0 || (protect & 1) != 0) if ((protect & 0x100) != 0 || (protect & 1) != 0)
@@ -9,7 +9,9 @@ using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using SharpEmu.Core.Cpu.Disasm; using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Cpu.Native.Windows;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native; 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)) if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
{ {
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged); _rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
if (_rawExceptionHandlerStub == 0) if (_rawExceptionHandlerStub == 0)
{ {
throw new InvalidOperationException("Failed to create raw exception handler trampoline"); 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}"); Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
} }
else else
@@ -37,22 +39,22 @@ public sealed partial class DirectExecutionBackend
_handlerDelegate = VectoredHandler; _handlerDelegate = VectoredHandler;
_handlerHandle = GCHandle.Alloc(_handlerDelegate); _handlerHandle = GCHandle.Alloc(_handlerDelegate);
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate)); _exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
if (_exceptionHandlerStub == 0) if (_exceptionHandlerStub == 0)
{ {
throw new InvalidOperationException("Failed to create exception handler trampoline"); 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}"); Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter; _unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate); _unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate)); _unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
if (_unhandledFilterStub == 0) if (_unhandledFilterStub == 0)
{ {
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline"); throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
} }
SetUnhandledExceptionFilter(_unhandledFilterStub); _faultHandling.SetUnhandledFilter(_unhandledFilterStub);
} }
private unsafe int UnhandledExceptionFilter(void* exceptionInfo) private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
@@ -60,8 +62,8 @@ public sealed partial class DirectExecutionBackend
try try
{ {
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord; EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248); ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152); ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired."); Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}"); Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}"); Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
@@ -100,8 +102,8 @@ public sealed partial class DirectExecutionBackend
return 0; return 0;
} }
ulong rip = ReadCtxU64(contextRecord, 248); ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
ulong rsp = ReadCtxU64(contextRecord, 152); ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
// Thread-mode probe: a hardware exception raised while this thread is inside // Thread-mode probe: a hardware exception raised while this thread is inside
// the managed import gateway means the VEH->managed reentry happened from // 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}"); $"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; return -1;
} }
@@ -127,10 +129,10 @@ public sealed partial class DirectExecutionBackend
switch (exceptionCode) switch (exceptionCode)
{ {
case 3221225477u: case WindowsFaultCodes.AccessViolation:
LogAccessViolationTrace(exceptionAddress, exceptionRecord); LogAccessViolationTrace(exceptionAddress, exceptionRecord);
break; break;
case 3221226505u: case WindowsFaultCodes.FastFail:
{ {
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0; ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 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 rax = ReadCtxU64(contextRecord, CTX_RAX);
ulong rbx = ReadCtxU64(contextRecord, 144); ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
ulong rcx = ReadCtxU64(contextRecord, 128); ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
ulong rdx = ReadCtxU64(contextRecord, 136); ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
ulong rsi = ReadCtxU64(contextRecord, 168); ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
ulong rdi = ReadCtxU64(contextRecord, 176); ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
ulong rbp = ReadCtxU64(contextRecord, 160); ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
ulong r8 = ReadCtxU64(contextRecord, 184); ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
ulong r9 = ReadCtxU64(contextRecord, 192); ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
ulong r10 = ReadCtxU64(contextRecord, 200); ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
ulong r11 = ReadCtxU64(contextRecord, 208); ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
ulong r12 = ReadCtxU64(contextRecord, 216); ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
ulong r13 = ReadCtxU64(contextRecord, 224); ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
ulong r14 = ReadCtxU64(contextRecord, 232); ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
ulong r15 = ReadCtxU64(contextRecord, 240); ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
Console.Error.WriteLine("[LOADER][INFO] ========================================="); Console.Error.WriteLine("[LOADER][INFO] =========================================");
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!"); Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
@@ -185,7 +187,7 @@ public sealed partial class DirectExecutionBackend
ulong accessType = 0; ulong accessType = 0;
ulong target = 0; ulong target = 0;
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2) if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
{ {
accessType = *exceptionRecord->ExceptionInformation; accessType = *exceptionRecord->ExceptionInformation;
target = exceptionRecord->ExceptionInformation[1]; 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 access: " + accessText);
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}"); 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) switch (exceptionCode)
{ {
case 3221225477u: case WindowsFaultCodes.AccessViolation:
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation"); Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
Console.Error.WriteLine("[LOADER][ERROR] This usually means:"); Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import"); Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
@@ -295,11 +297,11 @@ public sealed partial class DirectExecutionBackend
DumpGuestReferenceDiagnostics(); DumpGuestReferenceDiagnostics();
DumpGuestPointerWindowDiagnostics(); DumpGuestPointerWindowDiagnostics();
break; break;
case 2147483651u: case WindowsFaultCodes.Breakpoint:
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)"); Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode"); Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
break; break;
case 3221225501u: case WindowsFaultCodes.IllegalInstruction:
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction"); Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
break; break;
} }
@@ -332,8 +334,8 @@ public sealed partial class DirectExecutionBackend
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo; EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
EXCEPTION_RECORD* record = pointers->ExceptionRecord; EXCEPTION_RECORD* record = pointers->ExceptionRecord;
void* contextRecord = pointers->ContextRecord; void* contextRecord = pointers->ContextRecord;
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0; ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0; ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0; ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0; ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
Console.Error.WriteLine( Console.Error.WriteLine(
@@ -479,7 +481,7 @@ public sealed partial class DirectExecutionBackend
ulong address = scanBase; ulong address = scanBase;
while (address < scanEnd) while (address < scanEnd)
{ {
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0) if (!_hostMemory.Query(address, out var mbi))
{ {
break; break;
} }
@@ -491,9 +493,9 @@ public sealed partial class DirectExecutionBackend
break; break;
} }
if (mbi.State == MEM_COMMIT && if (mbi.State == HostRegionState.Committed &&
IsReadableProtection(mbi.Protect) && IsReadableProtection(mbi.RawProtection) &&
IsExecutableProtection(mbi.Protect)) IsExecutableProtection(mbi.RawProtection))
{ {
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget); ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
} }
@@ -798,13 +800,13 @@ public sealed partial class DirectExecutionBackend
return false; return false;
} }
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0) if (!_hostMemory.Query(address, out var mbi))
{ {
return false; return false;
} }
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize; 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; return false;
} }
@@ -916,25 +918,25 @@ public sealed partial class DirectExecutionBackend
{ {
return false; return false;
} }
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0) if (!_hostMemory.Query(faultAddress, out var mbi))
{ {
return false; return false;
} }
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL; ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect); uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount); int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex); bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
if (traceLazyCommit) 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) 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; return true;
} }
@@ -943,10 +945,10 @@ public sealed partial class DirectExecutionBackend
ulong committedBase = 0; ulong committedBase = 0;
ulong committedSize = 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) && 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; committed = true;
committedBase = windowBase; committedBase = windowBase;
@@ -955,7 +957,7 @@ public sealed partial class DirectExecutionBackend
else else
{ {
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL; ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect)) if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{ {
committed = true; committed = true;
committedBase = largeBase; committedBase = largeBase;
@@ -966,13 +968,13 @@ public sealed partial class DirectExecutionBackend
if (!committed) if (!committed)
{ {
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL; ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect)) if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
{ {
committed = true; committed = true;
committedBase = region64kBase; committedBase = region64kBase;
committedSize = 65536uL; committedSize = 65536uL;
} }
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect)) else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
{ {
committed = true; committed = true;
committedBase = pageBase; committedBase = pageBase;
@@ -985,7 +987,7 @@ public sealed partial class DirectExecutionBackend
return false; return false;
} }
TryCommitRange(pageBase + 4096, 4096uL, commitProtect); TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
if (traceLazyCommit) 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}"); 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; return true;
} }
if (mbi.State != 8192) if (mbi.State != HostRegionState.Reserved)
{ {
return false; return false;
} }
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) && if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect)) TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
{ {
committed = true; committed = true;
committedBase = commitWindowBase; committedBase = commitWindowBase;
@@ -1008,7 +1010,7 @@ public sealed partial class DirectExecutionBackend
else else
{ {
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL; ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect)) if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
{ {
committed = true; committed = true;
committedBase = largeCommitBase; committedBase = largeCommitBase;
@@ -1019,19 +1021,19 @@ public sealed partial class DirectExecutionBackend
if (!committed) if (!committed)
{ {
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL; ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(region64kBase, 65536uL, commitProtect)) if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
{ {
committed = true; committed = true;
committedBase = region64kBase; committedBase = region64kBase;
committedSize = 65536uL; committedSize = 65536uL;
} }
else if (TryCommitRange(pageBase, 8192uL, commitProtect)) else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
{ {
committed = true; committed = true;
committedBase = pageBase; committedBase = pageBase;
committedSize = 8192uL; committedSize = 8192uL;
} }
else if (TryCommitRange(pageBase, 4096uL, commitProtect)) else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
{ {
committed = true; committed = true;
committedBase = pageBase; committedBase = pageBase;
@@ -1044,7 +1046,7 @@ public sealed partial class DirectExecutionBackend
return false; return false;
} }
TryCommitRange(pageBase + 4096, 4096uL, commitProtect); TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
if (traceLazyCommit) if (traceLazyCommit)
{ {
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}"); 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; 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) if (length == 0)
{ {
return false; 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) if (length == 0)
{ {
return false; 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 false;
} }
return TryCommitRange(commitAddress, commitSize, protection); return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
} }
static bool IsAccessCompatible(ulong accessType, uint protection) static bool IsAccessCompatible(ulong accessType, uint protection)
@@ -8,8 +8,10 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using SharpEmu.Core.Cpu; using SharpEmu.Core.Cpu;
using SharpEmu.Core.Cpu.Native.Windows;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native; namespace SharpEmu.Core.Cpu.Native;
@@ -69,23 +71,23 @@ public sealed partial class DirectExecutionBackend
private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo) private unsafe static int TryRecoverUnresolvedSentinel(void* exceptionInfo)
{ {
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord; EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
if (exceptionRecord->ExceptionCode != 3221225477u) if (exceptionRecord->ExceptionCode != WindowsFaultCodes.AccessViolation)
{ {
return 0; return 0;
} }
void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord; void* contextRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
ulong value = ReadCtxU64(contextRecord, 248); ulong value = ReadCtxU64(contextRecord, CTX_RIP);
ulong value2 = (ulong)exceptionRecord->ExceptionAddress; ulong value2 = (ulong)exceptionRecord->ExceptionAddress;
if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2)) if (!IsUnresolvedSentinel(value) && !IsUnresolvedSentinel(value2))
{ {
return 0; return 0;
} }
ulong rsp = ReadCtxU64(contextRecord, 152); ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
WriteCtxU64(contextRecord, 120, 0uL); WriteCtxU64(contextRecord, CTX_RAX, 0uL);
if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp)) if (TryGetPlausibleReturnFromStack(rsp, out var returnRip, out var nextRsp))
{ {
WriteCtxU64(contextRecord, 152, nextRsp); WriteCtxU64(contextRecord, CTX_RSP, nextRsp);
WriteCtxU64(contextRecord, 248, returnRip); WriteCtxU64(contextRecord, CTX_RIP, returnRip);
Interlocked.Increment(ref _rawSentinelRecoveries); Interlocked.Increment(ref _rawSentinelRecoveries);
if (LogThreadMode) if (LogThreadMode)
{ {
@@ -533,8 +535,7 @@ public sealed partial class DirectExecutionBackend
out var blockContinuation, out var blockContinuation,
out var hasBlockContinuation, out var hasBlockContinuation,
out var blockWakeKey, out var blockWakeKey,
out var blockResumeHandler, out var blockWaiter,
out var blockWakeHandler,
out var blockDeadlineTimestamp) && out var blockDeadlineTimestamp) &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason)) TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
{ {
@@ -544,8 +545,7 @@ public sealed partial class DirectExecutionBackend
GuestThreadExecution.CurrentGuestThreadHandle, GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation, blockContinuation,
blockWakeKey, blockWakeKey,
blockResumeHandler, blockWaiter,
blockWakeHandler,
blockDeadlineTimestamp); blockDeadlineTimestamp);
} }
@@ -676,8 +676,7 @@ public sealed partial class DirectExecutionBackend
out var blockContinuation, out var blockContinuation,
out var hasBlockContinuation, out var hasBlockContinuation,
out var blockWakeKey, out var blockWakeKey,
out var blockResumeHandler, out var blockWaiter,
out var blockWakeHandler,
out var blockDeadlineTimestamp) && out var blockDeadlineTimestamp) &&
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason)) TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
{ {
@@ -687,8 +686,7 @@ public sealed partial class DirectExecutionBackend
GuestThreadExecution.CurrentGuestThreadHandle, GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation, blockContinuation,
blockWakeKey, blockWakeKey,
blockResumeHandler, blockWaiter,
blockWakeHandler,
blockDeadlineTimestamp); blockDeadlineTimestamp);
} }
@@ -1725,9 +1723,9 @@ public sealed partial class DirectExecutionBackend
{ {
var candidateBase = ImportStubRegionCanonicalBase - var candidateBase = ImportStubRegionCanonicalBase -
(ulong)candidateIndex * ImportStubRegionAddressStride; (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.RegionSize == 0 ||
memoryInfo.State != 4096) memoryInfo.State != HostRegionState.Committed)
{ {
continue; continue;
} }
@@ -1968,23 +1966,36 @@ public sealed partial class DirectExecutionBackend
return false; return false;
} }
List<byte> list = new List<byte>(Math.Min(maxLength, 256)); // Reads stay byte-by-byte through TryReadByteCompat (its Marshal.ReadByte
Span<byte> destination = stackalloc byte[1]; // fallback must probe exactly up to the terminator), but the bytes land in a
for (int i = 0; i < maxLength; i++) // stack buffer instead of a List<byte> + ToArray per symbol resolution.
const int StackBufferLength = 512;
byte[]? rented = maxLength > StackBufferLength ? System.Buffers.ArrayPool<byte>.Shared.Rent(maxLength) : null;
Span<byte> buffer = rented is null ? stackalloc byte[StackBufferLength] : rented;
try
{ {
if (!TryReadByteCompat(address + (ulong)i, destination)) for (int i = 0; i < maxLength; i++)
{ {
return false; if (!TryReadByteCompat(address + (ulong)i, buffer.Slice(i, 1)))
{
return false;
}
if (buffer[i] == 0)
{
value = System.Text.Encoding.ASCII.GetString(buffer[..i]);
return true;
}
}
value = System.Text.Encoding.ASCII.GetString(buffer[..maxLength]);
return true;
}
finally
{
if (rented is not null)
{
System.Buffers.ArrayPool<byte>.Shared.Return(rented);
} }
if (destination[0] == 0)
{
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
return true;
}
list.Add(destination[0]);
} }
value = System.Text.Encoding.ASCII.GetString(list.ToArray());
return true;
} }
private bool TryReadByteCompat(ulong address, Span<byte> destination) private bool TryReadByteCompat(ulong address, Span<byte> destination)
@@ -2066,7 +2077,7 @@ public sealed partial class DirectExecutionBackend
uint flNewProtect = default(uint); uint flNewProtect = default(uint);
try 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; return;
} }
@@ -2074,7 +2085,7 @@ public sealed partial class DirectExecutionBackend
{ {
Marshal.WriteByte(num2 + i, 144); Marshal.WriteByte(num2 + i, 144);
} }
FlushInstructionCache(GetCurrentProcess(), (void*)num, 5u); _hostMemory.FlushInstructionCache((ulong)(void*)num, 5u);
_patchedEa020eLookupCall = true; _patchedEa020eLookupCall = true;
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5"); Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched hash-lookup call at 0x{num:X16} -> NOP*5");
} }
@@ -2085,7 +2096,7 @@ public sealed partial class DirectExecutionBackend
{ {
if (flNewProtect != 0) 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.Runtime.InteropServices;
using System.Threading; using System.Threading;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native; namespace SharpEmu.Core.Cpu.Native;
@@ -35,20 +36,6 @@ public sealed partial class DirectExecutionBackend
private bool _nativeWorkersDisposed; private bool _nativeWorkersDisposed;
private int _nativeWorkerCreationFailedLogged; 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 // 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; falls back to the historical inline calli (guest frames above this
// thread's managed frames) when workers are disabled or unavailable. // thread's managed frames) when workers are disabled or unavailable.
@@ -61,7 +48,7 @@ public sealed partial class DirectExecutionBackend
var worker = RentNativeGuestExecutor(); var worker = RentNativeGuestExecutor();
if (worker is null) if (worker is null)
{ {
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot); _hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
return CallNativeEntry(entryStub); return CallNativeEntry(entryStub);
} }
try try
@@ -229,7 +216,7 @@ public sealed partial class DirectExecutionBackend
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend) public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
{ {
if (!EnsureKernel32Exports()) if (!EnsureHostRuntimeExports(backend._hostSymbols))
{ {
return null; return null;
} }
@@ -242,32 +229,27 @@ public sealed partial class DirectExecutionBackend
return executor; return executor;
} }
private static bool EnsureKernel32Exports() private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
{ {
if (_exitThreadAddress != 0) if (_exitThreadAddress != 0)
{ {
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0; return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
} }
nint kernel32 = GetModuleHandle("kernel32.dll"); _waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
if (kernel32 == 0) _setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
{ _exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
return false;
}
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0; return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
} }
private bool Initialize() private bool Initialize()
{ {
_selfHandle = GCHandle.Alloc(this); _selfHandle = GCHandle.Alloc(this);
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u); _controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
if (_controlBlock == null) if (_controlBlock == null)
{ {
return false; return false;
} }
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u); _loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
if (_loopStub == null) if (_loopStub == null)
{ {
return false; return false;
@@ -349,17 +331,15 @@ public sealed partial class DirectExecutionBackend
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int)); *(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
uint oldProtect = 0; uint oldProtect = 0;
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect)) if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
{ {
return false; return false;
} }
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize); _backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
_threadHandle = CreateThread( _threadHandle = _backend._hostThreading.CreateNativeThread(
0,
WorkerStackReservation,
(nint)_loopStub, (nint)_loopStub,
0, 0,
StackSizeParamIsAReservation, WorkerStackReservation,
out _nativeThreadId); out _nativeThreadId);
if (_threadHandle == 0) if (_threadHandle == 0)
{ {
@@ -465,7 +445,7 @@ public sealed partial class DirectExecutionBackend
_prevYieldRequested = _activeGuestThreadYieldRequested; _prevYieldRequested = _activeGuestThreadYieldRequested;
_prevYieldReason = _activeGuestThreadYieldReason; _prevYieldReason = _activeGuestThreadYieldReason;
_prevState = _activeGuestThreadState; _prevState = _activeGuestThreadState;
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex); _prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle); _prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
_entered = true; _entered = true;
_activeExecutionBackend = backend; _activeExecutionBackend = backend;
@@ -477,11 +457,11 @@ public sealed partial class DirectExecutionBackend
_activeGuestThreadYieldReason = null; _activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState; _activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!); backend.BindTlsBase(_runContext!);
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot); backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (_runState is { } state) if (_runState is { } state)
{ {
_prevHostThreadId = Volatile.Read(ref state.HostThreadId); _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) if (_runAffinityMask != 0)
{ {
@@ -511,7 +491,7 @@ public sealed partial class DirectExecutionBackend
{ {
Volatile.Write(ref state.HostThreadId, _prevHostThreadId); Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
} }
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot); _backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle); GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
_activeExecutionBackend = _prevBackend; _activeExecutionBackend = _prevBackend;
_activeCpuContext = _prevContext; _activeCpuContext = _prevContext;
@@ -548,8 +528,8 @@ public sealed partial class DirectExecutionBackend
var exited = _threadHandle == 0; var exited = _threadHandle == 0;
if (_threadHandle != 0) if (_threadHandle != 0)
{ {
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u; exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
CloseHandle(_threadHandle); _backend._hostThreading.CloseThreadHandle(_threadHandle);
_threadHandle = 0; _threadHandle = 0;
} }
if (!exited) if (!exited)
@@ -563,12 +543,12 @@ public sealed partial class DirectExecutionBackend
} }
if (_loopStub != null) if (_loopStub != null)
{ {
VirtualFree(_loopStub, 0u, 32768u); _backend._hostMemory.Free((ulong)_loopStub);
_loopStub = null; _loopStub = null;
} }
if (_controlBlock != null) if (_controlBlock != null)
{ {
VirtualFree(_controlBlock, 0u, 32768u); _backend._hostMemory.Free((ulong)_controlBlock);
_controlBlock = null; _controlBlock = null;
} }
if (_selfHandle.IsAllocated) 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 System.Runtime.InteropServices;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native; namespace SharpEmu.Core.Cpu.Native;
@@ -11,17 +12,15 @@ public sealed unsafe class StubManager : IDisposable
private readonly List<nint> _allocatedStubs = new(); private readonly List<nint> _allocatedStubs = new();
private readonly Dictionary<string, nint> _importHandlers = new(); private readonly Dictionary<string, nint> _importHandlers = new();
private readonly Dictionary<ulong, nint> _stubAddresses = new(); private readonly Dictionary<ulong, nint> _stubAddresses = new();
private readonly IHostMemory _hostMemory;
private byte* _pltMemory; private byte* _pltMemory;
private int _pltOffset; private int _pltOffset;
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
public StubManager() public StubManager(IHostMemory? hostMemory = null)
{ {
_pltMemory = (byte*)VirtualAlloc( _hostMemory = hostMemory ?? HostPlatform.Current.Memory;
null, _pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
(nuint)PltMemorySize,
AllocationType.Reserve | AllocationType.Commit,
MemoryProtection.ExecuteReadWrite);
if (_pltMemory == null) if (_pltMemory == null)
{ {
@@ -185,7 +184,7 @@ public sealed unsafe class StubManager : IDisposable
{ {
if (_pltMemory != null) if (_pltMemory != null)
{ {
VirtualFree(_pltMemory, 0, FreeType.Release); _hostMemory.Free((ulong)_pltMemory);
_pltMemory = null; _pltMemory = null;
} }
@@ -194,29 +193,5 @@ public sealed unsafe class StubManager : IDisposable
_stubAddresses.Clear(); _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); public delegate void ImportHandler(CpuContext context);
} }
@@ -0,0 +1,32 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Byte offsets into the Win64 CONTEXT record delivered to vectored exception
/// handlers. The handlers read/write guest registers directly at these offsets
/// (no managed CONTEXT struct exists); a future POSIX backend gets a sibling
/// class for its mcontext layout.
/// </summary>
internal static class Win64ContextOffsets
{
public const int Mxcsr = 52;
public const int Rax = 120;
public const int Rcx = 128;
public const int Rdx = 136;
public const int Rbx = 144;
public const int Rsp = 152;
public const int Rbp = 160;
public const int Rsi = 168;
public const int Rdi = 176;
public const int R8 = 184;
public const int R9 = 192;
public const int R10 = 200;
public const int R11 = 208;
public const int R12 = 216;
public const int R13 = 224;
public const int R14 = 232;
public const int R15 = 240;
public const int Rip = 248;
}
@@ -0,0 +1,24 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Windows NTSTATUS exception codes and EXCEPTION_RECORD access-type values the
/// fault handlers filter on. Values are the same numbers the handlers previously
/// compared as bare literals; only the spelling changed.
/// </summary>
internal static class WindowsFaultCodes
{
public const uint AccessViolation = 0xC0000005u; // 3221225477
public const uint Breakpoint = 0x80000003u; // 2147483651
public const uint IllegalInstruction = 0xC000001Du; // 3221225501
public const uint FastFail = 0xC0000409u; // 3221226505
public const uint StackOverflow = 0xC00000FDu;
public const uint ClrManagedException = 0xE0434352u;
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
public const ulong AccessRead = 0;
public const ulong AccessWrite = 1;
public const ulong AccessExecute = 8;
}
@@ -0,0 +1,191 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Vectored-exception-handler installation and the handler pre-filter thunk.
/// The thunk is inherently Windows-shaped (TEB stack-limit reads via gs:,
/// NTSTATUS pre-filtering, Win64 calling convention) and moved here whole from
/// DirectExecutionBackend; a POSIX backend supplies a sibling built around
/// sigaction/sigaltstack instead.
/// </summary>
internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
{
private readonly IHostMemory _memory;
public WindowsFaultHandling(IHostMemory memory)
{
_memory = memory;
}
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
{
const uint stubSize = 256u;
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
if (ptr == null)
{
return 0;
}
byte* code = (byte*)ptr;
int offset = 0;
// Native pre-filter: these exception codes are raised while the thread can be in
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
// code" — this is why no managed throw (even one with a catch handler) ever
// survived inside the emulator. Continue the handler search without touching
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
// returned CONTINUE_SEARCH for them.
ReadOnlySpan<uint> nonManagedExceptionCodes =
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
EmitByte(code, ref offset, 0x74); // je pass
passJumpOffsets[i] = offset;
EmitByte(code, ref offset, 0x00);
}
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
int passOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
EmitByte(code, ref offset, 0xC3); // ret
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
}
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, 8u);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x83); // jae guestStack
int aboveStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[0x10]
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, 0x10u);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x82); // jb guestStack
int belowStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = managedCallback;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xE9);
int hostRestoreJump = offset;
EmitUInt32(code, ref offset, 0u);
int guestStackOffset = offset;
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xB9);
EmitUInt32(code, ref offset, hostRspSwitchTlsSlot);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = tlsGetValueAddress;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
int missingTlsJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x18); // mov r11, [rax]
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xDB); // test r11, r11
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
int missingHostStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = managedCallback;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xE9);
int guestRestoreJump = offset;
EmitUInt32(code, ref offset, 0u);
int passThroughOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
int restoreOffset = offset;
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov rsp, r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
EmitByte(code, ref offset, 0xC3);
*(int*)(code + aboveStackJump) = guestStackOffset - (aboveStackJump + sizeof(int));
*(int*)(code + belowStackJump) = guestStackOffset - (belowStackJump + sizeof(int));
*(int*)(code + hostRestoreJump) = restoreOffset - (hostRestoreJump + sizeof(int));
*(int*)(code + missingTlsJump) = passThroughOffset - (missingTlsJump + sizeof(int));
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
if (!_memory.Protect((ulong)ptr, stubSize, HostPageProtection.ReadExecute, out _))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
_ = _memory.Free((ulong)ptr);
return 0;
}
_memory.FlushInstructionCache((ulong)ptr, (ulong)offset);
return (nint)ptr;
}
public void FreeThunk(nint thunk)
{
_ = _memory.Free((ulong)thunk);
}
public nint AddFirstChanceHandler(nint thunk)
{
return (nint)AddVectoredExceptionHandler(1u, thunk);
}
public void RemoveHandler(nint handle)
{
_ = RemoveVectoredExceptionHandler((void*)handle);
}
public void SetUnhandledFilter(nint thunk)
{
_ = SetUnhandledExceptionFilter(thunk);
}
private static void EmitByte(byte* code, ref int offset, byte value)
{
code[offset++] = value;
}
private static void EmitUInt32(byte* code, ref int offset, uint value)
{
*(uint*)(code + offset) = value;
offset += sizeof(uint);
}
[LibraryImport("kernel32.dll")]
private static partial void* AddVectoredExceptionHandler(uint first, IntPtr handler);
[LibraryImport("kernel32.dll")]
private static partial uint RemoveVectoredExceptionHandler(void* handle);
[LibraryImport("kernel32.dll")]
private static partial IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
}
+6 -1
View File
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu; namespace SharpEmu.Core.Cpu;
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator, ICpuMemoryWrapper
{ {
private readonly ICpuMemory _inner; private readonly ICpuMemory _inner;
@@ -50,4 +50,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
address = 0; address = 0;
return false; return false;
} }
public bool TryFreeGuestMemory(ulong address)
{
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
}
} }
+208 -95
View File
@@ -4,11 +4,12 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging; using SharpEmu.Logging;
namespace SharpEmu.Core.Memory; 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"); private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
@@ -28,41 +29,27 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
private const uint MEM_COMMIT = 0x1000; // Raw Windows PAGE_* values retained for the internal region/protection
private const uint MEM_RESERVE = 0x2000; // bookkeeping: regions and saved old-protection values always carry the raw
private const uint MEM_RELEASE = 0x8000; // 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_READ = 0x20;
private const uint PAGE_EXECUTE_READWRITE = 0x40; private const uint PAGE_EXECUTE_READWRITE = 0x40;
private const uint PAGE_EXECUTE = 0x10; private const uint PAGE_EXECUTE = 0x10;
private const uint PAGE_EXECUTE_WRITECOPY = 0x80; private const uint PAGE_EXECUTE_WRITECOPY = 0x80;
private const uint PAGE_NOACCESS = 0x01;
private const uint PAGE_READWRITE = 0x04; private const uint PAGE_READWRITE = 0x04;
private const uint PAGE_READONLY = 0x02; private const uint PAGE_READONLY = 0x02;
private readonly IHostMemory _hostMemory;
private ulong _guestAllocationArenaBase; private ulong _guestAllocationArenaBase;
private ulong _guestAllocationOffset; private readonly SortedDictionary<ulong, ulong> _guestAllocationFreeRanges = new();
private readonly Dictionary<ulong, (ulong Offset, ulong Size)> _guestAllocations = new();
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes(); private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
[DllImport("kernel32.dll", SetLastError = true)] public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect); {
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
[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 bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress) public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
{ {
@@ -74,17 +61,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var alignedSize = (size + 0xFFF) & ~0xFFFUL; var alignedSize = (size + 0xFFF) & ~0xFFFUL;
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE; var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
var allocationType = MEM_COMMIT | MEM_RESERVE; var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
var result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, allocationType, protection); var result = _hostMemory.Allocate(desiredAddress, alignedSize, hostProtection);
if (result == null) if (result == 0)
{ {
return false; return false;
} }
actualAddress = (ulong)result; actualAddress = result;
if (actualAddress != desiredAddress) if (actualAddress != desiredAddress)
{ {
VirtualFree(result, 0, MEM_RELEASE); _hostMemory.Free(result);
actualAddress = 0; actualAddress = 0;
return false; return false;
} }
@@ -119,33 +106,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var alignedSize = (size + 0xFFF) & ~0xFFFUL; var alignedSize = (size + 0xFFF) & ~0xFFFUL;
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE; 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 reservedOnly = false;
var preferReserveOnly = !executable && var preferReserveOnly = !executable &&
alignedSize >= LargeDataReserveThreshold && alignedSize >= LargeDataReserveThreshold &&
alignedSize > FullCommitRegionLimit; alignedSize > FullCommitRegionLimit;
void* result = null; ulong result = 0;
if (preferReserveOnly) if (preferReserveOnly)
{ {
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE); result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
if (result == null && allowAlternative) 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; 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) if (!allowAlternative)
{ {
@@ -153,32 +140,32 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address..."); TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection); result = _hostMemory.Allocate(0, alignedSize, hostProtection);
if (result == null) if (result == 0)
{ {
if (!executable) if (!executable)
{ {
result = VirtualAlloc((void*)desiredAddress, (nuint)alignedSize, MEM_RESERVE, PAGE_READWRITE); result = _hostMemory.Reserve(desiredAddress, alignedSize, HostPageProtection.ReadWrite);
if (result == null && allowAlternative) 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; reservedOnly = true;
} }
} }
if (result == null) if (result == 0)
{ {
throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory"); throw new OutOfMemoryException($"Failed to allocate {alignedSize} bytes of virtual memory");
} }
} }
} }
var actualAddress = (ulong)result; var actualAddress = result;
var lazyPrimeState = "n/a"; var lazyPrimeState = "n/a";
if (reservedOnly) if (reservedOnly)
@@ -191,9 +178,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{ {
var remaining = primeBytes - committedBytes; var remaining = primeBytes - committedBytes;
var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes); var chunkBytes = Math.Min(remaining, LazyReservePrimeChunkBytes);
var commitAddress = (void*)(actualAddress + committedBytes); var commitAddress = actualAddress + committedBytes;
var committed = VirtualAlloc(commitAddress, (nuint)chunkBytes, MEM_COMMIT, PAGE_READWRITE); if (!_hostMemory.Commit(commitAddress, chunkBytes, HostPageProtection.ReadWrite))
if (committed == null)
{ {
break; break;
} }
@@ -316,7 +302,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
GuestAllocationArenaSize, GuestAllocationArenaSize,
executable: false, executable: false,
allowAlternative: true); allowAlternative: true);
_guestAllocationOffset = GuestAllocationArenaStartOffset; _guestAllocationFreeRanges.Add(
GuestAllocationArenaStartOffset,
GuestAllocationArenaSize - GuestAllocationArenaStartOffset);
} }
catch (Exception) catch (Exception)
{ {
@@ -324,18 +312,128 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
} }
var alignedOffset = AlignUp(_guestAllocationOffset, alignment); ulong rangeOffset = 0;
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset) ulong rangeSize = 0;
ulong alignedOffset = 0;
var found = false;
foreach (var range in _guestAllocationFreeRanges)
{
alignedOffset = AlignUp(range.Key, alignment);
if (alignedOffset >= range.Key &&
alignedOffset - range.Key <= range.Value &&
size <= range.Value - (alignedOffset - range.Key))
{
rangeOffset = range.Key;
rangeSize = range.Value;
found = true;
break;
}
}
if (!found)
{ {
return false; return false;
} }
_guestAllocationFreeRanges.Remove(rangeOffset);
if (alignedOffset > rangeOffset)
{
_guestAllocationFreeRanges.Add(rangeOffset, alignedOffset - rangeOffset);
}
var allocationEnd = alignedOffset + size;
var rangeEnd = rangeOffset + rangeSize;
if (allocationEnd < rangeEnd)
{
_guestAllocationFreeRanges.Add(allocationEnd, rangeEnd - allocationEnd);
}
address = _guestAllocationArenaBase + alignedOffset; address = _guestAllocationArenaBase + alignedOffset;
_guestAllocationOffset = alignedOffset + size; _guestAllocations.Add(address, (alignedOffset, size));
return true; return true;
} }
} }
public bool TryFreeGuestMemory(ulong address)
{
lock (_guestAllocationGate)
{
if (!_guestAllocations.Remove(address, out var allocation))
{
return false;
}
var freeOffset = allocation.Offset;
var freeSize = allocation.Size;
ulong? previousOffset = null;
ulong? nextOffset = null;
foreach (var range in _guestAllocationFreeRanges)
{
if (range.Key < freeOffset)
{
previousOffset = range.Key;
continue;
}
nextOffset = range.Key;
break;
}
if (previousOffset is { } previous &&
previous + _guestAllocationFreeRanges[previous] == freeOffset)
{
freeOffset = previous;
freeSize += _guestAllocationFreeRanges[previous];
_guestAllocationFreeRanges.Remove(previous);
}
if (nextOffset is { } next && freeOffset + freeSize == next)
{
freeSize += _guestAllocationFreeRanges[next];
_guestAllocationFreeRanges.Remove(next);
}
_guestAllocationFreeRanges.Add(freeOffset, freeSize);
return true;
}
}
public bool TryProtect(ulong address, ulong size, GuestPageProtection protection)
{
if (size == 0)
{
return false;
}
return _hostMemory.Protect(address, size, ResolveProtection(protection), out _);
}
// Reproduces the decomposition KernelMemoryCompatExports.ResolveHostProtection
// performed before this seam existed; the Windows backend maps each case back
// to the identical PAGE_* value.
private static HostPageProtection ResolveProtection(GuestPageProtection protection)
{
var read = (protection & GuestPageProtection.Read) != 0;
var write = (protection & GuestPageProtection.Write) != 0;
var execute = (protection & GuestPageProtection.Execute) != 0;
if (execute)
{
return write
? HostPageProtection.ReadWriteExecute
: read
? HostPageProtection.ReadExecute
: HostPageProtection.Execute;
}
return write
? HostPageProtection.ReadWrite
: read
? HostPageProtection.ReadOnly
: HostPageProtection.NoAccess;
}
public void Clear() public void Clear()
{ {
lock (_guestAllocationGate) lock (_guestAllocationGate)
@@ -345,7 +443,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE); _hostMemory.Free(region.VirtualAddress);
} }
_regions.Clear(); _regions.Clear();
_pageProtections.Clear(); _pageProtections.Clear();
@@ -360,7 +458,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
_guestAllocationArenaBase = 0; _guestAllocationArenaBase = 0;
_guestAllocationOffset = 0; _guestAllocationFreeRanges.Clear();
_guestAllocations.Clear();
} }
} }
@@ -419,46 +518,67 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags) private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
{ {
var runStart = mapStart;
var runFlags = ProgramHeaderFlags.None;
var hasRun = false;
for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize) for (var pageAddress = mapStart; pageAddress < mapEnd; pageAddress += PageSize)
{ {
_pageProtections.TryGetValue(pageAddress, out var existingFlags); _pageProtections.TryGetValue(pageAddress, out var existingFlags);
var mergedFlags = existingFlags | flags; var mergedFlags = existingFlags | flags;
_pageProtections[pageAddress] = mergedFlags; _pageProtections[pageAddress] = mergedFlags;
SetProtection(pageAddress, PageSize, mergedFlags);
if (!hasRun)
{
runStart = pageAddress;
runFlags = mergedFlags;
hasRun = true;
}
else if (mergedFlags != runFlags)
{
SetProtection(runStart, pageAddress - runStart, runFlags);
runStart = pageAddress;
runFlags = mergedFlags;
}
}
if (hasRun)
{
SetProtection(runStart, mapEnd - runStart, runFlags);
} }
} }
private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags) private void SetProtection(ulong address, ulong size, ProgramHeaderFlags flags)
{ {
uint protection; HostPageProtection protection;
if (flags == ProgramHeaderFlags.None) if (flags == ProgramHeaderFlags.None)
{ {
protection = PAGE_NOACCESS; protection = HostPageProtection.NoAccess;
} }
else if ((flags & ProgramHeaderFlags.Execute) != 0) else if ((flags & ProgramHeaderFlags.Execute) != 0)
{ {
protection = (flags & ProgramHeaderFlags.Write) != 0 protection = (flags & ProgramHeaderFlags.Write) != 0
? PAGE_EXECUTE_READWRITE ? HostPageProtection.ReadWriteExecute
: PAGE_EXECUTE_READ; : HostPageProtection.ReadExecute;
} }
else if ((flags & ProgramHeaderFlags.Write) != 0) else if ((flags & ProgramHeaderFlags.Write) != 0)
{ {
protection = PAGE_READWRITE; protection = HostPageProtection.ReadWrite;
} }
else 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}"); throw new InvalidOperationException($"Failed to set memory protection at 0x{address:X16}");
} }
if ((flags & ProgramHeaderFlags.Execute) != 0) if ((flags & ProgramHeaderFlags.Execute) != 0)
{ {
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size); _hostMemory.FlushInstructionCache(address, size);
} }
} }
@@ -730,7 +850,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true; 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; return false;
} }
@@ -744,10 +864,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
finally finally
{ {
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _); _hostMemory.ProtectRaw((ulong)destPtr, (ulong)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect)) if (IsExecutableProtection(oldProtect))
{ {
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length); _hostMemory.FlushInstructionCache((ulong)destPtr, (ulong)source.Length);
} }
} }
@@ -769,9 +889,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock(); _gate.EnterReadLock();
try try
{ {
return FindRegion(virtualAddress, 1) is not null var region = FindRegion(virtualAddress, 1);
? (void*)virtualAddress if (region is null ||
: null; (region.IsReservedOnly && !EnsureRangeCommitted(virtualAddress, 1, region)))
{
return null;
}
return (void*)virtualAddress;
} }
finally finally
{ {
@@ -973,12 +1098,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE; 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) if (size == 0 || !region.IsReservedOnly)
{ {
@@ -992,7 +1117,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var pageAddress = startPage; var pageAddress = startPage;
while (pageAddress < endPage) while (pageAddress < endPage)
{ {
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0) if (!_hostMemory.Query(pageAddress, out var info))
{ {
return false; return false;
} }
@@ -1006,19 +1131,19 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false; return false;
} }
if (info.State == MEM_COMMIT) if (info.State == HostRegionState.Committed)
{ {
pageAddress = rangeEnd; pageAddress = rangeEnd;
continue; continue;
} }
if (info.State != MEM_RESERVE) if (info.State != HostRegionState.Reserved)
{ {
return false; return false;
} }
var commitSize = rangeEnd - pageAddress; var commitSize = rangeEnd - pageAddress;
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null) if (!_hostMemory.Commit(pageAddress, commitSize, commitProtection))
{ {
return false; return false;
} }
@@ -1039,11 +1164,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var startPage = AlignDown(address, PageSize); var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, 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) 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); RestorePageProtections(touchedPages);
touchedPages.Clear(); touchedPages.Clear();
@@ -1056,11 +1181,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true; 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) foreach (var (pageAddress, protection) in touchedPages)
{ {
VirtualProtect((void*)pageAddress, (nuint)PageSize, protection, out _); _hostMemory.ProtectRaw(pageAddress, PageSize, protection, out _);
} }
} }
@@ -1117,16 +1242,4 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public uint Protection { get; set; } public uint Protection { get; set; }
} }
private struct MemoryBasicInformation64
{
public ulong BaseAddress;
public ulong AllocationBase;
public uint AllocationProtect;
public uint Alignment1;
public ulong RegionSize;
public uint State;
public uint Protect;
public uint Type;
public uint Alignment2;
}
} }
+116 -29
View File
@@ -41,15 +41,14 @@ public sealed class VirtualMemory : IVirtualMemory
lock (_gate) lock (_gate)
{ {
foreach (var existing in _regions) var insertionIndex = FindInsertionIndex(virtualAddress);
if ((insertionIndex > 0 && virtualAddress < _regions[insertionIndex - 1].EndAddress) ||
(insertionIndex < _regions.Count && endAddress > _regions[insertionIndex].Region.VirtualAddress))
{ {
if (virtualAddress < existing.EndAddress && endAddress > existing.Region.VirtualAddress) throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
{
throw new InvalidOperationException("Attempted to map an overlapping virtual memory region.");
}
} }
_regions.Add(new MappedRegion( _regions.Insert(insertionIndex, new MappedRegion(
new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection), new VirtualMemoryRegion(virtualAddress, memorySize, fileOffset, (ulong)fileData.Length, protection),
endAddress, endAddress,
backingMemory)); backingMemory));
@@ -74,12 +73,12 @@ public sealed class VirtualMemory : IVirtualMemory
{ {
lock (_gate) lock (_gate)
{ {
if (!TryResolveRegion(virtualAddress, destination.Length, out var region, out var offset)) if (!TryValidateRange(virtualAddress, destination.Length, ProgramHeaderFlags.Read, out var regionIndex))
{ {
return false; return false;
} }
region.BackingMemory.AsSpan(offset, destination.Length).CopyTo(destination); CopyFromRegions(virtualAddress, destination, regionIndex);
return true; return true;
} }
} }
@@ -88,39 +87,127 @@ public sealed class VirtualMemory : IVirtualMemory
{ {
lock (_gate) lock (_gate)
{ {
if (!TryResolveRegion(virtualAddress, source.Length, out var region, out var offset)) if (!TryValidateRange(virtualAddress, source.Length, ProgramHeaderFlags.Write, out var regionIndex))
{ {
return false; return false;
} }
source.CopyTo(region.BackingMemory.AsSpan(offset, source.Length)); CopyToRegions(virtualAddress, source, regionIndex);
return true; return true;
} }
} }
private bool TryResolveRegion(ulong virtualAddress, int length, out MappedRegion region, out int offset) private bool TryValidateRange(
ulong virtualAddress,
int length,
ProgramHeaderFlags requiredProtection,
out int regionIndex)
{ {
foreach (var candidate in _regions) regionIndex = FindContainingRegionIndex(virtualAddress);
if (regionIndex < 0)
{ {
if (virtualAddress < candidate.Region.VirtualAddress || virtualAddress >= candidate.EndAddress) return false;
{
continue;
}
var candidateOffset = checked((int)(virtualAddress - candidate.Region.VirtualAddress));
if (candidateOffset + length > candidate.BackingMemory.Length)
{
break;
}
region = candidate;
offset = candidateOffset;
return true;
} }
region = default; var currentAddress = virtualAddress;
offset = 0; var remaining = length;
return false; var currentIndex = regionIndex;
while (true)
{
if (currentIndex >= _regions.Count)
{
return false;
}
var region = _regions[currentIndex];
if (currentAddress < region.Region.VirtualAddress ||
currentAddress >= region.EndAddress ||
(region.Region.Protection & requiredProtection) == 0)
{
return false;
}
if (remaining == 0)
{
return true;
}
var available = region.EndAddress - currentAddress;
var chunkLength = (int)Math.Min((ulong)remaining, available);
remaining -= chunkLength;
if (remaining == 0)
{
return true;
}
currentAddress += (ulong)chunkLength;
currentIndex++;
}
}
private int FindContainingRegionIndex(ulong virtualAddress)
{
var insertionIndex = FindInsertionIndex(virtualAddress);
if (insertionIndex < _regions.Count &&
_regions[insertionIndex].Region.VirtualAddress == virtualAddress)
{
return insertionIndex;
}
var candidateIndex = insertionIndex - 1;
return candidateIndex >= 0 && virtualAddress < _regions[candidateIndex].EndAddress
? candidateIndex
: -1;
}
private void CopyFromRegions(ulong virtualAddress, Span<byte> destination, int regionIndex)
{
var copied = 0;
var currentAddress = virtualAddress;
while (copied < destination.Length)
{
var region = _regions[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(destination.Length - copied, region.BackingMemory.Length - regionOffset);
region.BackingMemory.AsSpan(regionOffset, chunkLength).CopyTo(destination[copied..]);
copied += chunkLength;
currentAddress += (ulong)chunkLength;
}
}
private void CopyToRegions(ulong virtualAddress, ReadOnlySpan<byte> source, int regionIndex)
{
var copied = 0;
var currentAddress = virtualAddress;
while (copied < source.Length)
{
var region = _regions[regionIndex++];
var regionOffset = checked((int)(currentAddress - region.Region.VirtualAddress));
var chunkLength = Math.Min(source.Length - copied, region.BackingMemory.Length - regionOffset);
source.Slice(copied, chunkLength).CopyTo(region.BackingMemory.AsSpan(regionOffset, chunkLength));
copied += chunkLength;
currentAddress += (ulong)chunkLength;
}
}
private int FindInsertionIndex(ulong virtualAddress)
{
var lower = 0;
var upper = _regions.Count;
while (lower < upper)
{
var middle = lower + ((upper - lower) / 2);
if (_regions[middle].Region.VirtualAddress < virtualAddress)
{
lower = middle + 1;
}
else
{
upper = middle;
}
}
return lower;
} }
private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory); private readonly record struct MappedRegion(VirtualMemoryRegion Region, ulong EndAddress, byte[] BackingMemory);
+8 -2
View File
@@ -7,6 +7,7 @@ using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory; using SharpEmu.Core.Memory;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.VideoOut; using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent; 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.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
moduleManager.Freeze(); 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(); var fileSystem = new PhysicalFileSystem();
return new SharpEmuRuntime( return new SharpEmuRuntime(
new SelfLoader(), new SelfLoader(),
virtualMemory, virtualMemory,
new CpuDispatcher(virtualMemory, moduleManager), new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
moduleManager, moduleManager,
Aerolib.Instance, Aerolib.Instance,
cpuExecutionOptions, cpuExecutionOptions,
+3
View File
@@ -48,6 +48,9 @@ public sealed class GuiSettings
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary> /// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
public bool DiscordRichPresence { get; set; } = true; 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> /// <summary>
/// Discord application ID used for Rich Presence; the default is the /// Discord application ID used for Rich Presence; the default is the
/// SharpEmu application. Override to rebrand what Discord shows as /// SharpEmu application. Override to rebrand what Discord shows as
+10 -1
View File
@@ -26,6 +26,15 @@
"Library.Loading": "Carregando biblioteca…", "Library.Loading": "Carregando biblioteca…",
"Options.General": "Opções Gerais", "Options.General": "Opções Gerais",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"Options.Env.Desc": "Switches passados para o emulador como variáveis de ambiente na inicialização.",
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica esperando indefinidamente.\nDeixe desativado normalmente. Alguns títulos travam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada por tempo demais.\nExperimente isso quando um jogo fecha sozinho durante o carregamento.",
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração de GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e suas traduções SPIR-V para a pasta shader-dumps.\nUse ao reportar bugs de shader ou renderização.",
"Options.Env.LogDirectMemory.Desc": "Registra alocações de memória direta e falhas no console.\nUse quando um jogo aborta ou fecha durante a inicialização (boot).",
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
"Options.Section.Emulation": "EMULAÇÃO", "Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "LOGS", "Options.Section.Logging": "LOGS",
"Options.Section.Launcher": "INICIALIZADOR", "Options.Section.Launcher": "INICIALIZADOR",
@@ -126,4 +135,4 @@
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log", "Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
"Dialog.PlainTextFiles": "Arquivos de texto simples", "Dialog.PlainTextFiles": "Arquivos de texto simples",
"Dialog.LogFiles": "Arquivos de log" "Dialog.LogFiles": "Arquivos de log"
} }
+9
View File
@@ -26,6 +26,15 @@
"Library.Loading": "Loading library…", "Library.Loading": "Loading library…",
"Options.General": "General", "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.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING", "Options.Section.Logging": "LOGGING",
"Options.Section.Launcher": "LAUNCHER", "Options.Section.Launcher": "LAUNCHER",
+146
View File
@@ -0,0 +1,146 @@
{
"_languageName": "Hungarian",
"Page.Library": "Könyvtár",
"Page.Options": "Beállítások",
"Page.GameCount.One": "1 játék",
"Page.GameCount.Other": "{0} játékok",
"Library.SearchWatermark": "Keresés a könyvtárban",
"Library.AddFolder": " Mappa hozzáadása",
"Library.Rescan": "⟳ Újrakeresés",
"Library.OpenFile": "Fájl megnyitása…",
"Library.Context.Launch": "Inditás",
"Library.Context.OpenFolder": "Játékmappa megnyitása",
"Library.Context.CopyPath": "Elérési út másolása",
"Library.Context.CopyTitleId": "Cím ID másolása",
"Library.Context.Remove": "Eltávolítás a Könyvtárból",
"Library.Empty.Title": "A könyvtárad üres",
"Library.Empty.Hint": "Add meg a játékaidat tartalmazó mappát a kezdáshez.",
"Library.Empty.SearchTitle": "Nincs találat a elemre",
"Library.Empty.SearchHint": "A könyvtárban nincs olyan elem, amely egyezne a „{0}” kifejezéssel.",
"Library.Empty.AddFolder": " Játékmappa hozzáadása",
"Library.Loading": "Könyvtár betöltése",
"Options.General": "Általános",
"Options.Env.Tab": "Környezet",
"Options.Section.Environment": "KÖRNYEZETI VÁLTOZÓK",
"Options.Env.Desc": "Indításkor környezeti változóként az emulátorhoz átadott kapcsolók.",
"Options.Env.Bthid.Desc": "Jelenti, amely címeknél a Bluetooth HID nem elérhető, amelyeknél a kormány/FFB-közbenső szoftver végtelenül lekérdezi az adatokat.\nNormál esetben hagyja ki. Egyes címek lefagyanak, ha az inicializálás sikertelen.",
"Options.Env.LoopGuard.Desc": "Ne erőltesse a kilépést azoknál a címeknél, amelyek túl sokáig ismételnek ugyanazt a hívást.\nPróbálja ki ezt, ha egy játék betöltés közben magától kilép.",
"Options.Env.VkValidation.Desc": "Engedélyezze a Vulkan-érvényesítési rétegeket a GPU hibakereséshez.\nLassú. A Vulkan SDK telepítését igényli.",
"Options.Env.DumpSpirv.Desc": "Az AGC-shaderek és azok SPIR-V-fordításainak mentése a shader-dumps mappába.\nHasználd shader- vagy renderelési hibák jelentésekor.",
"Options.Env.LogDirectMemory.Desc": "A közvetlen memóriaallokációk és hibák naplózása a konzolra.\nHasználd, ha egy játék a rendszerindítás során megszakad vagy kilép.",
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
"Options.Section.Emulation": "EMULÁCIÓ",
"Options.Section.Logging": "LOGOLÁS",
"Options.Section.Launcher": "INDITÓ",
"Options.CpuEngine.Label": "CPU motor",
"Options.CpuEngine.Desc": "A játék kódjának futtatásához használt végrehajtó motor.",
"Options.CpuEngine.Native": "Natív",
"Options.Strict.Label": "Szigorú dynlib felbontás",
"Options.Strict.Desc": "Indítás megszakítása, ha egy importált szimbólum nem oldható fel.",
"Options.LogLevel.Label": "Naplózási szint",
"Options.LogLevel.Desc": "Az emulátor konzol kimenetének részletessége.",
"Options.LogLevel.Trace": "Trace",
"Options.LogLevel.Debug": "Debug",
"Options.LogLevel.Info": "Információ",
"Options.LogLevel.Warning": "Figyelmeztetés",
"Options.LogLevel.Error": "Hiba",
"Options.LogLevel.Critical": "Kritikus",
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Az első N darab import nyomon követése modulonként (0 = ki).",
"Options.LogToFile.Label": "Naplozás fájlba",
"Options.LogToFile.Desc": "Az emulátor kimenetének tükrözése egy log fájlba.",
"Options.LogFilePath.Label": "Naplófájl elérési útja",
"Options.LogFilePath.Default": "Nincs egyéni út — a logok az emulátor melletti user/logs mappába kerülnek.",
"Options.LogFilePath.Select": "Kiválasztás…",
"Options.OverrideLogFile.Label": "Naplófájl felülírása",
"Options.OverrideLogFile.Desc": "A pontos fájlútvonal használata a cím ID és időbélyeg hozzáfűzése helyett.",
"Options.TitleMusic.Label": "Címzene",
"Options.TitleMusic.Desc": "A kiválasztott játék előnézeti zenéjének ismétlése a könyvtárban.",
"Options.Discord.Label": "Discord jelenlét",
"Options.Discord.Desc": "A futó játék megjelenítése a Discord profilodon.",
"Options.Language.Label": "Emulátor nyelve",
"Options.Language.Desc": "Az indítóban használt nyelv. Azonnal érvénybe lép.",
"Common.On": "Be",
"Common.Off": "Ki",
"Console.Title": "KONZOL",
"Console.SearchWatermark": "Keresés...",
"Console.AutoScroll": "Automatikus görgetés",
"Console.Split": "Felosztás",
"Console.Copy": "Másolás",
"Console.Clear": "Törlés",
"Console.WindowTitle": "SharpEmu Konzol",
"Launch.NoGameSelected": "Nincs játék kiválasztva",
"Launch.NoGameHint": "Válassz egy játékot a könyvtárból, vagy nyiss meg közvetlenül egy eboot.bin fájlt.",
"Launch.Idle": "Tétlen",
"Launch.Console": "≡ Konzol",
"Launch.Launch": "▶ Inditás",
"Launch.Stop": "■ Leállítás",
"Launch.Running": "Fut — {0}",
"Launch.Stopping": "Leállítás…",
"Launch.Exited": "Kilépett a következő kóddal: {0} ({1})",
"Launch.ExeNotFound": "A SharpEmu futtatható fájl nem található. Előbb építsd fel a SharpEmu.CLI projektet (dotnet build).",
"Launch.LogFile": "Naplófájl: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Nem sikerült elindítani az emulátort: {0}",
"Launch.ProcessExited": "A folyamat kilépett a következő kóddal: {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "nemérvényes argumentumok",
"Exit.EbootNotFound": "eboot nem található",
"Exit.RuntimeException": "runtime exception",
"Exit.EmulationError": "emulációs hiba",
"Exit.Unknown": "ismeretlen",
"Status.EmulatorLocating": "Emulátor: keresés…",
"Status.EmulatorPath": "Emulátor: {0}",
"Status.EmulatorNotFound": "Emulátor: a SharpEmu futtatható fájl nem található — előbb építsd fel a SharpEmu.CLI-t.",
"Status.ScanningLibrary": "Könyvtár beolvasása…",
"Status.AddFolderPrompt": "Adj hozzá egy játékmappát a könyvtár feltöltéséhez.",
"Status.LibraryScanned": "Könyvtár beolvasva: {0} játék {1} mappában.",
"Status.CouldNotOpenFolder": "Nem sikerült megnyitni a mappát: {0}",
"Status.CopiedToClipboard": "{0} másolva a vágólapra.",
"Status.RemovedFromLibrary": "„{0}” eltávolítva a könyvtárból. A visszaállításához add hozzá újra a mappáját.",
"Status.Running": "Fut {0}",
"Status.Stopping": "Leállítás…",
"Status.Idle": "Nyugodt",
"Clipboard.Path": "Út",
"Clipboard.TitleId": "Cím ID",
"Discord.Playing": "Játékban {0}",
"Discord.Browsing": "Böngéssz a könyvtárban",
"Dialog.ChooseGameFolder": "Válassz egy mappát ami a játékaidat tartalmazza",
"Dialog.OpenExecutable": "Futtatható fájl megnyitása az indításhoz",
"Dialog.PsExecutables": "PS futtatható fájlok",
"Dialog.SaveLogFile": "Válaszd ki, hogy hova szeretnéd menteni a napló fájlokat",
"Dialog.PlainTextFiles": "Egyszerű szöveges fájlok",
"Dialog.LogFiles": "Naplózási fájlok",
"Options.About" : "Erről",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Forrás kód, hibajelentések és a projekt fejlesztése.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
"About.GithubButton": "Járulj hozzá GitHubon!",
"About.DiscordButton": "Csatlakozz a Discordunhoz!"
}
+146
View File
@@ -0,0 +1,146 @@
{
"_languageName": "Português (Portugal)",
"Page.Library": "Biblioteca",
"Page.Options": "Opções",
"Page.GameCount.One": "1 jogo",
"Page.GameCount.Other": "{0} jogos",
"Library.SearchWatermark": "Pesquisar biblioteca…",
"Library.AddFolder": " Adicionar pasta",
"Library.Rescan": "⟳ Reanalisar",
"Library.OpenFile": "Abrir ficheiro…",
"Library.Context.Launch": "Iniciar",
"Library.Context.OpenFolder": "Abrir pasta do jogo",
"Library.Context.CopyPath": "Copiar caminho",
"Library.Context.CopyTitleId": "Copiar ID do título",
"Library.Context.Remove": "Remover da biblioteca",
"Library.Empty.Title": "A sua biblioteca está vazia",
"Library.Empty.Hint": "Adicione uma pasta com os seus jogos para começar.",
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua pesquisa",
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
"Library.Empty.AddFolder": " Adicionar pasta de jogos",
"Library.Loading": "A carregar biblioteca…",
"Options.General": "Geral",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIÁVEIS DE AMBIENTE",
"Options.Env.Desc": "Switches passados ao emulador como variáveis de ambiente no arranque.",
"Options.Env.Bthid.Desc": "Reporta o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica à espera indefinidamente.\nDeixe desativado normalmente. Alguns títulos bloqueiam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não force o encerramento de títulos que repetem a mesma chamada durante demasiado tempo.\nExperimente isto quando um jogo fecha sozinho durante o carregamento.",
"Options.Env.VkValidation.Desc": "Ativa as camadas de validação do Vulkan para depuração da GPU.\nLento. Requer que o Vulkan SDK esteja instalado.",
"Options.Env.DumpSpirv.Desc": "Extrai os shaders AGC e as respetivas traduções SPIR-V para a pasta shader-dumps.\nUtilize ao reportar problemas de shaders ou renderização.",
"Options.Env.LogDirectMemory.Desc": "Regista alocações de memória direta e falhas na consola.\nUtilize quando um jogo aborta ou fecha durante o arranque.",
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "REGISTOS",
"Options.Section.Launcher": "LANÇADOR",
"Options.CpuEngine.Label": "Motor de CPU",
"Options.CpuEngine.Desc": "Motor de execução utilizado para correr o código do jogo.",
"Options.CpuEngine.Native": "Nativo",
"Options.Strict.Label": "Resolução estrita de dynlib",
"Options.Strict.Desc": "Falha o arranque quando um símbolo importado não pode ser resolvido.",
"Options.LogLevel.Label": "Nível de registo",
"Options.LogLevel.Desc": "Nível de detalhe da saída da consola do emulador.",
"Options.LogLevel.Trace": "Rastreio",
"Options.LogLevel.Debug": "Depuração",
"Options.LogLevel.Info": "Informação",
"Options.LogLevel.Warning": "Aviso",
"Options.LogLevel.Error": "Erro",
"Options.LogLevel.Critical": "Crítico",
"Options.TraceImports.Label": "Limite de rastreio de importações",
"Options.TraceImports.Desc": "Rastreia as primeiras N importações por módulo (0 = desativado).",
"Options.LogToFile.Label": "Registar para ficheiro",
"Options.LogToFile.Desc": "Duplicar a saída do emulador para um ficheiro de registo.",
"Options.LogFilePath.Label": "Caminho do ficheiro de registo",
"Options.LogFilePath.Default": "Sem caminho personalizado — os registos vão para user/logs junto ao emulador.",
"Options.LogFilePath.Select": "Selecionar…",
"Options.OverrideLogFile.Label": "Substituir ficheiro de registo",
"Options.OverrideLogFile.Desc": "Utilizar o caminho de ficheiro exato em vez de acrescentar o ID do título e a hora.",
"Options.TitleMusic.Label": "Música do título",
"Options.TitleMusic.Desc": "Repetir em loop a música de pré-visualização do jogo selecionado na biblioteca.",
"Options.Discord.Label": "Presença no Discord",
"Options.Discord.Desc": "Mostrar o jogo em execução no seu perfil do Discord.",
"Options.Language.Label": "Idioma do emulador",
"Options.Language.Desc": "Idioma utilizado em todo o lançador. Aplica-se de imediato.",
"Common.On": "Ativado",
"Common.Off": "Desativado",
"Console.Title": "CONSOLA",
"Console.SearchWatermark": "Pesquisar...",
"Console.AutoScroll": "Deslocamento automático",
"Console.Split": "Dividir",
"Console.Copy": "Copiar",
"Console.Clear": "Limpar",
"Console.WindowTitle": "Consola do SharpEmu",
"Launch.NoGameSelected": "Nenhum jogo selecionado",
"Launch.NoGameHint": "Escolha um jogo da biblioteca ou abra um eboot.bin diretamente.",
"Launch.Idle": "Inativo",
"Launch.Console": "≡ Consola",
"Launch.Launch": "▶ Iniciar",
"Launch.Stop": "■ Parar",
"Launch.Running": "Em execução — {0}",
"Launch.Stopping": "A parar…",
"Launch.Exited": "Terminou com o código {0} ({1})",
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
"Launch.LogFile": "Ficheiro de registo: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
"Launch.ProcessExited": "O processo terminou com o código {0} ({1}).",
"Exit.Ok": "OK",
"Exit.InvalidArguments": "argumentos inválidos",
"Exit.EbootNotFound": "eboot não encontrado",
"Exit.RuntimeException": "exceção em tempo de execução",
"Exit.EmulationError": "erro de emulação",
"Exit.Unknown": "desconhecido",
"Status.EmulatorLocating": "Emulador: a localizar…",
"Status.EmulatorPath": "Emulador: {0}",
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile primeiro o SharpEmu.CLI.",
"Status.ScanningLibrary": "A analisar biblioteca…",
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
"Status.LibraryScanned": "Biblioteca analisada: {0} jogo(s) em {1} pasta(s).",
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente a pasta para o restaurar.",
"Status.Running": "A executar {0}",
"Status.Stopping": "A parar…",
"Status.Idle": "Inativo",
"Clipboard.Path": "Caminho",
"Clipboard.TitleId": "ID do título",
"Discord.Playing": "A jogar {0}",
"Discord.Browsing": "A navegar na biblioteca",
"Dialog.ChooseGameFolder": "Escolha uma pasta com jogos",
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
"Dialog.PsExecutables": "Executáveis PS",
"Dialog.SaveLogFile": "Selecione onde guardar o ficheiro de registo",
"Dialog.PlainTextFiles": "Ficheiros de Texto Simples",
"Dialog.LogFiles": "Ficheiros de Registo",
"Options.About" : "Sobre",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Código-fonte, problemas e desenvolvimento do projeto.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Junte-se ao nosso Discord!"
}
+82
View File
@@ -387,6 +387,88 @@ SPDX-License-Identifier: GPL-2.0-or-later
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
<ScrollViewer>
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
<Border Classes="card">
<StackPanel Spacing="14">
<TextBlock x:Name="EnvSectionTitle" Classes="sectionTitle" Text="ENVIRONMENT VARIABLES" />
<TextBlock x:Name="EnvDesc"
Text="Switches passed to the emulator as environment variables at launch."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_BTHID_UNAVAILABLE" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvBthidDesc"
Text="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.&#10;Leave off normally. Some titles freeze when init fails."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLoopGuardDesc"
Text="Do not force quit titles that repeat the same call for too long.&#10;Try this when a game exits on its own while loading."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvVkValidationDesc"
Text="Enable Vulkan validation layers for GPU debugging.&#10;Slow. Requires the Vulkan SDK to be installed."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_DUMP_SPIRV" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvDumpSpirvDesc"
Text="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.&#10;Use when reporting shader or rendering bugs."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_DIRECT_MEMORY" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogDirectMemoryDesc"
Text="Log direct memory allocations and failures to the console.&#10;Use when a game aborts or exits during boot."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogNpDesc"
Text="Log NP (PlayStation Network) library calls to the console."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</TabItem>
</TabControl> </TabControl>
</Grid> </Grid>
</Panel> </Panel>
+80 -14
View File
@@ -12,7 +12,8 @@ using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using SharpEmu.Libs.Pad; using SharpEmu.HLE.Host;
using SharpEmu.HLE.Host.Windows;
using SharpEmu.Logging; using SharpEmu.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
@@ -62,7 +63,7 @@ public partial class MainWindow : Window
// Controller navigation state. // Controller navigation state.
private readonly DispatcherTimer _gamepadTimer; private readonly DispatcherTimer _gamepadTimer;
private uint _previousPadButtons; private HostGamepadButtons _previousPadButtons;
private long _navLeftNextAt; private long _navLeftNextAt;
private long _navRightNextAt; private long _navRightNextAt;
private long _navUpNextAt; private long _navUpNextAt;
@@ -125,6 +126,18 @@ public partial class MainWindow : Window
UpdateDiscordPresence(); UpdateDiscordPresence();
}; };
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync(); 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(); LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel); GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
@@ -139,8 +152,8 @@ public partial class MainWindow : Window
Opened += async (_, _) => await OnOpenedAsync(); Opened += async (_, _) => await OnOpenedAsync();
Closing += (_, _) => OnWindowClosing(); Closing += (_, _) => OnWindowClosing();
DualSenseReader.EnsureStarted(); WindowsDualSenseReader.EnsureStarted();
XInputReader.EnsureStarted(); WindowsXInputReader.EnsureStarted();
_gamepadTimer = new DispatcherTimer _gamepadTimer = new DispatcherTimer
{ {
Interval = TimeSpan.FromMilliseconds(50), Interval = TimeSpan.FromMilliseconds(50),
@@ -212,9 +225,9 @@ public partial class MainWindow : Window
private void PollGamepad() private void PollGamepad()
{ {
// DualSense wins when both are connected; XInput covers Xbox pads. // DualSense wins when both are connected; XInput covers Xbox pads.
if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad)) if (!WindowsDualSenseReader.TryGetState(out var pad) && !WindowsXInputReader.TryGetState(out pad))
{ {
_previousPadButtons = 0; _previousPadButtons = HostGamepadButtons.None;
return; return;
} }
@@ -227,12 +240,12 @@ public partial class MainWindow : Window
} }
var shoulderPressed = pad.Buttons & ~_previousPadButtons; var shoulderPressed = pad.Buttons & ~_previousPadButtons;
if ((shoulderPressed & OrbisPadButton.L1) != 0) if ((shoulderPressed & HostGamepadButtons.L1) != 0)
{ {
SetActivePage(0); SetActivePage(0);
} }
if ((shoulderPressed & OrbisPadButton.R1) != 0) if ((shoulderPressed & HostGamepadButtons.R1) != 0)
{ {
SetActivePage(1); SetActivePage(1);
} }
@@ -244,10 +257,10 @@ public partial class MainWindow : Window
} }
var now = Environment.TickCount64; var now = Environment.TickCount64;
var left = (pad.Buttons & 0x0080) != 0 || pad.LeftX < 64; var left = (pad.Buttons & HostGamepadButtons.Left) != 0 || pad.LeftX < 64;
var right = (pad.Buttons & 0x0020) != 0 || pad.LeftX > 192; var right = (pad.Buttons & HostGamepadButtons.Right) != 0 || pad.LeftX > 192;
var up = (pad.Buttons & 0x0010) != 0 || pad.LeftY < 64; var up = (pad.Buttons & HostGamepadButtons.Up) != 0 || pad.LeftY < 64;
var down = (pad.Buttons & 0x0040) != 0 || pad.LeftY > 192; var down = (pad.Buttons & HostGamepadButtons.Down) != 0 || pad.LeftY > 192;
if (ShouldNavigate(left, ref _navLeftNextAt, now)) if (ShouldNavigate(left, ref _navLeftNextAt, now))
{ {
@@ -270,12 +283,12 @@ public partial class MainWindow : Window
} }
var pressed = pad.Buttons & ~_previousPadButtons; var pressed = pad.Buttons & ~_previousPadButtons;
if ((pressed & 0x4000) != 0) // Cross if ((pressed & HostGamepadButtons.Cross) != 0)
{ {
LaunchSelected(); LaunchSelected();
} }
if ((pressed & 0x2000) != 0) // Circle if ((pressed & HostGamepadButtons.Circle) != 0)
{ {
StopEmulator(); StopEmulator();
} }
@@ -403,6 +416,15 @@ public partial class MainWindow : Window
LoadingStateText.Text = loc.Get("Library.Loading"); LoadingStateText.Text = loc.Get("Library.Loading");
GeneralTabItem.Header = loc.Get("Options.General"); 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"); EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging"); LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher"); LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
@@ -584,9 +606,34 @@ public partial class MainWindow : Window
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile; OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic; TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
DiscordToggle.IsChecked = _settings.DiscordRichPresence; 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(); 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() private string SelectedLogLevel()
{ {
return LogLevelBox.SelectedIndex switch return LogLevelBox.SelectedIndex switch
@@ -1402,6 +1449,24 @@ public partial class MainWindow : Window
Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)), Localization.Instance.Format("Launch.Command", string.Join(' ', arguments)),
DimLineBrush); 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(); var emulator = new EmulatorProcess();
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError)); emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code)); emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
@@ -1499,6 +1564,7 @@ public partial class MainWindow : Window
2 => "Exit.EbootNotFound", 2 => "Exit.EbootNotFound",
3 => "Exit.RuntimeException", 3 => "Exit.RuntimeException",
4 => "Exit.EmulationError", 4 => "Exit.EmulationError",
-1073741819 => "Exit.EmulationError",
_ => "Exit.Unknown", _ => "Exit.Unknown",
}; };
var meaning = Localization.Instance.Get(meaningKey); var meaning = Localization.Instance.Get(meaningKey);
+10 -7
View File
@@ -10,6 +10,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PropertyGroup> <PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version> <Version>0.0.1</Version>
<!-- Required by the source-generated LibraryImport stubs in the linked
controller readers below. -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<!-- Dependency-free; provides the BuildInfo provenance shown in the <!-- Dependency-free; provides the BuildInfo provenance shown in the
@@ -41,14 +44,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared <!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
with the emulator's pad HLE. They are dependency-free, so they are with the emulator's host input backend. They are dependency-free, so
compiled in directly rather than pulling a reference to all of they are compiled in directly rather than pulling a reference to all
SharpEmu.Libs into the launcher. --> of SharpEmu.HLE into the launcher. -->
<ItemGroup> <ItemGroup>
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" /> <Compile Include="..\SharpEmu.HLE\Host\HostGamepadState.cs" Link="Input/HostGamepadState.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" /> <Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsHidNative.cs" Link="Input/WindowsHidNative.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" /> <Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsDualSenseReader.cs" Link="Input/WindowsDualSenseReader.cs" />
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" /> <Compile Include="..\SharpEmu.HLE\Host\Windows\WindowsXInputReader.cs" Link="Input/WindowsXInputReader.cs" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+51 -10
View File
@@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text; using System.Text;
@@ -238,23 +239,63 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
return false; return false;
} }
var bytes = new byte[capacity]; const int StackBufferLength = 512;
for (var index = 0; index < bytes.Length; index++) const int ReadChunkLength = 128;
var rented = capacity > StackBufferLength ? ArrayPool<byte>.Shared.Rent(capacity) : null;
Span<byte> bytes = rented is null ? stackalloc byte[StackBufferLength] : rented;
try
{ {
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1))) var length = 0;
while (length < capacity)
{ {
return false; // Bulk-read in bounded chunks rather than the full capacity: the string
// may end just before unmapped memory, and overreading past the
// terminator by more than a chunk could fault where the old
// byte-by-byte loop succeeded.
var chunk = Math.Min(ReadChunkLength, capacity - length);
var span = bytes.Slice(length, chunk);
if (Memory.TryRead(address + (ulong)length, span))
{
var terminator = span.IndexOf((byte)0);
if (terminator >= 0)
{
value = Encoding.UTF8.GetString(bytes[..(length + terminator)]);
return true;
}
length += chunk;
continue;
}
// The chunk touches an unreadable range; fall back to per-byte reads so a
// terminator sitting before the bad byte still yields the string.
for (var i = 0; i < chunk; i++)
{
if (!Memory.TryRead(address + (ulong)(length + i), bytes.Slice(length + i, 1)))
{
return false;
}
if (bytes[length + i] == 0)
{
value = Encoding.UTF8.GetString(bytes[..(length + i)]);
return true;
}
}
length += chunk;
} }
if (bytes[index] == 0) value = Encoding.UTF8.GetString(bytes[..capacity]);
return true;
}
finally
{
if (rented is not null)
{ {
value = Encoding.UTF8.GetString(bytes, 0, index); ArrayPool<byte>.Shared.Return(rented);
return true;
} }
} }
value = Encoding.UTF8.GetString(bytes);
return true;
} }
public bool PushUInt64(ulong value) public bool PushUInt64(ulong value)
+13
View File
@@ -0,0 +1,13 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
[Flags]
public enum GuestPageProtection
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
}
+25 -25
View File
@@ -23,6 +23,20 @@ public readonly record struct GuestThreadSnapshot(
ulong LastReturnRip, ulong LastReturnRip,
string? BlockReason); string? BlockReason);
/// <summary>
/// Continuation state for a blocked guest thread, replacing the closure pair a blocking
/// wait used to allocate. TryWake runs under the scheduler's guest-thread gate and
/// returns true when the waiter has a final result and the thread should be re-readied;
/// false leaves it parked. Resume runs later on the woken thread outside that gate, and
/// its return value becomes the guest's RAX for the resumed call.
/// </summary>
public interface IGuestThreadBlockWaiter
{
int Resume();
bool TryWake();
}
public interface IGuestThreadScheduler public interface IGuestThreadScheduler
{ {
bool SupportsGuestContextTransfer { get; } bool SupportsGuestContextTransfer { get; }
@@ -106,10 +120,7 @@ public static class GuestThreadExecution
private static string? _pendingBlockWakeKey; private static string? _pendingBlockWakeKey;
[ThreadStatic] [ThreadStatic]
private static Func<int>? _pendingBlockResumeHandler; private static IGuestThreadBlockWaiter? _pendingBlockWaiter;
[ThreadStatic]
private static Func<bool>? _pendingBlockWakeHandler;
[ThreadStatic] [ThreadStatic]
private static long _pendingBlockDeadlineTimestamp; private static long _pendingBlockDeadlineTimestamp;
@@ -157,8 +168,7 @@ public static class GuestThreadExecution
_pendingBlockContinuationValid = false; _pendingBlockContinuationValid = false;
_pendingBlockContinuation = default; _pendingBlockContinuation = default;
_pendingBlockWakeKey = null; _pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null; _pendingBlockWaiter = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0; _pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false; _pendingEntryExit = false;
_pendingEntryExitValue = 0; _pendingEntryExitValue = 0;
@@ -179,8 +189,7 @@ public static class GuestThreadExecution
_pendingBlockContinuationValid = false; _pendingBlockContinuationValid = false;
_pendingBlockContinuation = default; _pendingBlockContinuation = default;
_pendingBlockWakeKey = null; _pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null; _pendingBlockWaiter = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0; _pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false; _pendingEntryExit = false;
_pendingEntryExitValue = 0; _pendingEntryExitValue = 0;
@@ -211,8 +220,7 @@ public static class GuestThreadExecution
CpuContext? context, CpuContext? context,
string reason, string reason,
string? wakeKey = null, string? wakeKey = null,
Func<int>? resumeHandler = null, IGuestThreadBlockWaiter? waiter = null,
Func<bool>? wakeHandler = null,
long blockDeadlineTimestamp = 0) long blockDeadlineTimestamp = 0)
{ {
if (!IsGuestThread) if (!IsGuestThread)
@@ -222,8 +230,7 @@ public static class GuestThreadExecution
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason; _pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey; _pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
_pendingBlockResumeHandler = resumeHandler; _pendingBlockWaiter = waiter;
_pendingBlockWakeHandler = wakeHandler;
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp; _pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation)) if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
{ {
@@ -255,7 +262,6 @@ public static class GuestThreadExecution
out hasContinuation, out hasContinuation,
out _, out _,
out _, out _,
out _,
out _); out _);
} }
@@ -264,16 +270,14 @@ public static class GuestThreadExecution
out GuestCpuContinuation continuation, out GuestCpuContinuation continuation,
out bool hasContinuation, out bool hasContinuation,
out string wakeKey, out string wakeKey,
out Func<int>? resumeHandler, out IGuestThreadBlockWaiter? waiter)
out Func<bool>? wakeHandler)
{ {
return TryConsumeCurrentThreadBlock( return TryConsumeCurrentThreadBlock(
out reason, out reason,
out continuation, out continuation,
out hasContinuation, out hasContinuation,
out wakeKey, out wakeKey,
out resumeHandler, out waiter,
out wakeHandler,
out _); out _);
} }
@@ -282,8 +286,7 @@ public static class GuestThreadExecution
out GuestCpuContinuation continuation, out GuestCpuContinuation continuation,
out bool hasContinuation, out bool hasContinuation,
out string wakeKey, out string wakeKey,
out Func<int>? resumeHandler, out IGuestThreadBlockWaiter? waiter,
out Func<bool>? wakeHandler,
out long blockDeadlineTimestamp) out long blockDeadlineTimestamp)
{ {
reason = _pendingBlockReason ?? string.Empty; reason = _pendingBlockReason ?? string.Empty;
@@ -292,8 +295,7 @@ public static class GuestThreadExecution
continuation = default; continuation = default;
hasContinuation = false; hasContinuation = false;
wakeKey = string.Empty; wakeKey = string.Empty;
resumeHandler = null; waiter = null;
wakeHandler = null;
blockDeadlineTimestamp = 0; blockDeadlineTimestamp = 0;
return false; return false;
} }
@@ -301,15 +303,13 @@ public static class GuestThreadExecution
continuation = _pendingBlockContinuation; continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid; hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason; wakeKey = _pendingBlockWakeKey ?? reason;
resumeHandler = _pendingBlockResumeHandler; waiter = _pendingBlockWaiter;
wakeHandler = _pendingBlockWakeHandler;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp; blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null; _pendingBlockReason = null;
_pendingBlockContinuation = default; _pendingBlockContinuation = default;
_pendingBlockContinuationValid = false; _pendingBlockContinuationValid = false;
_pendingBlockWakeKey = null; _pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null; _pendingBlockWaiter = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0; _pendingBlockDeadlineTimestamp = 0;
return true; return true;
} }
@@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// General-purpose register snapshot of a suspended thread, produced by
/// <see cref="IHostThreading.TryCaptureThreadRegisters"/>. Registers are named
/// after the guest ISA (x86-64), which every supported host executes natively.
/// </summary>
public readonly record struct HostCapturedRegisters(
ulong Rip,
ulong Rsp,
ulong Rbp,
ulong Rax,
ulong Rbx,
ulong Rcx,
ulong Rdx);
+46
View File
@@ -0,0 +1,46 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host-neutral gamepad button flags. Named after the PlayStation layout the guest API
/// exposes, but the numeric values are the seam's own — the HLE pad exports translate
/// them to SCE_PAD_BUTTON bits, so guest ABI values never leak into host backends.
/// </summary>
[Flags]
public enum HostGamepadButtons : uint
{
None = 0,
Up = 1 << 0,
Down = 1 << 1,
Left = 1 << 2,
Right = 1 << 3,
Cross = 1 << 4,
Circle = 1 << 5,
Square = 1 << 6,
Triangle = 1 << 7,
L1 = 1 << 8,
R1 = 1 << 9,
L2 = 1 << 10,
R2 = 1 << 11,
L3 = 1 << 12,
R3 = 1 << 13,
Options = 1 << 14,
TouchPad = 1 << 15,
}
/// <summary>
/// Snapshot of one host gamepad: sticks are 0..255 with 128 centered and Y growing
/// downward; triggers 0..255. Unmanaged on purpose so per-frame polls can stackalloc
/// snapshot buffers.
/// </summary>
public readonly record struct HostGamepadState(
bool Connected,
HostGamepadButtons Buttons,
byte LeftX,
byte LeftY,
byte RightX,
byte RightY,
byte LeftTrigger,
byte RightTrigger);
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Platform-neutral page protection. Values intentionally enumerate the exact
/// combinations the emulator uses today so each maps 1:1 onto a single native
/// protection constant (PAGE_* on Windows, PROT_* elsewhere).
/// </summary>
public enum HostPageProtection
{
NoAccess,
ReadOnly,
ReadWrite,
Execute,
ReadExecute,
ReadWriteExecute,
ExecuteWriteCopy,
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE.Host.Windows;
namespace SharpEmu.HLE.Host;
/// <summary>
/// Process-wide access point for the host platform backend. Static HLE export
/// classes (which cannot receive constructor injection) resolve host primitives
/// through <see cref="Current"/>; injectable components should instead accept an
/// <see cref="IHostPlatform"/> and merely default to this.
/// </summary>
public static class HostPlatform
{
private static readonly Lazy<IHostPlatform> Instance = new(Create);
public static IHostPlatform Current => Instance.Value;
private static IHostPlatform Create()
{
// The Windows backend executes guest x86-64 natively and emits x86-64
// stubs, so a native ARM64 process must be rejected here rather than
// crash undefined later (x64 processes under emulation report X64).
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
return new WindowsHostPlatform();
}
throw new PlatformNotSupportedException(
"SharpEmu native guest execution requires a host platform backend and none exists for this OS/architecture yet (currently Windows x64 only).");
}
}
+20
View File
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Result of <see cref="IHostMemory.Query"/>. The Raw* fields carry the
/// untranslated OS values so call sites migrated from direct VirtualQuery use
/// keep comparing (and logging) the exact native words they did before;
/// <see cref="State"/> and <see cref="Protection"/> are neutral views.
/// </summary>
public readonly record struct HostRegionInfo(
ulong BaseAddress,
ulong AllocationBase,
ulong RegionSize,
HostRegionState State,
uint RawState,
HostPageProtection Protection,
uint RawProtection,
uint RawAllocationProtection);
+11
View File
@@ -0,0 +1,11 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
public enum HostRegionState
{
Free,
Reserved,
Committed,
}
@@ -0,0 +1,21 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host functions whose addresses the execution engine bakes into emitted
/// stubs (spin-waits, worker run loops, TLS reads). Enum-keyed rather than a
/// free-form name lookup: each platform's emitters need their own specific
/// functions, and this set is exactly what the current emitters consume.
/// </summary>
public enum HostRuntimeFunction
{
TlsGetValue,
QueryPerformanceCounter,
SwitchToThread,
Sleep,
WaitForSingleObject,
SetEvent,
ExitThread,
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host audio-output device access. The HLE audio exports convert guest submissions to
/// interleaved stereo 16-bit PCM (the format every backend accepts) and feed them through
/// streams opened here; everything device-specific — queueing, backpressure, native
/// buffer lifetime — lives behind <see cref="IHostAudioStream"/>.
/// </summary>
public interface IHostAudioOutput
{
/// <summary>Backend identifier for diagnostics (e.g. "winmm").</summary>
string BackendName { get; }
/// <summary>
/// Opens an interleaved stereo 16-bit PCM output stream at the given sample rate.
/// Throws when the host has no usable output device; callers degrade to a silent
/// port and pace the guest instead.
/// </summary>
IHostAudioStream OpenStereoPcm16Stream(uint sampleRate);
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// One open host audio output stream. Submissions are interleaved stereo 16-bit PCM at
/// the sample rate the stream was opened with.
/// </summary>
public interface IHostAudioStream : IDisposable
{
/// <summary>
/// Submits one buffer. May block briefly while the device drains its queue (this is
/// what paces the guest's audio loop); returns false when the stream cannot accept
/// audio, in which case the caller paces the guest itself.
/// </summary>
bool Submit(ReadOnlySpan<byte> stereoPcm16);
}
@@ -0,0 +1,32 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Installation mechanics for the process-wide fault interception the execution
/// engine relies on to catch guest faults. Deliberately thin: the managed
/// handlers keep receiving the platform's raw exception data, and the emitted
/// pre-filter thunk is an opaque per-platform unit. Implementations live next
/// to the execution backend (SharpEmu.Core), not behind HostPlatform.Current.
/// </summary>
public interface IHostFaultHandling
{
/// <summary>
/// Emits the native thunk that wraps a managed fault handler: it pre-filters
/// exception codes that must never enter managed code and, when the fault
/// happened on a guest stack, switches to the host stack saved in
/// <paramref name="hostRspSwitchTlsSlot"/> before the call. Returns 0 on failure.
/// </summary>
nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress);
void FreeThunk(nint thunk);
/// <summary>Installs a first-chance handler ahead of existing ones; returns a removal handle (0 on failure).</summary>
nint AddFirstChanceHandler(nint thunk);
void RemoveHandler(nint handle);
/// <summary>Installs the last-resort filter; pass 0 to clear.</summary>
void SetUnhandledFilter(nint thunk);
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// Host input devices: gamepad state snapshots, force-feedback/lightbar sinks, and the
/// keyboard-fallback queries. Which physical readers exist (DualSense over raw HID,
/// XInput, evdev, ...) is a backend detail; merge policy between devices and the
/// keyboard lives in the HLE pad exports.
/// </summary>
public interface IHostInput
{
/// <summary>Starts the background device readers once; safe to call repeatedly.</summary>
void EnsureStarted();
/// <summary>
/// Fills <paramref name="destination"/> with snapshots of currently connected
/// gamepads and returns how many were written (0 when none are connected).
/// </summary>
int GetGamepadStates(Span<HostGamepadState> destination);
/// <summary>Human-readable name of the first connected gamepad, or null.</summary>
string? DescribeConnectedGamepad();
/// <summary>Sets rumble on all connected gamepads; large = strong/left motor.</summary>
void SetRumble(byte largeMotor, byte smallMotor);
/// <summary>
/// Approximates per-trigger vibration on gamepads without independent trigger
/// actuators; null leaves that trigger's current value unchanged.
/// </summary>
void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger);
void SetLightbar(byte red, byte green, byte blue);
void ResetLightbar();
/// <summary>True when a window of this process has keyboard focus.</summary>
bool IsHostWindowFocused();
/// <summary>Windows virtual-key code semantics; other backends translate.</summary>
bool IsKeyDown(int virtualKey);
}
+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);
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.Host;
/// <summary>
/// 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; }
IHostAudioOutput Audio { get; }
IHostInput Input { 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);
}
+51
View File
@@ -0,0 +1,51 @@
// 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>
/// Asks the OS for ~1 ms timed-wait granularity for the life of the process
/// (idempotent; best-effort). No-op on platforms whose default is already fine.
/// </summary>
void RequestTimerResolution();
/// <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);
}
@@ -3,21 +3,21 @@
using Microsoft.Win32.SafeHandles; using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Pad; namespace SharpEmu.HLE.Host.Windows;
/// <summary> /// <summary>
/// Reads a DualSense controller over raw HID on a background thread. /// Reads a DualSense controller over raw HID on a background thread.
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31, /// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
/// activated by requesting feature report 0x05), with hot-plug retry. /// activated by requesting feature report 0x05), with hot-plug retry.
/// </summary> /// </summary>
internal static class DualSenseReader internal static class WindowsDualSenseReader
{ {
private const ushort SonyVendorId = 0x054C; private const ushort SonyVendorId = 0x054C;
private const ushort DualSenseProductId = 0x0CE6; private const ushort DualSenseProductId = 0x0CE6;
private const ushort DualSenseEdgeProductId = 0x0DF2; private const ushort DualSenseEdgeProductId = 0x0DF2;
private static readonly object Gate = new(); private static readonly object Gate = new();
private static PadState _state; private static HostGamepadState _state;
private static bool _started; private static bool _started;
// Output (rumble/lightbar) state, all guarded by Gate. // Output (rumble/lightbar) state, all guarded by Gate.
@@ -37,6 +37,8 @@ internal static class DualSenseReader
/// <summary>Starts the background reader once; safe to call repeatedly.</summary> /// <summary>Starts the background reader once; safe to call repeatedly.</summary>
internal static void EnsureStarted() internal static void EnsureStarted()
{ {
// The GUI source-links this reader and calls it directly, without the
// host-platform resolution that otherwise guarantees Windows.
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
{ {
return; return;
@@ -59,7 +61,7 @@ internal static class DualSenseReader
} }
} }
internal static bool TryGetState(out PadState state) internal static bool TryGetState(out HostGamepadState state)
{ {
lock (Gate) lock (Gate)
{ {
@@ -69,7 +71,7 @@ internal static class DualSenseReader
return state.Connected; return state.Connected;
} }
private static void SetState(in PadState state) private static void SetState(in HostGamepadState state)
{ {
lock (Gate) lock (Gate)
{ {
@@ -148,11 +150,11 @@ internal static class DualSenseReader
{ {
if (_outputStream is null) if (_outputStream is null)
{ {
var handle = HidNative.CreateFile( var handle = WindowsHidNative.CreateFile(
_devicePath, _devicePath,
HidNative.GenericRead | HidNative.GenericWrite, WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
HidNative.FileShareRead | HidNative.FileShareWrite, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0); 0, WindowsHidNative.OpenExisting, 0, 0);
if (handle.IsInvalid) if (handle.IsInvalid)
{ {
handle.Dispose(); handle.Dispose();
@@ -262,7 +264,7 @@ internal static class DualSenseReader
// to the full 0x31 input report. Harmless over USB. // to the full 0x31 input report. Harmless over USB.
var feature = new byte[41]; var feature = new byte[41];
feature[0] = 0x05; feature[0] = 0x05;
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length); _ = WindowsHidNative.HidD_GetFeature(handle, feature, feature.Length);
if (!announcedConnect) if (!announcedConnect)
{ {
@@ -320,18 +322,18 @@ internal static class DualSenseReader
private static SafeFileHandle? OpenDualSense(out string? devicePath) private static SafeFileHandle? OpenDualSense(out string? devicePath)
{ {
devicePath = null; devicePath = null;
foreach (var path in HidNative.EnumerateHidDevicePaths()) foreach (var path in WindowsHidNative.EnumerateHidDevicePaths())
{ {
// Open without access rights just to query VID/PID. // Open without access rights just to query VID/PID.
using var probe = HidNative.CreateFile( using var probe = WindowsHidNative.CreateFile(
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0); path, 0, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite, 0, WindowsHidNative.OpenExisting, 0, 0);
if (probe.IsInvalid) if (probe.IsInvalid)
{ {
continue; continue;
} }
var attributes = new HidNative.HiddAttributes { Size = 12 }; var attributes = new WindowsHidNative.HiddAttributes { Size = 12 };
if (!HidNative.HidD_GetAttributes(probe, ref attributes) || if (!WindowsHidNative.HidD_GetAttributes(probe, ref attributes) ||
attributes.VendorId != SonyVendorId || attributes.VendorId != SonyVendorId ||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId)) (attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
{ {
@@ -339,19 +341,19 @@ internal static class DualSenseReader
} }
// Read+write so feature reports work; fall back to read-only. // Read+write so feature reports work; fall back to read-only.
var handle = HidNative.CreateFile( var handle = WindowsHidNative.CreateFile(
path, path,
HidNative.GenericRead | HidNative.GenericWrite, WindowsHidNative.GenericRead | WindowsHidNative.GenericWrite,
HidNative.FileShareRead | HidNative.FileShareWrite, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0); 0, WindowsHidNative.OpenExisting, 0, 0);
if (handle.IsInvalid) if (handle.IsInvalid)
{ {
handle.Dispose(); handle.Dispose();
handle = HidNative.CreateFile( handle = WindowsHidNative.CreateFile(
path, path,
HidNative.GenericRead, WindowsHidNative.GenericRead,
HidNative.FileShareRead | HidNative.FileShareWrite, WindowsHidNative.FileShareRead | WindowsHidNative.FileShareWrite,
0, HidNative.OpenExisting, 0, 0); 0, WindowsHidNative.OpenExisting, 0, 0);
} }
if (!handle.IsInvalid) if (!handle.IsInvalid)
@@ -366,7 +368,7 @@ internal static class DualSenseReader
return null; return null;
} }
private static bool TryParseReport(ReadOnlySpan<byte> report, out PadState state) private static bool TryParseReport(ReadOnlySpan<byte> report, out HostGamepadState state)
{ {
// USB: report id 0x01, payload starts at [1]. // USB: report id 0x01, payload starts at [1].
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2]. // Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
@@ -395,43 +397,43 @@ internal static class DualSenseReader
var buttons1 = report[offset + 8]; var buttons1 = report[offset + 8];
var buttons2 = report[offset + 9]; var buttons2 = report[offset + 9];
uint buttons = 0; var buttons = HostGamepadButtons.None;
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0; buttons |= (buttons0 & 0x10) != 0 ? HostGamepadButtons.Square : 0;
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0; buttons |= (buttons0 & 0x20) != 0 ? HostGamepadButtons.Cross : 0;
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0; buttons |= (buttons0 & 0x40) != 0 ? HostGamepadButtons.Circle : 0;
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0; buttons |= (buttons0 & 0x80) != 0 ? HostGamepadButtons.Triangle : 0;
buttons |= HatToButtons(buttons0 & 0x0F); buttons |= HatToButtons(buttons0 & 0x0F);
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0; buttons |= (buttons1 & 0x01) != 0 ? HostGamepadButtons.L1 : 0;
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0; buttons |= (buttons1 & 0x02) != 0 ? HostGamepadButtons.R1 : 0;
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0; buttons |= (buttons1 & 0x04) != 0 ? HostGamepadButtons.L2 : 0;
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0; buttons |= (buttons1 & 0x08) != 0 ? HostGamepadButtons.R2 : 0;
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0; buttons |= (buttons1 & 0x20) != 0 ? HostGamepadButtons.Options : 0;
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0; buttons |= (buttons1 & 0x40) != 0 ? HostGamepadButtons.L3 : 0;
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0; buttons |= (buttons1 & 0x80) != 0 ? HostGamepadButtons.R3 : 0;
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0; buttons |= (buttons2 & 0x02) != 0 ? HostGamepadButtons.TouchPad : 0;
state = new PadState( state = new HostGamepadState(
Connected: true, Connected: true,
Buttons: buttons, Buttons: buttons,
LeftX: leftX, LeftX: leftX,
LeftY: leftY, LeftY: leftY,
RightX: rightX, RightX: rightX,
RightY: rightY, RightY: rightY,
L2: l2, LeftTrigger: l2,
R2: r2); RightTrigger: r2);
return true; return true;
} }
private static uint HatToButtons(int hat) => hat switch private static HostGamepadButtons HatToButtons(int hat) => hat switch
{ {
0 => OrbisPadButton.Up, 0 => HostGamepadButtons.Up,
1 => OrbisPadButton.Up | OrbisPadButton.Right, 1 => HostGamepadButtons.Up | HostGamepadButtons.Right,
2 => OrbisPadButton.Right, 2 => HostGamepadButtons.Right,
3 => OrbisPadButton.Right | OrbisPadButton.Down, 3 => HostGamepadButtons.Right | HostGamepadButtons.Down,
4 => OrbisPadButton.Down, 4 => HostGamepadButtons.Down,
5 => OrbisPadButton.Down | OrbisPadButton.Left, 5 => HostGamepadButtons.Down | HostGamepadButtons.Left,
6 => OrbisPadButton.Left, 6 => HostGamepadButtons.Left,
7 => OrbisPadButton.Left | OrbisPadButton.Up, 7 => HostGamepadButtons.Left | HostGamepadButtons.Up,
_ => 0, _ => 0,
}; };
} }
@@ -4,13 +4,13 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles; using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Pad; namespace SharpEmu.HLE.Host.Windows;
/// <summary> /// <summary>
/// Minimal Win32 HID interop used to talk to a DualSense controller /// Minimal Win32 HID interop used to talk to a DualSense controller
/// directly, without any external input library. /// directly, without any external input library.
/// </summary> /// </summary>
internal static partial class HidNative internal static partial class WindowsHidNative
{ {
internal const int DigcfPresent = 0x02; internal const int DigcfPresent = 0x02;
internal const int DigcfDeviceInterface = 0x10; internal const int DigcfDeviceInterface = 0x10;
@@ -38,28 +38,32 @@ internal static partial class HidNative
public ushort VersionNumber; public ushort VersionNumber;
} }
[DllImport("hid.dll")] [LibraryImport("hid.dll")]
internal static extern void HidD_GetHidGuid(out Guid hidGuid); internal static partial void HidD_GetHidGuid(out Guid hidGuid);
[DllImport("hid.dll")] [LibraryImport("hid.dll")]
internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes); [return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
[DllImport("hid.dll")] [LibraryImport("hid.dll")]
internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength); [return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool HidD_GetFeature(SafeFileHandle hidDeviceObject, [In, Out] byte[] reportBuffer, int reportBufferLength);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)] [LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetClassDevsW")]
internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags); internal static partial nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
[DllImport("setupapi.dll")] [LibraryImport("setupapi.dll")]
internal static extern bool SetupDiEnumDeviceInterfaces( [return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetupDiEnumDeviceInterfaces(
nint deviceInfoSet, nint deviceInfoSet,
nint deviceInfoData, nint deviceInfoData,
ref Guid interfaceClassGuid, ref Guid interfaceClassGuid,
int memberIndex, int memberIndex,
ref SpDeviceInterfaceData deviceInterfaceData); ref SpDeviceInterfaceData deviceInterfaceData);
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)] [LibraryImport("setupapi.dll", EntryPoint = "SetupDiGetDeviceInterfaceDetailW")]
internal static extern bool SetupDiGetDeviceInterfaceDetail( [return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetupDiGetDeviceInterfaceDetail(
nint deviceInfoSet, nint deviceInfoSet,
ref SpDeviceInterfaceData deviceInterfaceData, ref SpDeviceInterfaceData deviceInterfaceData,
nint deviceInterfaceDetailData, nint deviceInterfaceDetailData,
@@ -67,11 +71,12 @@ internal static partial class HidNative
out int requiredSize, out int requiredSize,
nint deviceInfoData); nint deviceInfoData);
[DllImport("setupapi.dll")] [LibraryImport("setupapi.dll")]
internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet); [return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [LibraryImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
internal static extern SafeFileHandle CreateFile( internal static partial SafeFileHandle CreateFile(
string fileName, string fileName,
uint desiredAccess, uint desiredAccess,
uint shareMode, uint shareMode,
@@ -0,0 +1,84 @@
// 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 input backend: DualSense over raw HID plus XInput controllers for gamepads,
/// user32 for the keyboard-fallback queries. Rumble fans out to every reader; lightbar
/// only exists on the DualSense.
/// </summary>
internal sealed partial class WindowsHostInput : IHostInput
{
public void EnsureStarted()
{
WindowsDualSenseReader.EnsureStarted();
WindowsXInputReader.EnsureStarted();
}
public int GetGamepadStates(Span<HostGamepadState> destination)
{
var count = 0;
if (count < destination.Length && WindowsDualSenseReader.TryGetState(out var dualSense))
{
destination[count++] = dualSense;
}
if (count < destination.Length && WindowsXInputReader.TryGetState(out var xinput))
{
destination[count++] = xinput;
}
return count;
}
public string? DescribeConnectedGamepad()
{
if (WindowsDualSenseReader.TryGetState(out _))
{
return "DualSense";
}
return WindowsXInputReader.TryGetState(out _) ? "Xbox controller" : null;
}
public void SetRumble(byte largeMotor, byte smallMotor)
{
WindowsDualSenseReader.SetRumble(largeMotor, smallMotor);
WindowsXInputReader.SetRumble(largeMotor, smallMotor);
}
public void SetTriggerRumble(byte? leftTrigger, byte? rightTrigger) =>
WindowsXInputReader.SetTriggerRumble(leftTrigger, rightTrigger);
public void SetLightbar(byte red, byte green, byte blue) =>
WindowsDualSenseReader.SetLightbar(red, green, blue);
public void ResetLightbar() => WindowsDualSenseReader.ResetLightbar();
public bool IsHostWindowFocused()
{
var foregroundWindow = GetForegroundWindow();
if (foregroundWindow == 0)
{
return false;
}
GetWindowThreadProcessId(foregroundWindow, out var processId);
return processId == (uint)Environment.ProcessId;
}
public bool IsKeyDown(int virtualKey) =>
(GetAsyncKeyState(virtualKey) & 0x8000) != 0;
[LibraryImport("user32.dll")]
private static partial short GetAsyncKeyState(int vKey);
[LibraryImport("user32.dll")]
private static partial nint GetForegroundWindow();
[LibraryImport("user32.dll")]
private static partial uint GetWindowThreadProcessId(nint hWnd, out uint processId);
}
@@ -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,17 @@
// 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();
public IHostAudioOutput Audio { get; } = new WindowsWaveOutAudio();
public IHostInput Input { get; } = new WindowsHostInput();
}
@@ -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,193 @@
// 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;
private static int _timerResolutionRequested;
public void RequestTimerResolution()
{
if (Interlocked.Exchange(ref _timerResolutionRequested, 1) != 0)
{
return;
}
try
{
if (TimeBeginPeriod(1) != 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}");
}
}
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);
[LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
private static partial uint TimeBeginPeriod(uint uPeriod);
}
@@ -0,0 +1,243 @@
// 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 WindowsWaveOutAudio : IHostAudioOutput
{
public string BackendName => "winmm";
public IHostAudioStream OpenStereoPcm16Stream(uint sampleRate) => new WaveOutStream(sampleRate);
private sealed partial class WaveOutStream : IHostAudioStream
{
private const uint WaveMapper = uint.MaxValue;
private const uint CallbackEvent = 0x0005_0000;
private const ushort WaveFormatPcm = 1;
private const uint WaveHeaderDone = 0x0000_0001;
private const int MaximumQueuedPcmBytes = 32 * 1024;
private readonly object _gate = new();
private readonly AutoResetEvent _completion = new(false);
private readonly Queue<NativeBuffer> _buffers = new();
private IntPtr _device;
private int _queuedPcmBytes;
private bool _disposed;
public WaveOutStream(uint sampleRate)
{
var format = new WaveFormat
{
FormatTag = WaveFormatPcm,
Channels = 2,
SamplesPerSecond = sampleRate,
AverageBytesPerSecond = checked(sampleRate * 4),
BlockAlign = 4,
BitsPerSample = 16,
ExtraSize = 0,
};
var result = WaveOutOpen(
out _device,
WaveMapper,
ref format,
_completion.SafeWaitHandle.DangerousGetHandle(),
IntPtr.Zero,
CallbackEvent);
if (result != 0)
{
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
}
}
public bool Submit(ReadOnlySpan<byte> stereoPcm16)
{
lock (_gate)
{
if (_disposed)
{
return false;
}
ReapCompletedBuffers();
while (_queuedPcmBytes != 0 &&
_queuedPcmBytes + stereoPcm16.Length > MaximumQueuedPcmBytes)
{
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
{
return false;
}
ReapCompletedBuffers();
}
return QueueBuffer(stereoPcm16);
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_device != IntPtr.Zero)
{
WaveOutReset(_device);
while (_buffers.TryDequeue(out var buffer))
{
ReleaseBuffer(buffer);
}
WaveOutClose(_device);
_device = IntPtr.Zero;
}
_completion.Dispose();
}
}
private bool QueueBuffer(ReadOnlySpan<byte> data)
{
var dataAddress = Marshal.AllocHGlobal(data.Length);
var headerAddress = IntPtr.Zero;
try
{
unsafe
{
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
}
var header = new WaveHeader
{
Data = dataAddress,
BufferLength = checked((uint)data.Length),
};
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
Marshal.StructureToPtr(header, headerAddress, false);
var result = WaveOutPrepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
return false;
}
result = WaveOutWrite(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
WaveOutUnprepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
return false;
}
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
_queuedPcmBytes += data.Length;
dataAddress = IntPtr.Zero;
headerAddress = IntPtr.Zero;
return true;
}
finally
{
if (headerAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(headerAddress);
}
if (dataAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(dataAddress);
}
}
}
private void ReapCompletedBuffers()
{
while (_buffers.TryPeek(out var buffer))
{
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
if ((header.Flags & WaveHeaderDone) == 0)
{
return;
}
_buffers.Dequeue();
ReleaseBuffer(buffer);
}
}
private void ReleaseBuffer(NativeBuffer buffer)
{
WaveOutUnprepareHeader(
_device,
buffer.Header,
checked((uint)Marshal.SizeOf<WaveHeader>()));
_queuedPcmBytes -= buffer.Length;
Marshal.FreeHGlobal(buffer.Header);
Marshal.FreeHGlobal(buffer.Data);
}
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat
{
public ushort FormatTag;
public ushort Channels;
public uint SamplesPerSecond;
public uint AverageBytesPerSecond;
public ushort BlockAlign;
public ushort BitsPerSample;
public ushort ExtraSize;
}
[StructLayout(LayoutKind.Sequential)]
private struct WaveHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public nuint User;
public uint Flags;
public uint Loops;
public IntPtr Next;
public nuint Reserved;
}
[LibraryImport("winmm.dll", EntryPoint = "waveOutOpen")]
private static partial uint WaveOutOpen(
out IntPtr device,
uint deviceId,
ref WaveFormat format,
IntPtr callback,
IntPtr instance,
uint flags);
[LibraryImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
private static partial uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutWrite")]
private static partial uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
private static partial uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
[LibraryImport("winmm.dll", EntryPoint = "waveOutReset")]
private static partial uint WaveOutReset(IntPtr device);
[LibraryImport("winmm.dll", EntryPoint = "waveOutClose")]
private static partial uint WaveOutClose(IntPtr device);
}
}
@@ -3,15 +3,15 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad; namespace SharpEmu.HLE.Host.Windows;
/// <summary> /// <summary>
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via /// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
/// the Windows XInput API on a background thread, translated to the same /// the Windows XInput API on a background thread, translated to
/// ORBIS pad conventions as <see cref="DualSenseReader"/>. Supports rumble /// <see cref="HostGamepadState"/> conventions. Supports rumble and hot-plug
/// and hot-plug retry; the first connected slot (of four) is used. /// retry; the first connected slot (of four) is used.
/// </summary> /// </summary>
internal static class XInputReader internal static partial class WindowsXInputReader
{ {
private const uint ErrorSuccess = 0; private const uint ErrorSuccess = 0;
private const int SlotCount = 4; private const int SlotCount = 4;
@@ -34,7 +34,7 @@ internal static class XInputReader
private const ushort XinputY = 0x8000; private const ushort XinputY = 0x8000;
private static readonly object Gate = new(); private static readonly object Gate = new();
private static PadState _state; private static HostGamepadState _state;
private static bool _started; private static bool _started;
private static int _slot = -1; // connected XInput user index, -1 when none private static int _slot = -1; // connected XInput user index, -1 when none
private static byte _motorLeft; private static byte _motorLeft;
@@ -45,6 +45,8 @@ internal static class XInputReader
/// <summary>Starts the background reader once; safe to call repeatedly.</summary> /// <summary>Starts the background reader once; safe to call repeatedly.</summary>
internal static void EnsureStarted() internal static void EnsureStarted()
{ {
// The GUI source-links this reader and calls it directly, without the
// host-platform resolution that otherwise guarantees Windows.
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
{ {
return; return;
@@ -67,7 +69,7 @@ internal static class XInputReader
} }
} }
internal static bool TryGetState(out PadState state) internal static bool TryGetState(out HostGamepadState state)
{ {
lock (Gate) lock (Gate)
{ {
@@ -77,7 +79,7 @@ internal static class XInputReader
return state.Connected; return state.Connected;
} }
private static void SetState(in PadState state) private static void SetState(in HostGamepadState state)
{ {
lock (Gate) lock (Gate)
{ {
@@ -204,40 +206,40 @@ internal static class XInputReader
return -1; return -1;
} }
private static PadState Translate(in XInputGamepad pad) private static HostGamepadState Translate(in XInputGamepad pad)
{ {
uint buttons = 0; var buttons = HostGamepadButtons.None;
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0; buttons |= (pad.Buttons & XinputDpadUp) != 0 ? HostGamepadButtons.Up : 0;
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0; buttons |= (pad.Buttons & XinputDpadDown) != 0 ? HostGamepadButtons.Down : 0;
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0; buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? HostGamepadButtons.Left : 0;
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0; buttons |= (pad.Buttons & XinputDpadRight) != 0 ? HostGamepadButtons.Right : 0;
buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0; buttons |= (pad.Buttons & XinputStart) != 0 ? HostGamepadButtons.Options : 0;
buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0; buttons |= (pad.Buttons & XinputBack) != 0 ? HostGamepadButtons.TouchPad : 0;
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0; buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? HostGamepadButtons.L3 : 0;
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0; buttons |= (pad.Buttons & XinputRightThumb) != 0 ? HostGamepadButtons.R3 : 0;
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0; buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? HostGamepadButtons.L1 : 0;
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0; buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? HostGamepadButtons.R1 : 0;
buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0; buttons |= (pad.Buttons & XinputA) != 0 ? HostGamepadButtons.Cross : 0;
buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0; buttons |= (pad.Buttons & XinputB) != 0 ? HostGamepadButtons.Circle : 0;
buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0; buttons |= (pad.Buttons & XinputX) != 0 ? HostGamepadButtons.Square : 0;
buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0; buttons |= (pad.Buttons & XinputY) != 0 ? HostGamepadButtons.Triangle : 0;
buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0; buttons |= pad.LeftTrigger > TriggerThreshold ? HostGamepadButtons.L2 : 0;
buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0; buttons |= pad.RightTrigger > TriggerThreshold ? HostGamepadButtons.R2 : 0;
return new PadState( return new HostGamepadState(
Connected: true, Connected: true,
Buttons: buttons, Buttons: buttons,
LeftX: AxisToByte(pad.ThumbLX), LeftX: AxisToByte(pad.ThumbLX),
LeftY: AxisToByteInverted(pad.ThumbLY), LeftY: AxisToByteInverted(pad.ThumbLY),
RightX: AxisToByte(pad.ThumbRX), RightX: AxisToByte(pad.ThumbRX),
RightY: AxisToByteInverted(pad.ThumbRY), RightY: AxisToByteInverted(pad.ThumbRY),
L2: pad.LeftTrigger, LeftTrigger: pad.LeftTrigger,
R2: pad.RightTrigger); RightTrigger: pad.RightTrigger);
} }
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8); private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
// XInput Y grows upward, ORBIS pads report Y growing downward. // XInput Y grows upward, host pad conventions report Y growing downward.
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8)); private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
@@ -267,9 +269,9 @@ internal static class XInputReader
} }
// xinput1_4.dll ships with Windows 8 and later. // xinput1_4.dll ships with Windows 8 and later.
[DllImport("xinput1_4.dll")] [LibraryImport("xinput1_4.dll")]
private static extern uint XInputGetState(uint userIndex, out XInputState state); private static partial uint XInputGetState(uint userIndex, out XInputState state);
[DllImport("xinput1_4.dll")] [LibraryImport("xinput1_4.dll")]
private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration); private static partial uint XInputSetState(uint userIndex, ref XInputVibration vibration);
} }
+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);
}
@@ -6,4 +6,6 @@ namespace SharpEmu.HLE;
public interface IGuestMemoryAllocator public interface IGuestMemoryAllocator
{ {
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address); bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
bool TryFreeGuestMemory(ulong address);
} }
+170 -39
View File
@@ -29,6 +29,7 @@ public static class AgcExports
private const uint ItDispatchDirect = 0x15; private const uint ItDispatchDirect = 0x15;
private const uint ItDispatchIndirect = 0x16; private const uint ItDispatchIndirect = 0x16;
private const uint ItWaitRegMem = 0x3C; private const uint ItWaitRegMem = 0x3C;
private const uint ItIndirectBuffer = 0x3F;
private const uint ItEventWrite = 0x46; private const uint ItEventWrite = 0x46;
private const uint ItDmaData = 0x50; private const uint ItDmaData = 0x50;
private const uint ItSetContextReg = 0x69; private const uint ItSetContextReg = 0x69;
@@ -157,7 +158,7 @@ public static class AgcExports
private static readonly HashSet<uint> _tracedSubmittedDrawOpcodes = new(); 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 Ps, ulong State, Gen5PixelOutputKind Output), byte[]> _pixelSpirvCache = new();
private static readonly Dictionary< private static readonly Dictionary<
(ulong Es, ulong EsState, ulong Ps, ulong PsState, Gen5PixelOutputKind Output, uint Attributes), (ulong Es, ulong EsState, ulong Ps, ulong PsState, string OutputLayout, uint Attributes),
(byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new(); (byte[] Vertex, byte[] Pixel)> _graphicsSpirvCache = new();
private static readonly Dictionary< private static readonly Dictionary<
(ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ), (ulong Cs, ulong State, uint LocalX, uint LocalY, uint LocalZ),
@@ -1943,6 +1944,96 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress); 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( [SysAbiExport(
Nid = "w2rJhmD+dsE", Nid = "w2rJhmD+dsE",
ExportName = "sceAgcDriverAddEqEvent", ExportName = "sceAgcDriverAddEqEvent",
@@ -3329,12 +3420,13 @@ public static class AgcExports
textures, textures,
globalMemoryBuffers, globalMemoryBuffers,
translatedDraw.AttributeCount, translatedDraw.AttributeCount,
new VulkanGuestRenderTarget( translatedDraw.RenderTargets.Select(target =>
firstTarget.Address, new VulkanGuestRenderTarget(
firstTarget.Width, target.Address,
firstTarget.Height, target.Width,
firstTarget.Format, target.Height,
firstTarget.NumberType), target.Format,
target.NumberType)).ToArray(),
translatedDraw.VertexSpirv, translatedDraw.VertexSpirv,
translatedDraw.VertexCount, translatedDraw.VertexCount,
translatedDraw.InstanceCount, translatedDraw.InstanceCount,
@@ -3462,11 +3554,34 @@ public static class AgcExports
} }
var renderTargets = GetRenderTargets(state.CxRegisters) var renderTargets = GetRenderTargets(state.CxRegisters)
.Where(target => .Where(target => HasPixelColorExport(pixelState, target.Slot))
target.Slot == 0 && .OrderBy(target => target.Slot)
HasPixelColorExport(pixelState, target.Slot))
.ToArray(); .ToArray();
var outputKind = GetPixelOutputKind(renderTargets.FirstOrDefault().NumberType); 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 attributeCount = GetInterpolatedAttributeCount(pixelState);
var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation); var exportStateFingerprint = ComputeShaderStructureFingerprint(exportEvaluation);
var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation); var pixelStateFingerprint = ComputeShaderStructureFingerprint(pixelEvaluation);
@@ -3475,7 +3590,7 @@ public static class AgcExports
exportStateFingerprint, exportStateFingerprint,
pixelShaderAddress, pixelShaderAddress,
pixelStateFingerprint, pixelStateFingerprint,
outputKind, outputLayout,
attributeCount); attributeCount);
var totalGlobalBuffers = var totalGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count + pixelEvaluation.GlobalMemoryBindings.Count +
@@ -3491,7 +3606,7 @@ public static class AgcExports
if (!Gen5SpirvTranslator.TryCompilePixelShader( if (!Gen5SpirvTranslator.TryCompilePixelShader(
pixelState, pixelState,
pixelEvaluation, pixelEvaluation,
outputKind, pixelOutputs,
out var pixelShader, out var pixelShader,
out error, out error,
globalBufferBase: 0, globalBufferBase: 0,
@@ -3579,7 +3694,7 @@ public static class AgcExports
vertexInputs, vertexInputs,
renderTargets, renderTargets,
ApplyTransparentPremultipliedFillClear( ApplyTransparentPremultipliedFillClear(
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()), CreateRenderState(state.CxRegisters, renderTargets, pixelState),
textures, textures,
vertexInputs, vertexInputs,
pixelEvaluation.InitialScalarRegisters)); pixelEvaluation.InitialScalarRegisters));
@@ -3595,7 +3710,8 @@ public static class AgcExports
/// Chowdren resets its effect layers with an untextured transparent-black /// Chowdren resets its effect layers with an untextured transparent-black
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw /// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
/// is otherwise a no-op, causing fog and vignette layers to accumulate. /// 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> /// </summary>
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear( private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
VulkanGuestRenderState renderState, VulkanGuestRenderState renderState,
@@ -3607,13 +3723,7 @@ public static class AgcExports
textures.Count != 0 || textures.Count != 0 ||
vertexInputs.Count != 0 || vertexInputs.Count != 0 ||
pixelUserData.Count < 4 || pixelUserData.Count < 4 ||
renderState.Blend is not !renderState.Blends.All(IsTransparentPremultipliedFillBlend))
{
Enable: true,
ColorSrcFactor: 1,
ColorDstFactor: 5,
ColorFunc: 0,
})
{ {
return renderState; return renderState;
} }
@@ -3628,10 +3738,21 @@ public static class AgcExports
return renderState with 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( private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
CpuContext ctx, CpuContext ctx,
SubmittedDcbState state, SubmittedDcbState state,
@@ -3654,19 +3775,15 @@ public static class AgcExports
: null; : 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) => private static bool HasPixelColorExport(Gen5ShaderState state, uint target) =>
state.Program.Instructions.Any(instruction => GetPixelColorExportMask(state, target) != 0;
instruction.Control is Gen5ExportControl export &&
export.Target == target && private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) =>
export.EnableMask != 0); 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) private static uint GetInterpolatedAttributeCount(Gen5ShaderState state)
{ {
@@ -3824,11 +3941,25 @@ public static class AgcExports
private static VulkanGuestRenderState CreateRenderState( private static VulkanGuestRenderState CreateRenderState(
IReadOnlyDictionary<uint, uint> registers, 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); var scissor = DecodeScissor(registers, target.Width, target.Height);
return new VulkanGuestRenderState( 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, scissor,
DecodeViewport(registers, target.Width, target.Height, scissor)); DecodeViewport(registers, target.Width, target.Height, scissor));
} }
@@ -3853,7 +3984,7 @@ public static class AgcExports
(control >> 24) & 0x1Fu, (control >> 24) & 0x1Fu,
(control >> 21) & 0x7u, (control >> 21) & 0x7u,
((control >> 29) & 1u) != 0, ((control >> 29) & 1u) != 0,
writeMask == 0 ? 0xFu : writeMask); writeMask);
} }
private static VulkanGuestRect? DecodeScissor( private static VulkanGuestRect? DecodeScissor(
@@ -5191,7 +5322,7 @@ public static class AgcExports
if (Gen5SpirvTranslator.TryCompilePixelShader( if (Gen5SpirvTranslator.TryCompilePixelShader(
pixelState, pixelState,
evaluation, evaluation,
Gen5PixelOutputKind.Float, [new(0, 0, Gen5PixelOutputKind.Float)],
out var compiledPixel, out var compiledPixel,
out var compileError)) out var compileError))
{ {
+5
View File
@@ -49,6 +49,11 @@ internal enum Gen5PixelOutputKind
Sint, Sint,
} }
internal readonly record struct Gen5PixelOutputBinding(
uint GuestSlot,
uint HostLocation,
Gen5PixelOutputKind Kind);
internal enum Gen5SpirvStage internal enum Gen5SpirvStage
{ {
Vertex, Vertex,
+98 -33
View File
@@ -19,13 +19,59 @@ internal static partial class Gen5SpirvTranslator
int globalBufferBase = 0, int globalBufferBase = 0,
int totalGlobalBufferCount = -1, 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) 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( var context = new CompilationContext(
Gen5SpirvStage.Pixel, Gen5SpirvStage.Pixel,
state, state,
evaluation, evaluation,
outputKind, outputs,
1, 1,
1, 1,
1, 1,
@@ -50,7 +96,7 @@ internal static partial class Gen5SpirvTranslator
Gen5SpirvStage.Vertex, Gen5SpirvStage.Vertex,
state, state,
evaluation, evaluation,
Gen5PixelOutputKind.Float, [],
1, 1,
1, 1,
1, 1,
@@ -74,7 +120,7 @@ internal static partial class Gen5SpirvTranslator
Gen5SpirvStage.Compute, Gen5SpirvStage.Compute,
state, state,
evaluation, evaluation,
Gen5PixelOutputKind.Float, [],
Math.Max(localSizeX, 1), Math.Max(localSizeX, 1),
Math.Max(localSizeY, 1), Math.Max(localSizeY, 1),
Math.Max(localSizeZ, 1), Math.Max(localSizeZ, 1),
@@ -91,7 +137,7 @@ internal static partial class Gen5SpirvTranslator
private readonly Gen5SpirvStage _stage; private readonly Gen5SpirvStage _stage;
private readonly Gen5ShaderState _state; private readonly Gen5ShaderState _state;
private readonly Gen5ShaderEvaluation _evaluation; private readonly Gen5ShaderEvaluation _evaluation;
private readonly Gen5PixelOutputKind _outputKind; private readonly IReadOnlyList<Gen5PixelOutputBinding> _pixelOutputBindings;
private readonly uint _localSizeX; private readonly uint _localSizeX;
private readonly uint _localSizeY; private readonly uint _localSizeY;
private readonly uint _localSizeZ; private readonly uint _localSizeZ;
@@ -101,6 +147,7 @@ internal static partial class Gen5SpirvTranslator
private readonly int _scalarRegisterBufferIndex; private readonly int _scalarRegisterBufferIndex;
private readonly List<uint> _interfaces = []; private readonly List<uint> _interfaces = [];
private readonly Dictionary<uint, uint> _pixelInputs = []; private readonly Dictionary<uint, uint> _pixelInputs = [];
private readonly Dictionary<uint, SpirvPixelOutput> _pixelOutputs = [];
private readonly Dictionary<uint, uint> _vertexOutputs = []; private readonly Dictionary<uint, uint> _vertexOutputs = [];
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = []; private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
private readonly List<SpirvImageResource> _imageResources = []; private readonly List<SpirvImageResource> _imageResources = [];
@@ -133,7 +180,6 @@ internal static partial class Gen5SpirvTranslator
private uint _lds; private uint _lds;
private uint _workgroupUintPointer; private uint _workgroupUintPointer;
private uint _positionOutput; private uint _positionOutput;
private uint _pixelOutput;
private uint _vertexIndexInput; private uint _vertexIndexInput;
private uint _instanceIndexInput; private uint _instanceIndexInput;
private uint _fragCoordInput; private uint _fragCoordInput;
@@ -163,11 +209,16 @@ internal static partial class Gen5SpirvTranslator
uint Type, uint Type,
uint ComponentCount); uint ComponentCount);
private readonly record struct SpirvPixelOutput(
uint Variable,
uint Type,
Gen5PixelOutputKind Kind);
public CompilationContext( public CompilationContext(
Gen5SpirvStage stage, Gen5SpirvStage stage,
Gen5ShaderState state, Gen5ShaderState state,
Gen5ShaderEvaluation evaluation, Gen5ShaderEvaluation evaluation,
Gen5PixelOutputKind outputKind, IReadOnlyList<Gen5PixelOutputBinding> pixelOutputBindings,
uint localSizeX, uint localSizeX,
uint localSizeY, uint localSizeY,
uint localSizeZ, uint localSizeZ,
@@ -179,7 +230,7 @@ internal static partial class Gen5SpirvTranslator
_stage = stage; _stage = stage;
_state = state; _state = state;
_evaluation = evaluation; _evaluation = evaluation;
_outputKind = outputKind; _pixelOutputBindings = pixelOutputBindings;
_localSizeX = localSizeX; _localSizeX = localSizeX;
_localSizeY = localSizeY; _localSizeY = localSizeY;
_localSizeZ = localSizeZ; _localSizeZ = localSizeZ;
@@ -700,19 +751,24 @@ internal static partial class Gen5SpirvTranslator
(uint)SpirvBuiltIn.FragCoord); (uint)SpirvBuiltIn.FragCoord);
_interfaces.Add(_fragCoordInput); _interfaces.Add(_fragCoordInput);
var outputType = _outputKind switch foreach (var binding in _pixelOutputBindings)
{ {
Gen5PixelOutputKind.Uint => _uvec4Type, var outputType = GetPixelOutputType(binding.Kind);
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4), var outputPointer =
_ => _vec4Type, _module.TypePointer(SpirvStorageClass.Output, outputType);
}; var variable = _module.AddGlobalVariable(
var outputPointer = outputPointer,
_module.TypePointer(SpirvStorageClass.Output, outputType); SpirvStorageClass.Output);
_pixelOutput = _module.AddGlobalVariable( _module.AddName(variable, $"mrt{binding.GuestSlot}");
outputPointer, _module.AddDecoration(
SpirvStorageClass.Output); variable,
_module.AddDecoration(_pixelOutput, SpirvDecoration.Location, 0); SpirvDecoration.Location,
_interfaces.Add(_pixelOutput); binding.HostLocation);
_pixelOutputs.Add(
binding.GuestSlot,
new SpirvPixelOutput(variable, outputType, binding.Kind));
_interfaces.Add(variable);
}
} }
else else
{ {
@@ -831,7 +887,10 @@ internal static partial class Gen5SpirvTranslator
1); 1);
StoreV(2, Bitcast(_uintType, x), guardWithExec: false); StoreV(2, Bitcast(_uintType, x), guardWithExec: false);
StoreV(3, Bitcast(_uintType, y), 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 else
{ {
@@ -2084,21 +2143,27 @@ internal static partial class Gen5SpirvTranslator
if (_stage == Gen5SpirvStage.Pixel) if (_stage == Gen5SpirvStage.Pixel)
{ {
if (export.Target != 0) if (!_pixelOutputs.TryGetValue(export.Target, out var output))
{ {
return true; return true;
} }
var outputType = GetPixelOutputType();
var values = new uint[4]; var values = new uint[4];
for (var component = 0; component < 4; component++) for (var component = 0; component < 4; component++)
{ {
var enabled = (export.EnableMask & (1u << component)) != 0; var enabled = (export.EnableMask & (1u << component)) != 0;
if (!enabled) if (!enabled)
{ {
values[component] = _outputKind == Gen5PixelOutputKind.Sint values[component] = _module.AddInstruction(
? Bitcast(_intType, UInt(0)) SpirvOp.CompositeExtract,
: UInt(0); output.Kind switch
{
Gen5PixelOutputKind.Uint => _uintType,
Gen5PixelOutputKind.Sint => _intType,
_ => _floatType,
},
Load(output.Type, output.Variable),
(uint)component);
continue; continue;
} }
@@ -2107,7 +2172,7 @@ internal static partial class Gen5SpirvTranslator
var value = LoadCompressedExportComponent( var value = LoadCompressedExportComponent(
instruction, instruction,
component); component);
values[component] = _outputKind switch values[component] = output.Kind switch
{ {
Gen5PixelOutputKind.Uint => _module.AddInstruction( Gen5PixelOutputKind.Uint => _module.AddInstruction(
SpirvOp.ConvertFToU, SpirvOp.ConvertFToU,
@@ -2123,7 +2188,7 @@ internal static partial class Gen5SpirvTranslator
} }
var raw = LoadV(instruction.Sources[component].Value); var raw = LoadV(instruction.Sources[component].Value);
values[component] = _outputKind switch values[component] = output.Kind switch
{ {
Gen5PixelOutputKind.Uint => raw, Gen5PixelOutputKind.Uint => raw,
Gen5PixelOutputKind.Sint => Bitcast(_intType, raw), Gen5PixelOutputKind.Sint => Bitcast(_intType, raw),
@@ -2133,15 +2198,15 @@ internal static partial class Gen5SpirvTranslator
var vector = _module.AddInstruction( var vector = _module.AddInstruction(
SpirvOp.CompositeConstruct, SpirvOp.CompositeConstruct,
outputType, output.Type,
values); values);
vector = _module.AddInstruction( vector = _module.AddInstruction(
SpirvOp.Select, SpirvOp.Select,
outputType, output.Type,
Load(_boolType, _exec), Load(_boolType, _exec),
vector, vector,
Load(outputType, _pixelOutput)); Load(output.Type, output.Variable));
Store(_pixelOutput, vector); Store(output.Variable, vector);
return true; return true;
} }
@@ -2209,8 +2274,8 @@ internal static partial class Gen5SpirvTranslator
(uint)(component & 1)); (uint)(component & 1));
} }
private uint GetPixelOutputType() => private uint GetPixelOutputType(Gen5PixelOutputKind kind) =>
_outputKind switch kind switch
{ {
Gen5PixelOutputKind.Uint => _uvec4Type, Gen5PixelOutputKind.Uint => _uvec4Type,
Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4), Gen5PixelOutputKind.Sint => _module.TypeVector(_intType, 4),
+46
View File
@@ -348,6 +348,18 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "C+IEj+BsAFM", Nid = "C+IEj+BsAFM",
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion", ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
@@ -440,6 +452,40 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "sJXyWHjP-F8", Nid = "sJXyWHjP-F8",
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion", ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
+31 -10
View File
@@ -54,20 +54,41 @@ internal static class PakDirectoryTracker
if (state.Directory is { } entries) 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) foreach (var entry in entries)
{ {
if (!entry.Consumed && entry.FileLen == requestedSize) if (entry.Consumed || entry.FileLen != requestedSize)
{ {
entry.Consumed = true; continue;
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;
} }
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;
} }
} }
+30 -12
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using System.Buffers; using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
@@ -27,7 +28,7 @@ public static class AudioOutExports
int channels, int channels,
int bytesPerSample, int bytesPerSample,
bool isFloat, bool isFloat,
WinMmAudioPort? backend) IHostAudioStream? backend)
{ {
UserId = userId; UserId = userId;
Type = type; Type = type;
@@ -48,7 +49,7 @@ public static class AudioOutExports
public int Channels { get; } public int Channels { get; }
public int BytesPerSample { get; } public int BytesPerSample { get; }
public bool IsFloat { get; } public bool IsFloat { get; }
public WinMmAudioPort? Backend { get; } public IHostAudioStream? Backend { get; }
public int BufferByteLength => public int BufferByteLength =>
checked((int)BufferLength * Channels * BytesPerSample); checked((int)BufferLength * Channels * BytesPerSample);
@@ -103,12 +104,13 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
WinMmAudioPort? backend = null; IHostAudioStream? backend = null;
string backendName; string backendName;
try try
{ {
backend = new WinMmAudioPort(frequency); var audio = HostPlatform.Current.Audio;
backendName = "winmm"; backend = audio.OpenStereoPcm16Stream(frequency);
backendName = audio.BackendName;
} }
catch (Exception exception) catch (Exception exception)
{ {
@@ -180,15 +182,31 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (port.Backend is null || if (port.Backend is null)
!port.Backend.Submit(
source,
port.BufferLength,
port.Channels,
port.BytesPerSample,
port.IsFloat))
{ {
port.PaceSilence(); port.PaceSilence();
return ctx.SetReturn(0);
}
var outputLength = checked((int)port.BufferLength * AudioPcmConversion.OutputFrameSize);
var output = ArrayPool<byte>.Shared.Rent(outputLength);
try
{
AudioPcmConversion.ConvertToStereoPcm16(
source,
output.AsSpan(0, outputLength),
checked((int)port.BufferLength),
port.Channels,
port.BytesPerSample,
port.IsFloat);
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
{
port.PaceSilence();
}
}
finally
{
ArrayPool<byte>.Shared.Return(output);
} }
return ctx.SetReturn(0); return ctx.SetReturn(0);
@@ -0,0 +1,55 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
namespace SharpEmu.Libs.Audio;
/// <summary>
/// Converts guest AudioOut submissions (mono/stereo/7.1, s16 or float32) into the
/// interleaved stereo 16-bit PCM that host audio streams accept. Platform-neutral —
/// device specifics live behind IHostAudioStream.
/// </summary>
internal static class AudioPcmConversion
{
/// <summary>Bytes per output frame: two 16-bit channels.</summary>
public const int OutputFrameSize = 4;
public static void ConvertToStereoPcm16(
ReadOnlySpan<byte> source,
Span<byte> destination,
int frames,
int channels,
int bytesPerSample,
bool isFloat)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
{
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
}
}
private static short ReadSample(
ReadOnlySpan<byte> frame,
int channel,
int bytesPerSample,
bool isFloat)
{
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
if (!isFloat)
{
return BinaryPrimitives.ReadInt16LittleEndian(sample);
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
}
-302
View File
@@ -1,302 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Audio;
internal sealed class WinMmAudioPort : IDisposable
{
private const uint WaveMapper = uint.MaxValue;
private const uint CallbackEvent = 0x0005_0000;
private const ushort WaveFormatPcm = 1;
private const uint WaveHeaderDone = 0x0000_0001;
private const int MaximumQueuedPcmBytes = 32 * 1024;
private readonly object _gate = new();
private readonly AutoResetEvent _completion = new(false);
private readonly Queue<NativeBuffer> _buffers = new();
private IntPtr _device;
private int _queuedPcmBytes;
private bool _disposed;
public WinMmAudioPort(uint sampleRate)
{
if (!OperatingSystem.IsWindows())
{
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
}
var format = new WaveFormat
{
FormatTag = WaveFormatPcm,
Channels = 2,
SamplesPerSecond = sampleRate,
AverageBytesPerSecond = checked(sampleRate * 4),
BlockAlign = 4,
BitsPerSample = 16,
ExtraSize = 0,
};
var result = WaveOutOpen(
out _device,
WaveMapper,
ref format,
_completion.SafeWaitHandle.DangerousGetHandle(),
IntPtr.Zero,
CallbackEvent);
if (result != 0)
{
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
}
}
public bool Submit(
ReadOnlySpan<byte> source,
uint frames,
int channels,
int bytesPerSample,
bool isFloat)
{
lock (_gate)
{
if (_disposed)
{
return false;
}
var outputLength = checked((int)frames * 4);
ReapCompletedBuffers();
while (_queuedPcmBytes != 0 &&
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
{
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
{
return false;
}
ReapCompletedBuffers();
}
var output = ArrayPool<byte>.Shared.Rent(outputLength);
try
{
ConvertToStereoPcm16(
source,
output.AsSpan(0, outputLength),
checked((int)frames),
channels,
bytesPerSample,
isFloat);
return QueueBuffer(output.AsSpan(0, outputLength));
}
finally
{
ArrayPool<byte>.Shared.Return(output);
}
}
}
public void Dispose()
{
lock (_gate)
{
if (_disposed)
{
return;
}
_disposed = true;
if (_device != IntPtr.Zero)
{
WaveOutReset(_device);
while (_buffers.TryDequeue(out var buffer))
{
ReleaseBuffer(buffer);
}
WaveOutClose(_device);
_device = IntPtr.Zero;
}
_completion.Dispose();
}
}
private bool QueueBuffer(ReadOnlySpan<byte> data)
{
var dataAddress = Marshal.AllocHGlobal(data.Length);
var headerAddress = IntPtr.Zero;
try
{
unsafe
{
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
}
var header = new WaveHeader
{
Data = dataAddress,
BufferLength = checked((uint)data.Length),
};
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
Marshal.StructureToPtr(header, headerAddress, false);
var result = WaveOutPrepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
return false;
}
result = WaveOutWrite(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
if (result != 0)
{
WaveOutUnprepareHeader(
_device,
headerAddress,
checked((uint)Marshal.SizeOf<WaveHeader>()));
return false;
}
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
_queuedPcmBytes += data.Length;
dataAddress = IntPtr.Zero;
headerAddress = IntPtr.Zero;
return true;
}
finally
{
if (headerAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(headerAddress);
}
if (dataAddress != IntPtr.Zero)
{
Marshal.FreeHGlobal(dataAddress);
}
}
}
private void ReapCompletedBuffers()
{
while (_buffers.TryPeek(out var buffer))
{
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
if ((header.Flags & WaveHeaderDone) == 0)
{
return;
}
_buffers.Dequeue();
ReleaseBuffer(buffer);
}
}
private void ReleaseBuffer(NativeBuffer buffer)
{
WaveOutUnprepareHeader(
_device,
buffer.Header,
checked((uint)Marshal.SizeOf<WaveHeader>()));
_queuedPcmBytes -= buffer.Length;
Marshal.FreeHGlobal(buffer.Header);
Marshal.FreeHGlobal(buffer.Data);
}
private static void ConvertToStereoPcm16(
ReadOnlySpan<byte> source,
Span<byte> destination,
int frames,
int channels,
int bytesPerSample,
bool isFloat)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
{
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
}
}
private static short ReadSample(
ReadOnlySpan<byte> frame,
int channel,
int bytesPerSample,
bool isFloat)
{
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
if (!isFloat)
{
return BinaryPrimitives.ReadInt16LittleEndian(sample);
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
}
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
[StructLayout(LayoutKind.Sequential, Pack = 2)]
private struct WaveFormat
{
public ushort FormatTag;
public ushort Channels;
public uint SamplesPerSecond;
public uint AverageBytesPerSecond;
public ushort BlockAlign;
public ushort BitsPerSample;
public ushort ExtraSize;
}
[StructLayout(LayoutKind.Sequential)]
private struct WaveHeader
{
public IntPtr Data;
public uint BufferLength;
public uint BytesRecorded;
public nuint User;
public uint Flags;
public uint Loops;
public IntPtr Next;
public nuint Reserved;
}
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
private static extern uint WaveOutOpen(
out IntPtr device,
uint deviceId,
ref WaveFormat format,
IntPtr callback,
IntPtr instance,
uint flags);
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
private static extern uint WaveOutReset(IntPtr device);
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
private static extern uint WaveOutClose(IntPtr device);
}
-46
View File
@@ -1,46 +0,0 @@
// 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);
}
+24
View File
@@ -78,6 +78,30 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "WSOuge5IsCg", Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json14InitParameter2C1Ev", ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
+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;
}
}
@@ -34,9 +34,43 @@ public static class KernelEventFlagCompatExports
public object Gate { get; } = new(); public object Gate { get; } = new();
} }
private sealed class EventFlagWaiter private sealed class EventFlagWaiter : IGuestThreadBlockWaiter
{ {
public required CpuContext Ctx { get; init; }
public required EventFlagState State { get; init; }
public required ulong Pattern { get; init; }
public required uint WaitMode { get; init; }
public required ulong ResultAddress { get; init; }
public bool Timed { get; init; }
// Timed-wait completion state; unused when Timed is false.
public ulong TimeoutAddress { get; init; }
public long DeadlineTimestamp { get; init; }
public OrbisGen2Result? Result { get; set; } public OrbisGen2Result? Result { get; set; }
// Untimed waits stash the prepared result here at wake and return it at resume.
private OrbisGen2Result _blockedResult = OrbisGen2Result.ORBIS_GEN2_OK;
public int Resume() => Timed
? CompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress, TimeoutAddress, DeadlineTimestamp)
: (int)_blockedResult;
public bool TryWake()
{
if (Timed)
{
return TryCompleteBlockedTimedWait(Ctx, State, this, Pattern, WaitMode, ResultAddress);
}
if (!TryPrepareBlockedWait(Ctx, State, Pattern, WaitMode, ResultAddress, out var preparedResult))
{
return false;
}
_blockedResult = preparedResult;
return true;
}
} }
[SysAbiExport( [SysAbiExport(
@@ -249,27 +283,22 @@ public static class KernelEventFlagCompatExports
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp( var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
TimeSpan.FromTicks((long)timeoutUsec * 10L)); TimeSpan.FromTicks((long)timeoutUsec * 10L));
var timedWaiter = new EventFlagWaiter(); var timedWaiter = new EventFlagWaiter
{
Ctx = ctx,
State = state,
Pattern = pattern,
WaitMode = waitMode,
ResultAddress = resultAddress,
Timed = true,
TimeoutAddress = timeoutAddress,
DeadlineTimestamp = deadline,
};
if (GuestThreadExecution.RequestCurrentThreadBlock( if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx, ctx,
"sceKernelWaitEventFlag", "sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle), GetEventFlagWakeKey(handle),
resumeHandler: () => CompleteBlockedTimedWait( timedWaiter,
ctx,
state,
timedWaiter,
pattern,
waitMode,
resultAddress,
timeoutAddress,
deadline),
wakeHandler: () => TryCompleteBlockedTimedWait(
ctx,
state,
timedWaiter,
pattern,
waitMode,
resultAddress),
blockDeadlineTimestamp: deadline)) blockDeadlineTimestamp: deadline))
{ {
state.WaitingThreads++; state.WaitingThreads++;
@@ -286,27 +315,17 @@ public static class KernelEventFlagCompatExports
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle; var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx); var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId; var managedThread = Environment.CurrentManagedThreadId;
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock( var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx, ctx,
"sceKernelWaitEventFlag", "sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle), GetEventFlagWakeKey(handle),
() => (int)blockedWaitResult, new EventFlagWaiter
() =>
{ {
if (!TryPrepareBlockedWait( Ctx = ctx,
ctx, State = state,
state, Pattern = pattern,
pattern, WaitMode = waitMode,
waitMode, ResultAddress = resultAddress,
resultAddress,
out var preparedResult))
{
return false;
}
blockedWaitResult = preparedResult;
return true;
}); });
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}"); TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}"); TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
@@ -2,7 +2,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading; using System.Threading;
namespace SharpEmu.Libs.Kernel; namespace SharpEmu.Libs.Kernel;
@@ -17,7 +19,7 @@ public static class KernelEventQueueCompatExports
private static readonly object _eventQueueGate = new(); private static readonly object _eventQueueGate = new();
private static readonly HashSet<ulong> _eventQueues = new(); private static readonly HashSet<ulong> _eventQueues = new();
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new(); private static readonly Dictionary<ulong, KernelEventDeque> _pendingEvents = new();
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new(); private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
private static long _nextEventQueueHandle = 1; private static long _nextEventQueueHandle = 1;
@@ -34,6 +36,76 @@ public static class KernelEventQueueCompatExports
short Filter, short Filter,
ulong UserData); ulong UserData);
// Grow-only ring buffer standing in for LinkedList<KernelQueuedEvent>, which
// allocated a node per enqueue — steady churn at one enqueue per vblank/flip edge
// per registered queue. Mutated only under _eventQueueGate.
private sealed class KernelEventDeque
{
private KernelQueuedEvent[] _items = new KernelQueuedEvent[4];
private int _head;
public int Count { get; private set; }
public KernelQueuedEvent this[int index]
{
get => _items[(_head + index) % _items.Length];
set => _items[(_head + index) % _items.Length] = value;
}
public void AddLast(in KernelQueuedEvent item)
{
if (Count == _items.Length)
{
var grown = new KernelQueuedEvent[_items.Length * 2];
for (var i = 0; i < Count; i++)
{
grown[i] = this[i];
}
_items = grown;
_head = 0;
}
_items[(_head + Count) % _items.Length] = item;
Count++;
}
public KernelQueuedEvent RemoveFirst()
{
var value = _items[_head];
_head = (_head + 1) % _items.Length;
Count--;
return value;
}
public int FindIndex(ulong ident, short filter)
{
for (var i = 0; i < Count; i++)
{
var candidate = this[i];
if (candidate.Ident == ident && candidate.Filter == filter)
{
return i;
}
}
return -1;
}
}
private sealed class EqueueWaiter : IGuestThreadBlockWaiter
{
public required CpuContext Ctx { get; init; }
public required ulong Handle { get; init; }
public required ulong EventsAddress { get; init; }
public required int EventCapacity { get; init; }
public required ulong OutCountAddress { get; init; }
public int Resume() => ResumeWaitEqueue(Ctx, Handle, EventsAddress, EventCapacity, OutCountAddress);
public bool TryWake() => HasPendingEvents(Handle);
}
[SysAbiExport( [SysAbiExport(
Nid = "D0OdFMjp46I", Nid = "D0OdFMjp46I",
ExportName = "sceKernelCreateEqueue", ExportName = "sceKernelCreateEqueue",
@@ -51,7 +123,7 @@ public static class KernelEventQueueCompatExports
lock (_eventQueueGate) lock (_eventQueueGate)
{ {
_eventQueues.Add(handle); _eventQueues.Add(handle);
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>(); _pendingEvents[handle] = new KernelEventDeque();
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>(); _registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
} }
@@ -79,6 +151,8 @@ public static class KernelEventQueueCompatExports
_registeredEvents.Remove(handle); _registeredEvents.Remove(handle);
} }
_wakeKeys.TryRemove(handle, out _);
TraceEventQueue(ctx, "delete", handle); TraceEventQueue(ctx, "delete", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -343,8 +417,14 @@ public static class KernelEventQueueCompatExports
ctx, ctx,
"sceKernelWaitEqueue", "sceKernelWaitEqueue",
GetEventQueueWakeKey(handle), GetEventQueueWakeKey(handle),
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress), new EqueueWaiter
() => HasPendingEvents(handle))) {
Ctx = ctx,
Handle = handle,
EventsAddress = eventsAddress,
EventCapacity = eventCapacity,
OutCountAddress = outCountAddress,
}))
{ {
TraceEventQueue(ctx, "wait-block", handle); TraceEventQueue(ctx, "wait-block", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -409,7 +489,7 @@ public static class KernelEventQueueCompatExports
if (!_pendingEvents.TryGetValue(handle, out var queue)) if (!_pendingEvents.TryGetValue(handle, out var queue))
{ {
queue = new LinkedList<KernelQueuedEvent>(); queue = new KernelEventDeque();
_pendingEvents[handle] = queue; _pendingEvents[handle] = queue;
} }
@@ -480,7 +560,7 @@ public static class KernelEventQueueCompatExports
if (!_pendingEvents.TryGetValue(handle, out var queue)) if (!_pendingEvents.TryGetValue(handle, out var queue))
{ {
queue = new LinkedList<KernelQueuedEvent>(); queue = new KernelEventDeque();
_pendingEvents[handle] = queue; _pendingEvents[handle] = queue;
} }
@@ -527,7 +607,7 @@ public static class KernelEventQueueCompatExports
if (!_pendingEvents.TryGetValue(handle, out var queue)) if (!_pendingEvents.TryGetValue(handle, out var queue))
{ {
queue = new LinkedList<KernelQueuedEvent>(); queue = new KernelEventDeque();
_pendingEvents[handle] = queue; _pendingEvents[handle] = queue;
} }
@@ -563,15 +643,15 @@ public static class KernelEventQueueCompatExports
if (!_pendingEvents.TryGetValue(handle, out var events)) if (!_pendingEvents.TryGetValue(handle, out var events))
{ {
events = new LinkedList<KernelQueuedEvent>(); events = new KernelEventDeque();
_pendingEvents[handle] = events; _pendingEvents[handle] = events;
} }
var count = 1UL; var count = 1UL;
var pendingNode = FindPendingEvent(events, ident, filter); var pendingIndex = events.FindIndex(ident, filter);
if (pendingNode is not null) if (pendingIndex >= 0)
{ {
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL); count = Math.Min(((events[pendingIndex].Data >> 12) & 0xFUL) + 1, 0xFUL);
} }
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL; var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
@@ -584,9 +664,9 @@ public static class KernelEventQueueCompatExports
eventData, eventData,
userData); userData);
if (pendingNode is not null) if (pendingIndex >= 0)
{ {
pendingNode.Value = triggeredEvent; events[pendingIndex] = triggeredEvent;
} }
else else
{ {
@@ -631,40 +711,28 @@ public static class KernelEventQueueCompatExports
} }
private static void QueueOrUpdateEvent( private static void QueueOrUpdateEvent(
LinkedList<KernelQueuedEvent> queue, KernelEventDeque queue,
KernelQueuedEvent queuedEvent) KernelQueuedEvent queuedEvent)
{ {
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter); var pendingIndex = queue.FindIndex(queuedEvent.Ident, queuedEvent.Filter);
if (pendingNode is null) if (pendingIndex < 0)
{ {
queue.AddLast(queuedEvent); queue.AddLast(queuedEvent);
return; return;
} }
pendingNode.Value = queuedEvent with queue[pendingIndex] = queuedEvent with
{ {
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags), Fflags = Math.Max(queue[pendingIndex].Fflags + 1, queuedEvent.Fflags),
}; };
} }
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent( // Wake keys are formatted once per handle: WakeEventQueue runs on every event
LinkedList<KernelQueuedEvent> queue, // enqueue (vblank/flip edges included), so formatting there is steady string churn.
ulong ident, private static readonly ConcurrentDictionary<ulong, string> _wakeKeys = new();
short filter)
{
for (var node = queue.First; node is not null; node = node.Next)
{
if (node.Value.Ident == ident && node.Value.Filter == filter)
{
return node;
}
}
return null;
}
private static string GetEventQueueWakeKey(ulong handle) => private static string GetEventQueueWakeKey(ulong handle) =>
$"sceKernelWaitEqueue:{handle:X16}"; _wakeKeys.GetOrAdd(handle, static h => $"sceKernelWaitEqueue:{h:X16}");
private static void WakeEventQueue(ulong handle) private static void WakeEventQueue(ulong handle)
{ {
@@ -678,7 +746,10 @@ public static class KernelEventQueueCompatExports
return 0; return 0;
} }
// Engines wait on the vblank/flip equeue every frame, so the delivery buffer
// (usually a single event) comes from the pool instead of a per-call array.
KernelQueuedEvent[] events; KernelQueuedEvent[] events;
int count;
lock (_eventQueueGate) lock (_eventQueueGate)
{ {
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0) if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
@@ -686,24 +757,30 @@ public static class KernelEventQueueCompatExports
return 0; return 0;
} }
var count = Math.Min(eventCapacity, queue.Count); count = Math.Min(eventCapacity, queue.Count);
events = new KernelQueuedEvent[count]; events = ArrayPool<KernelQueuedEvent>.Shared.Rent(count);
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
events[i] = queue.First!.Value; events[i] = queue.RemoveFirst();
queue.RemoveFirst();
} }
} }
for (var i = 0; i < events.Length; i++) try
{ {
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i])) for (var i = 0; i < count; i++)
{ {
return i; if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
{
return i;
}
} }
} }
finally
{
ArrayPool<KernelQueuedEvent>.Shared.Return(events);
}
return events.Length; return count;
} }
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent) private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
@@ -718,9 +795,12 @@ public static class KernelEventQueueCompatExports
return ctx.Memory.TryWrite(address, eventBytes); return ctx.Memory.TryWrite(address, eventBytes);
} }
private static readonly bool _logEqueue =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal);
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle) private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal)) if (!_logEqueue)
{ {
return; return;
} }
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Ampr; using SharpEmu.Libs.Ampr;
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
@@ -17,6 +18,10 @@ public static class KernelMemoryCompatExports
private const int MaxGuestStringLength = 4096; private const int MaxGuestStringLength = 4096;
private const int WideCharSize = sizeof(ushort); private const int WideCharSize = sizeof(ushort);
private const int MemsetChunkSize = 16 * 1024; private const int MemsetChunkSize = 16 * 1024;
private const int MemcpyChunkSize = 256 * 1024;
// Shared all-zero scratch for chunked zero-fill loops; never written to.
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
private const int TlsModuleBlockSize = 0x10000; private const int TlsModuleBlockSize = 0x10000;
private const int O_WRONLY = 0x1; private const int O_WRONLY = 0x1;
private const int O_RDWR = 0x2; private const int O_RDWR = 0x2;
@@ -52,14 +57,13 @@ public static class KernelMemoryCompatExports
private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024; private const ulong FlexibleMemorySizeBytes = 448UL * 1024 * 1024;
private const int OrbisVirtualQueryInfoSize = 72; private const int OrbisVirtualQueryInfoSize = 72;
private const int OrbisKernelMaximumNameLength = 32; private const int OrbisKernelMaximumNameLength = 32;
private const uint MemCommit = 0x1000; // Raw Windows PAGE_* values used only against HostRegionInfo.RawProtection,
private const uint MemReserve = 0x2000; // which by contract carries the untranslated protection word of the host
private const uint MemRelease = 0x8000; // platform in use (see IHostMemory).
private const uint HostPageNoAccess = 0x01; private const uint HostPageNoAccess = 0x01;
private const uint HostPageReadOnly = 0x02; private const uint HostPageReadOnly = 0x02;
private const uint HostPageReadWrite = 0x04; private const uint HostPageReadWrite = 0x04;
private const uint HostPageWriteCopy = 0x08; private const uint HostPageWriteCopy = 0x08;
private const uint HostPageExecute = 0x10;
private const uint HostPageExecuteRead = 0x20; private const uint HostPageExecuteRead = 0x20;
private const uint HostPageExecuteReadWrite = 0x40; private const uint HostPageExecuteReadWrite = 0x40;
private const uint HostPageExecuteWriteCopy = 0x80; private const uint HostPageExecuteWriteCopy = 0x80;
@@ -136,31 +140,9 @@ public static class KernelMemoryCompatExports
private static string? _cachedApp0Root; private static string? _cachedApp0Root;
private static string? _cachedDownload0Root; private static string? _cachedDownload0Root;
[StructLayout(LayoutKind.Sequential)] // Property (not a cached field) so merely touching this type never resolves
private struct MemoryBasicInformation // the platform backend; non-Windows hosts only throw if a call is reached.
{ private static IHostMemory HostMemory => HostPlatform.Current.Memory;
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);
private sealed class OpenDirectory private sealed class OpenDirectory
{ {
@@ -277,11 +259,10 @@ public static class KernelMemoryCompatExports
DirectStart: 0); DirectStart: 0);
} }
var zeroes = new byte[(int)Math.Min(mappedLength, (ulong)MemsetChunkSize)];
for (ulong offset = 0; offset < mappedLength;) for (ulong offset = 0; offset < mappedLength;)
{ {
var chunkLength = (int)Math.Min((ulong)zeroes.Length, mappedLength - offset); var chunkLength = (int)Math.Min((ulong)_zeroChunk.Length, mappedLength - offset);
if (!ctx.Memory.TryWrite(address + offset, zeroes.AsSpan(0, chunkLength))) if (!ctx.Memory.TryWrite(address + offset, _zeroChunk.AsSpan(0, chunkLength)))
{ {
return false; return false;
} }
@@ -444,33 +425,50 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
var chunk = new byte[MemsetChunkSize]; // Rent may hand back a larger array than requested; only the first chunkLength
Array.Fill(chunk, value); // bytes are filled, so the loop must cap at chunkLength rather than chunk.Length.
var remaining = length; var chunkLength = (int)Math.Min(length, (ulong)MemsetChunkSize);
var cursor = destination; var chunk = value == 0 ? _zeroChunk : ArrayPool<byte>.Shared.Rent(chunkLength);
while (remaining > 0) if (value != 0)
{ {
var take = (int)Math.Min((ulong)chunk.Length, remaining); chunk.AsSpan(0, chunkLength).Fill(value);
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take))) }
try
{
var remaining = length;
var cursor = destination;
while (remaining > 0)
{ {
if (length <= 0x40) var take = (int)Math.Min((ulong)chunkLength, remaining);
if (!TryWriteCompat(ctx, cursor, chunk.AsSpan(0, take)))
{ {
var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount); if (length <= 0x40)
if (recoveryIndex <= 8)
{ {
Console.Error.WriteLine( var recoveryIndex = Interlocked.Increment(ref _inaccessibleMemsetRecoveryCount);
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}"); if (recoveryIndex <= 8)
{
Console.Error.WriteLine(
$"[LOADER][WARNING] memset inaccessible-dst recovery#{recoveryIndex}: rip=0x{ctx.Rip:X16} dst=0x{destination:X16} len=0x{length:X} val=0x{value:X2}");
}
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
ctx[CpuRegister.Rax] = destination; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; cursor += (ulong)take;
remaining -= (ulong)take;
}
}
finally
{
if (value != 0)
{
ArrayPool<byte>.Shared.Return(chunk);
} }
cursor += (ulong)take;
remaining -= (ulong)take;
} }
ctx[CpuRegister.Rax] = destination; ctx[CpuRegister.Rax] = destination;
@@ -1207,11 +1205,10 @@ public static class KernelMemoryCompatExports
var rawCount = ctx[CpuRegister.Rdx]; var rawCount = ctx[CpuRegister.Rdx];
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified // A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
// upstream bug that also feeds bad lengths to memset) must not reach // upstream bug that also feeds bad lengths to memset) must not turn into a multi-GB
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread // copy attempt from a guest-thread call context, which corrupted the CLR outright
// call context corrupted the CLR outright ("Invalid Program: attempted to call a // ("Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
// UnmanagedCallersOnly method from managed code") instead of throwing a normal // code") instead of throwing a normal exception. Reject anything above a sane bound.
// exception. Reject anything above a sane bound before allocating.
const ulong maxSaneCount = 512UL * 1024 * 1024; const ulong maxSaneCount = 512UL * 1024 * 1024;
if (rawCount > maxSaneCount) if (rawCount > maxSaneCount)
{ {
@@ -1220,15 +1217,52 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
var count = (int)rawCount; ctx[CpuRegister.Rax] = destination;
var payload = GC.AllocateUninitializedArray<byte>(count); if (rawCount == 0)
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
{ {
ctx[CpuRegister.Rax] = destination; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; }
// Cap iterations at the requested chunk size, not chunk.Length: Rent may hand
// back a larger array, and the copy granularity should not depend on pool
// bucketing internals.
var chunkLength = (int)Math.Min(rawCount, (ulong)MemcpyChunkSize);
var chunk = ArrayPool<byte>.Shared.Rent(chunkLength);
try
{
// memmove aliases this export, so overlapping ranges must survive the chunked
// copy: when the destination starts inside the source range, copy high-to-low
// so no source byte is overwritten before it has been read.
var copyBackward = destination > source && destination - source < rawCount;
var remaining = rawCount;
ulong offset = copyBackward ? rawCount : 0;
while (remaining > 0)
{
var take = (int)Math.Min((ulong)chunkLength, remaining);
if (copyBackward)
{
offset -= (ulong)take;
}
var span = chunk.AsSpan(0, take);
if (!TryReadCompat(ctx, source + offset, span) || !TryWriteCompat(ctx, destination + offset, span))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!copyBackward)
{
offset += (ulong)take;
}
remaining -= (ulong)take;
}
}
finally
{
ArrayPool<byte>.Shared.Return(chunk);
} }
ctx[CpuRegister.Rax] = destination;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -1856,6 +1890,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( [SysAbiExport(
Nid = "NNtFaKJbPt0", Nid = "NNtFaKJbPt0",
ExportName = "_close", ExportName = "_close",
@@ -3563,7 +3636,7 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; 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; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
} }
@@ -3597,7 +3670,7 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; 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; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
} }
@@ -5797,42 +5870,40 @@ public static class KernelMemoryCompatExports
return alignedLength != 0; 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) if (length == 0 || length > nuint.MaxValue)
{ {
return false; return false;
} }
var hostProtection = ResolveHostProtection(orbisProtection); if (!KernelVirtualRangeAllocator.TryResolveAddressSpace(ctx.Memory, out var addressSpace))
if (!VirtualProtect((nint)address, (nuint)length, hostProtection, out _))
{ {
return false; 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 protection = GuestPageProtection.None;
var write = (orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0; if ((orbisProtection & (OrbisProtCpuRead | OrbisProtGpuRead)) != 0)
var execute = (orbisProtection & OrbisProtCpuExec) != 0;
if (execute)
{ {
return write protection |= GuestPageProtection.Read;
? HostPageExecuteReadWrite
: read
? HostPageExecuteRead
: HostPageExecute;
} }
return write if ((orbisProtection & (OrbisProtCpuWrite | OrbisProtGpuWrite)) != 0)
? HostPageReadWrite {
: read protection |= GuestPageProtection.Write;
? HostPageReadOnly }
: HostPageNoAccess;
if ((orbisProtection & OrbisProtCpuExec) != 0)
{
protection |= GuestPageProtection.Execute;
}
return protection;
} }
private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region) private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region)
@@ -6218,7 +6289,7 @@ public static class KernelMemoryCompatExports
var effectiveAlignment = Math.Max(alignment, pageSize); var effectiveAlignment = Math.Max(alignment, pageSize);
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize)); var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + 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) if (baseAddress == 0)
{ {
return false; return false;
@@ -6226,9 +6297,9 @@ public static class KernelMemoryCompatExports
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment); var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
var guardAddress = alignedAddress + (ulong)usableSize; 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; return false;
} }
@@ -6363,7 +6434,7 @@ public static class KernelMemoryCompatExports
if (allocation.IsGuarded) if (allocation.IsGuarded)
{ {
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease); _ = HostMemory.Free(unchecked((ulong)allocation.BaseAddress));
} }
else else
{ {
@@ -6459,7 +6530,7 @@ public static class KernelMemoryCompatExports
return false; return false;
} }
if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.Protect, writeAccess)) if (!TryQueryHostPage(address, out var startInfo) || !HasRequiredProtection(startInfo.RawProtection, writeAccess))
{ {
return false; return false;
} }
@@ -6470,7 +6541,7 @@ public static class KernelMemoryCompatExports
return true; return true;
} }
if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.Protect, writeAccess)) if (!TryQueryHostPage(endAddress, out var endInfo) || !HasRequiredProtection(endInfo.RawProtection, writeAccess))
{ {
return false; return false;
} }
@@ -6478,16 +6549,14 @@ public static class KernelMemoryCompatExports
return true; return true;
} }
private static bool TryQueryHostPage(ulong address, out MemoryBasicInformation info) private static bool TryQueryHostPage(ulong address, out HostRegionInfo info)
{ {
info = default; if (!HostMemory.Query(address, out info))
var size = (nuint)Marshal.SizeOf<MemoryBasicInformation>();
if (VirtualQuery((nint)address, out info, size) == 0)
{ {
return false; return false;
} }
return info.State == MemCommit; return info.State == HostRegionState.Committed;
} }
private static bool HasRequiredProtection(uint protect, bool writeAccess) private static bool HasRequiredProtection(uint protect, bool writeAccess)
@@ -61,10 +61,34 @@ public static class KernelPthreadCompatExports
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X"); public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
} }
private sealed class PthreadMutexWaiter private sealed class PthreadMutexWaiter : IGuestThreadBlockWaiter
{ {
public required ulong ThreadId { get; init; } public required ulong ThreadId { get; init; }
public required CpuContext Ctx { get; init; }
public required ulong MutexAddress { get; init; }
public required ulong ResolvedAddress { get; init; }
public required PthreadMutexState State { get; init; }
public int Reserved; public int Reserved;
public int Resume() => CompleteBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
public bool TryWake() => TryReserveBlockedMutexLock(Ctx, MutexAddress, ResolvedAddress, State, this);
}
private sealed class PthreadCondWaiter : IGuestThreadBlockWaiter
{
public required CpuContext Ctx { get; init; }
public required ulong CondAddress { get; init; }
public required ulong MutexAddress { get; init; }
public required PthreadCondState State { get; init; }
public required ulong ObservedEpoch { get; init; }
public required bool Timed { get; init; }
public required int ReleasedRecursion { get; init; }
public required bool PosixResult { get; init; }
public int Resume() => ResumePthreadCondWait(Ctx, CondAddress, MutexAddress, State, ObservedEpoch, Timed, ReleasedRecursion, PosixResult);
public bool TryWake() => State.SignalEpoch != ObservedEpoch;
} }
private sealed class PthreadCondState private sealed class PthreadCondState
@@ -629,6 +653,7 @@ public static class KernelPthreadCompatExports
} }
if (!InitializeMutexObject(ctx, handle, state)) if (!InitializeMutexObject(ctx, handle, state))
{ {
TryFreeOpaqueObject(ctx, handle);
state.Semaphore.Dispose(); state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -641,6 +666,7 @@ public static class KernelPthreadCompatExports
_mutexStates.TryRemove(mutexAddress, out _); _mutexStates.TryRemove(mutexAddress, out _);
_mutexStates.TryRemove(handle, out _); _mutexStates.TryRemove(handle, out _);
TryFreeOpaqueObject(ctx, handle);
state.Semaphore.Dispose(); state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -668,6 +694,7 @@ public static class KernelPthreadCompatExports
} }
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0); _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
TryFreeOpaqueObject(ctx, resolvedAddress);
state.Semaphore.Dispose(); state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -724,7 +751,6 @@ public static class KernelPthreadCompatExports
if (!acquired) if (!acquired)
{ {
TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId); TraceContendedMutex(ctx, mutexAddress, resolvedAddress, state, currentThreadId);
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
// Fibers retain the synchronous fallback to preserve switch state. // Fibers retain the synchronous fallback to preserve switch state.
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx); var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0; var canCooperativelyBlock = _enableMutexLockBlocking || currentFiber == 0;
@@ -736,8 +762,14 @@ public static class KernelPthreadCompatExports
ctx, ctx,
"pthread_mutex_lock", "pthread_mutex_lock",
state.WakeKey, state.WakeKey,
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter), new PthreadMutexWaiter
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter))) {
ThreadId = currentThreadId,
Ctx = ctx,
MutexAddress = mutexAddress,
ResolvedAddress = resolvedAddress,
State = state,
}))
{ {
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK); TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -895,6 +927,7 @@ public static class KernelPthreadCompatExports
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0); var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
if (!WriteMutexAttrObject(ctx, handle, initialState)) if (!WriteMutexAttrObject(ctx, handle, initialState))
{ {
TryFreeOpaqueObject(ctx, handle);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -912,6 +945,7 @@ public static class KernelPthreadCompatExports
_mutexAttrStates.Remove(handle); _mutexAttrStates.Remove(handle);
} }
TryFreeOpaqueObject(ctx, handle);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -935,6 +969,7 @@ public static class KernelPthreadCompatExports
} }
} }
TryFreeOpaqueObject(ctx, resolvedAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -1195,6 +1230,7 @@ public static class KernelPthreadCompatExports
{ {
if (_condStates.TryGetValue(condAddress, out var raced)) if (_condStates.TryGetValue(condAddress, out var raced))
{ {
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = condAddress; resolvedAddress = condAddress;
state = raced; state = raced;
return true; return true;
@@ -1212,6 +1248,7 @@ public static class KernelPthreadCompatExports
_condStates.Remove(handle); _condStates.Remove(handle);
} }
TryFreeOpaqueObject(ctx, handle);
return false; return false;
} }
@@ -1231,7 +1268,22 @@ public static class KernelPthreadCompatExports
Span<byte> initialData = stackalloc byte[size]; Span<byte> initialData = stackalloc byte[size];
initialData.Clear(); initialData.Clear();
return ctx.Memory.TryWrite(address, initialData); if (ctx.Memory.TryWrite(address, initialData))
{
return true;
}
allocator.TryFreeGuestMemory(address);
address = 0;
return false;
}
private static void TryFreeOpaqueObject(CpuContext ctx, ulong address)
{
if (ctx.Memory is IGuestMemoryAllocator allocator)
{
allocator.TryFreeGuestMemory(address);
}
} }
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) => private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
@@ -1269,6 +1321,7 @@ public static class KernelPthreadCompatExports
_condStates.Remove(handle); _condStates.Remove(handle);
} }
TryFreeOpaqueObject(ctx, handle);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -1293,6 +1346,7 @@ public static class KernelPthreadCompatExports
} }
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0); _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
TryFreeOpaqueObject(ctx, resolvedAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -1363,8 +1417,17 @@ public static class KernelPthreadCompatExports
ctx, ctx,
timed ? "pthread_cond_timedwait" : "pthread_cond_wait", timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
state.WakeKey, state.WakeKey,
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion, posixResult), new PthreadCondWaiter
() => state.SignalEpoch != observedEpoch, {
Ctx = ctx,
CondAddress = condAddress,
MutexAddress = mutexAddress,
State = state,
ObservedEpoch = observedEpoch,
Timed = timed,
ReleasedRecursion = releasedRecursion,
PosixResult = posixResult,
},
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0)) timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
{ {
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult); TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
@@ -1775,6 +1838,7 @@ public static class KernelPthreadCompatExports
} }
if (!InitializeMutexObject(ctx, handle, createdState)) if (!InitializeMutexObject(ctx, handle, createdState))
{ {
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = 0; resolvedAddress = 0;
state = null; state = null;
return false; return false;
@@ -1784,12 +1848,14 @@ public static class KernelPthreadCompatExports
{ {
if (_mutexStates.TryGetValue(mutexAddress, out state)) if (_mutexStates.TryGetValue(mutexAddress, out state))
{ {
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = mutexAddress; resolvedAddress = mutexAddress;
return true; return true;
} }
if (_mutexStates.TryGetValue(handle, out state)) if (_mutexStates.TryGetValue(handle, out state))
{ {
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = handle; resolvedAddress = handle;
return true; return true;
} }
@@ -1803,6 +1869,7 @@ public static class KernelPthreadCompatExports
_mutexStates.TryRemove(mutexAddress, out _); _mutexStates.TryRemove(mutexAddress, out _);
_mutexStates.TryRemove(handle, out _); _mutexStates.TryRemove(handle, out _);
TryFreeOpaqueObject(ctx, handle);
resolvedAddress = 0; resolvedAddress = 0;
state = null; state = null;
return false; return false;
@@ -164,6 +164,17 @@ public static class KernelPthreadExtendedCompatExports
} }
} }
private sealed class RwlockWaiter : IGuestThreadBlockWaiter
{
public required PthreadRwlockState Rwlock { get; init; }
public required ulong ThreadId { get; init; }
public required bool Write { get; init; }
public int Resume() => (int)OrbisGen2Result.ORBIS_GEN2_OK;
public bool TryWake() => TryAcquireBlockedRwlock(Rwlock, ThreadId, Write);
}
private readonly record struct TlsKeyState(ulong Destructor); private readonly record struct TlsKeyState(ulong Destructor);
private readonly record struct PthreadAttrState( private readonly record struct PthreadAttrState(
@@ -382,6 +393,47 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "nsYoNRywwNg", Nid = "nsYoNRywwNg",
ExportName = "scePthreadAttrInit", ExportName = "scePthreadAttrInit",
@@ -1318,8 +1370,7 @@ public static class KernelPthreadExtendedCompatExports
ctx, ctx,
"pthread_rwlock_wrlock", "pthread_rwlock_wrlock",
rwlock.WakeKey, rwlock.WakeKey,
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK, new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = true }))
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
{ {
transferredToScheduler = true; transferredToScheduler = true;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -1355,8 +1406,7 @@ public static class KernelPthreadExtendedCompatExports
ctx, ctx,
"pthread_rwlock_rdlock", "pthread_rwlock_rdlock",
rwlock.WakeKey, rwlock.WakeKey,
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK, new RwlockWaiter { Rwlock = rwlock, ThreadId = currentThreadId, Write = false }))
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Fiber; using SharpEmu.Libs.Fiber;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Diagnostics; using System.Diagnostics;
@@ -48,9 +49,6 @@ public static class KernelRuntimeCompatExports
private const int MapFlagFixed = 0x10; private const int MapFlagFixed = 0x10;
private const ulong DefaultVirtualRangeAlignment = 0x4000UL; private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
private const int AioInitParamSize = 0x3C; 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 object _stateGate = new();
private static readonly long _processStartCounter = Stopwatch.GetTimestamp(); private static readonly long _processStartCounter = Stopwatch.GetTimestamp();
private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader(); private static readonly RdtscDelegate? _rdtscReader = CreateRdtscReader();
@@ -1893,7 +1891,7 @@ public static class KernelRuntimeCompatExports
try 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) if (stubAddress == 0)
{ {
return null; return null;
@@ -1923,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( private static bool TryReserveVirtualRange(
CpuContext ctx, CpuContext ctx,
ulong desiredAddress, ulong desiredAddress,
@@ -19,6 +19,8 @@ public static class KernelSemaphoreCompatExports
private sealed class KernelSemaphoreState private sealed class KernelSemaphoreState
{ {
public required string Name { get; init; } public required string Name { get; init; }
// Formatted once at creation; signal/wait/cancel/delete all wake through this key.
public required string WakeKey { get; init; }
public required int InitialCount { get; init; } public required int InitialCount { get; init; }
public required int MaxCount { get; init; } public required int MaxCount { get; init; }
public int Count { get; set; } public int Count { get; set; }
@@ -28,14 +30,26 @@ public static class KernelSemaphoreCompatExports
public object Gate { get; } = new(); public object Gate { get; } = new();
} }
private sealed class SemaphoreWaiter private sealed class SemaphoreWaiter : IGuestThreadBlockWaiter
{ {
public required KernelSemaphoreState Semaphore { get; init; }
public required int NeedCount { get; init; } public required int NeedCount { get; init; }
public required int CancelEpochAtBlock { get; init; } public required int CancelEpochAtBlock { get; init; }
public bool Timed { get; init; } public bool Timed { get; init; }
// Timed-wait completion state; unused when Timed is false.
public CpuContext? Ctx { get; init; }
public ulong TimeoutAddress { get; init; }
public long DeadlineTimestamp { get; init; }
// Written and read only under the owning semaphore's Gate. // Written and read only under the owning semaphore's Gate.
public int? Result { get; set; } public int? Result { get; set; }
public int Resume() => Timed
? CompleteBlockedTimedSemaWait(Ctx!, Semaphore, this, TimeoutAddress, DeadlineTimestamp)
: CompleteBlockedSemaWait(Semaphore, this);
public bool TryWake() => TryConsumeBlockedSemaWait(Semaphore, this);
} }
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}"; private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
@@ -79,6 +93,7 @@ public static class KernelSemaphoreCompatExports
var state = new KernelSemaphoreState var state = new KernelSemaphoreState
{ {
Name = name, Name = name,
WakeKey = GetSemaphoreWakeKey(handle),
InitialCount = initialCount, InitialCount = initialCount,
MaxCount = maxCount, MaxCount = maxCount,
Count = initialCount, Count = initialCount,
@@ -96,11 +111,14 @@ public static class KernelSemaphoreCompatExports
state.Deleted = true; state.Deleted = true;
} }
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle)); _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}"); if (_traceSema)
{
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -131,7 +149,10 @@ public static class KernelSemaphoreCompatExports
if (semaphore.Count >= needCount) if (semaphore.Count >= needCount)
{ {
semaphore.Count -= needCount; semaphore.Count -= needCount;
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); if (_traceSema)
{
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -145,7 +166,10 @@ public static class KernelSemaphoreCompatExports
if (timeoutMicros == 0) if (timeoutMicros == 0)
{ {
_ = ctx.TryWriteUInt32(timeoutAddress, 0); _ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
pollTimedOut = true; pollTimedOut = true;
} }
else else
@@ -153,27 +177,36 @@ public static class KernelSemaphoreCompatExports
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L)); var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromTicks((long)timeoutMicros * 10L));
var timedWaiter = new SemaphoreWaiter var timedWaiter = new SemaphoreWaiter
{ {
Semaphore = semaphore,
NeedCount = needCount, NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch, CancelEpochAtBlock = semaphore.CancelEpoch,
Timed = true, Timed = true,
Ctx = ctx,
TimeoutAddress = timeoutAddress,
DeadlineTimestamp = deadline,
}; };
if (GuestThreadExecution.RequestCurrentThreadBlock( if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx, ctx,
"sceKernelWaitSema", "sceKernelWaitSema",
GetSemaphoreWakeKey(handle), semaphore.WakeKey,
resumeHandler: () => CompleteBlockedTimedSemaWait(ctx, semaphore, timedWaiter, timeoutAddress, deadline), timedWaiter,
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, timedWaiter),
blockDeadlineTimestamp: deadline)) blockDeadlineTimestamp: deadline))
{ {
semaphore.WaitingThreads++; semaphore.WaitingThreads++;
TraceSemaphore($"wait-block-timed handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout_us={timeoutMicros} waiters={semaphore.WaitingThreads}"); if (_traceSema)
{
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); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
// Host-owned threads cannot park in the guest scheduler; degrade to the // Host-owned threads cannot park in the guest scheduler; degrade to the
// immediate-timeout poll the callers already tolerate. // immediate-timeout poll the callers already tolerate.
_ = ctx.TryWriteUInt32(timeoutAddress, 0); _ = ctx.TryWriteUInt32(timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); if (_traceSema)
{
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
pollTimedOut = true; pollTimedOut = true;
} }
} }
@@ -182,22 +215,28 @@ public static class KernelSemaphoreCompatExports
{ {
var waiter = new SemaphoreWaiter var waiter = new SemaphoreWaiter
{ {
Semaphore = semaphore,
NeedCount = needCount, NeedCount = needCount,
CancelEpochAtBlock = semaphore.CancelEpoch, CancelEpochAtBlock = semaphore.CancelEpoch,
}; };
if (!GuestThreadExecution.RequestCurrentThreadBlock( if (!GuestThreadExecution.RequestCurrentThreadBlock(
ctx, ctx,
"sceKernelWaitSema", "sceKernelWaitSema",
GetSemaphoreWakeKey(handle), semaphore.WakeKey,
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter), waiter))
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
{ {
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); if (_traceSema)
{
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); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
} }
semaphore.WaitingThreads++; semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); if (_traceSema)
{
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); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -239,12 +278,18 @@ public static class KernelSemaphoreCompatExports
{ {
if (semaphore.Count < needCount) if (semaphore.Count < needCount)
{ {
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); if (_traceSema)
{
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
} }
semaphore.Count -= needCount; semaphore.Count -= needCount;
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}"); if (_traceSema)
{
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
} }
@@ -277,14 +322,17 @@ public static class KernelSemaphoreCompatExports
} }
semaphore.Count += signalCount; semaphore.Count += signalCount;
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); if (_traceSema)
{
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
} }
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate). // Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
// Wake everyone; the wake handler consumes the count per waiter, so a waiter // Wake everyone; the wake handler consumes the count per waiter, so a waiter
// whose needCount exceeds the remaining count stays parked while a smaller // whose needCount exceeds the remaining count stays parked while a smaller
// waiter can proceed. // waiter can proceed.
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle)); _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -322,10 +370,13 @@ public static class KernelSemaphoreCompatExports
// exactly once in its wake handler. Zeroing here as well would double-count // exactly once in its wake handler. Zeroing here as well would double-count
// and silently absorb the increment of a waiter that parks between this // and silently absorb the increment of a waiter that parks between this
// gate release and the wake-all below. // gate release and the wake-all below.
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}"); if (_traceSema)
{
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
}
} }
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle)); _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -349,8 +400,11 @@ public static class KernelSemaphoreCompatExports
semaphore.Deleted = true; semaphore.Deleted = true;
} }
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle)); _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(semaphore.WakeKey);
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'"); if (_traceSema)
{
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -376,7 +430,10 @@ public static class KernelSemaphoreCompatExports
{ {
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED; waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}"); if (_traceSema)
{
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
}
return true; return true;
} }
@@ -384,7 +441,10 @@ public static class KernelSemaphoreCompatExports
{ {
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED; waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}"); if (_traceSema)
{
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
}
return true; return true;
} }
@@ -393,7 +453,10 @@ public static class KernelSemaphoreCompatExports
semaphore.Count -= waiter.NeedCount; semaphore.Count -= waiter.NeedCount;
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK; waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); if (_traceSema)
{
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
return true; return true;
} }
@@ -434,7 +497,10 @@ public static class KernelSemaphoreCompatExports
{ {
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT; waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1); semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}"); if (_traceSema)
{
TraceSemaphore($"wake-timeout name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
}
} }
result = waiter.Result!.Value; result = waiter.Result!.Value;
@@ -456,11 +522,13 @@ public static class KernelSemaphoreCompatExports
return result; return result;
} }
// Call sites must check this before building the interpolated message; the trace
// strings would otherwise be allocated on every semaphore op even with tracing off.
private static readonly bool _traceSema =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal);
private static void TraceSemaphore(string message) private static void TraceSemaphore(string message)
{ {
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal)) Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
{
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
}
} }
} }
@@ -3,15 +3,12 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System; using System;
using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace SharpEmu.Libs.Kernel; namespace SharpEmu.Libs.Kernel;
internal static class KernelVirtualRangeAllocator internal static class KernelVirtualRangeAllocator
{ {
private static readonly ConcurrentDictionary<Type, Accessor> _accessors = new();
public static bool TryReserve( public static bool TryReserve(
CpuContext ctx, CpuContext ctx,
ulong desiredAddress, ulong desiredAddress,
@@ -31,38 +28,24 @@ internal static class KernelVirtualRangeAllocator
try 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}"); Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
return false; 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 }; mappedAddress = searchedAddress;
var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs); return true;
if (searchResult is bool trueValue && trueValue &&
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
{
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}"); Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {typeof(ulong).FullName} value=0");
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"}");
return false; 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; var target = rootMemory;
accessor = default;
for (var depth = 0; depth < 4; depth++) for (var depth = 0; depth < 4; depth++)
{ {
accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor); if (target is IGuestAddressSpace resolved)
if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null)
{ {
addressSpace = resolved;
return true; return true;
} }
if (accessor.InnerProperty is null) if (target is not ICpuMemoryWrapper wrapper)
{ {
break; break;
} }
var innerValue = accessor.InnerProperty.GetValue(target); var inner = wrapper.Inner;
if (innerValue is null || ReferenceEquals(innerValue, target)) if (inner is null || ReferenceEquals(inner, target))
{ {
break; break;
} }
target = innerValue; target = inner;
} }
addressSpace = null;
return false; 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);
} }
+11
View File
@@ -7,6 +7,17 @@ namespace SharpEmu.Libs.Mouse;
public static class MouseExports 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 // Returns 0 read entries: no mouse is connected. This NID was previously misbound
// as an sceNgs2VoiceGetState alias. // as an sceNgs2VoiceGetState alias.
[SysAbiExport( [SysAbiExport(
@@ -146,6 +146,13 @@ public static class NetCtlExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long)); 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( [SysAbiExport(
Nid = "obuxdTiwkF8", Nid = "obuxdTiwkF8",
ExportName = "sceNetCtlGetInfo", ExportName = "sceNetCtlGetInfo",
@@ -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}");
}
}
@@ -0,0 +1,42 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Pad;
// Minimal libSceBluetoothHid stub: no host Bluetooth passthrough, so report
// success and let the Pad path provide input (SHARPEMU_BTHID_UNAVAILABLE=1 fails instead).
public static class BluetoothHidExports
{
private const int BluetoothHidUnavailable = unchecked((int)0x80960001);
private static readonly bool _reportUnavailable = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_BTHID_UNAVAILABLE"),
"1",
StringComparison.Ordinal);
private static int Result(CpuContext ctx) =>
ctx.SetReturn(_reportUnavailable ? BluetoothHidUnavailable : 0);
[SysAbiExport(
Nid = "tul3-GzejQc",
ExportName = "sceBluetoothHidInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceBluetoothHid")]
public static int BluetoothHidInit(CpuContext ctx) => Result(ctx);
[SysAbiExport(
Nid = "4FUZ+c52d2k",
ExportName = "sceBluetoothHidRegisterDevice",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceBluetoothHid")]
public static int BluetoothHidRegisterDevice(CpuContext ctx) => Result(ctx);
[SysAbiExport(
Nid = "4Ypfo9RIwfM",
ExportName = "sceBluetoothHidRegisterCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceBluetoothHid")]
public static int BluetoothHidRegisterCallback(CpuContext ctx) => Result(ctx);
}
+129 -75
View File
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Diagnostics; using System.Diagnostics;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad; namespace SharpEmu.Libs.Pad;
@@ -38,8 +38,7 @@ public static class PadExports
public static int PadInit(CpuContext ctx) public static int PadInit(CpuContext ctx)
{ {
_initialized = true; _initialized = true;
DualSenseReader.EnsureStarted(); HostPlatform.Current.Input.EnsureStarted();
XInputReader.EnsureStarted();
return ctx.SetReturn(0); return ctx.SetReturn(0);
} }
@@ -48,7 +47,18 @@ public static class PadExports
ExportName = "scePadOpen", ExportName = "scePadOpen",
Target = Generation.Gen4 | Generation.Gen5, Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")] LibraryName = "libScePad")]
public static int PadOpen(CpuContext ctx) public static int PadOpen(CpuContext ctx) => PadOpenCore(ctx, extended: false);
[SysAbiExport(
Nid = "WFIiSfXGUq8",
ExportName = "scePadOpenExt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
private static int PadOpenCore(CpuContext ctx, bool extended)
{ {
var userId = unchecked((int)ctx[CpuRegister.Rdi]); var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]); var type = unchecked((int)ctx[CpuRegister.Rsi]);
@@ -64,25 +74,37 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle); return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
} }
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0) var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
{ {
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected); return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
} }
DualSenseReader.EnsureStarted(); var input = HostPlatform.Current.Input;
XInputReader.EnsureStarted(); input.EnsureStarted();
if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0) if (Interlocked.Exchange(ref _controlsAnnouncementLogged, 1) == 0)
{ {
Console.Error.WriteLine(DualSenseReader.TryGetState(out _) Console.Error.WriteLine(input.DescribeConnectedGamepad() is { } gamepadName
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)." ? $"[LOADER][INFO] Controls: {gamepadName} connected (keyboard fallback also active)."
: XInputReader.TryGetState(out _) : "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
} }
return ctx.SetReturn(PrimaryPadHandle); return ctx.SetReturn(PrimaryPadHandle);
} }
[SysAbiExport(
Nid = "6ncge5+l5Qs",
ExportName = "scePadClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return handle == PrimaryPadHandle
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
[SysAbiExport( [SysAbiExport(
Nid = "clVvL4ZDntw", Nid = "clVvL4ZDntw",
ExportName = "scePadSetMotionSensorState", ExportName = "scePadSetMotionSensorState",
@@ -131,6 +153,47 @@ public static class PadExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport(
Nid = "hGbf2QTBmqc",
ExportName = "scePadGetExtControllerInformation",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadGetExtControllerInformation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (informationAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// Base ScePadControllerInformation + device-class/connection fields: report a connected
// DualSense so the guest's open -> get-ext-info -> close probe loop resolves.
Span<byte> information = stackalloc byte[0x40];
information.Clear();
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
information[0x08] = 30;
information[0x09] = 30;
information[0x0A] = StandardPortType;
information[0x0B] = 1; // connected count
information[0x0C] = 1; // connected
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
information[0x1C] = 0; // deviceClass: 0 = standard controller / DualSense
information[0x1D] = 1; // connected (ext)
information[0x1E] = 0; // connectionType: local
return ctx.Memory.TryWrite(informationAddress, information)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport( [SysAbiExport(
Nid = "YndgXqQVV7c", Nid = "YndgXqQVV7c",
ExportName = "scePadReadState", ExportName = "scePadReadState",
@@ -216,7 +279,7 @@ public static class PadExports
} }
var triggerMask = parameter[0]; var triggerMask = parameter[0];
XInputReader.SetTriggerRumble( HostPlatform.Current.Input.SetTriggerRumble(
(triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null, (triggerMask & 0x01) != 0 ? DecodeTriggerVibration(parameter[8..64]) : null,
(triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null); (triggerMask & 0x02) != 0 ? DecodeTriggerVibration(parameter[64..120]) : null);
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
@@ -260,8 +323,7 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
DualSenseReader.SetRumble(parameter[0], parameter[1]); HostPlatform.Current.Input.SetRumble(parameter[0], parameter[1]);
XInputReader.SetRumble(parameter[0], parameter[1]);
return ctx.SetReturn(0); return ctx.SetReturn(0);
} }
@@ -291,7 +353,7 @@ public static class PadExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
DualSenseReader.SetLightbar(color[0], color[1], color[2]); HostPlatform.Current.Input.SetLightbar(color[0], color[1], color[2]);
return ctx.SetReturn(0); return ctx.SetReturn(0);
} }
@@ -308,7 +370,7 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorInvalidHandle); return ctx.SetReturn(OrbisPadErrorInvalidHandle);
} }
DualSenseReader.ResetLightbar(); HostPlatform.Current.Input.ResetLightbar();
return ctx.SetReturn(0); return ctx.SetReturn(0);
} }
@@ -354,37 +416,30 @@ public static class PadExports
return _cachedInputState; return _cachedInputState;
} }
var acceptsKeyboardInput = IsEmulatorWindowFocused(); var input = HostPlatform.Current.Input;
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0; var acceptsKeyboardInput = input.IsHostWindowFocused();
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128; var buttons = acceptsKeyboardInput ? ReadKeyboardButtons(input) : 0;
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128; var leftX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x41), input.IsKeyDown(0x44)) : (byte)128;
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128; var leftY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x57), input.IsKeyDown(0x53)) : (byte)128;
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128; var rightX = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x4A), input.IsKeyDown(0x4C)) : (byte)128;
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0; var rightY = acceptsKeyboardInput ? ReadAnalogStick(input.IsKeyDown(0x49), input.IsKeyDown(0x4B)) : (byte)128;
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0; var l2 = acceptsKeyboardInput && input.IsKeyDown(0x52) ? (byte)255 : (byte)0;
var r2 = acceptsKeyboardInput && input.IsKeyDown(0x46) ? (byte)255 : (byte)0;
if (DualSenseReader.TryGetState(out var pad)) Span<HostGamepadState> gamepads = stackalloc HostGamepadState[2];
var gamepadCount = input.GetGamepadStates(gamepads);
for (var index = 0; index < gamepadCount; index++)
{ {
buttons |= pad.Buttons; var pad = gamepads[index];
buttons |= ToOrbisButtons(pad.Buttons);
// The controller stick wins whenever it is deflected past a // The controller stick wins whenever it is deflected past a
// small deadzone; otherwise any keyboard value stays. // small deadzone; otherwise any keyboard value stays.
leftX = MergeAxis(pad.LeftX, leftX); leftX = MergeAxis(pad.LeftX, leftX);
leftY = MergeAxis(pad.LeftY, leftY); leftY = MergeAxis(pad.LeftY, leftY);
rightX = MergeAxis(pad.RightX, rightX); rightX = MergeAxis(pad.RightX, rightX);
rightY = MergeAxis(pad.RightY, rightY); rightY = MergeAxis(pad.RightY, rightY);
l2 = Math.Max(l2, pad.L2); l2 = Math.Max(l2, pad.LeftTrigger);
r2 = Math.Max(r2, pad.R2); r2 = Math.Max(r2, pad.RightTrigger);
}
if (XInputReader.TryGetState(out var xpad))
{
buttons |= xpad.Buttons;
leftX = MergeAxis(xpad.LeftX, leftX);
leftY = MergeAxis(xpad.LeftY, leftY);
rightX = MergeAxis(xpad.RightX, rightX);
rightY = MergeAxis(xpad.RightY, rightY);
l2 = Math.Max(l2, xpad.L2);
r2 = Math.Max(r2, xpad.R2);
} }
_cachedInputState = new PadState( _cachedInputState = new PadState(
@@ -400,50 +455,49 @@ public static class PadExports
return _cachedInputState; return _cachedInputState;
} }
[DllImport("user32.dll")] /// <summary>Maps the host seam's neutral button flags onto SCE_PAD_BUTTON bits.</summary>
private static extern short GetAsyncKeyState(int vKey); private static uint ToOrbisButtons(HostGamepadButtons buttons)
[DllImport("user32.dll")]
private static extern nint GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
private static bool IsKeyDown(int vk) =>
(GetAsyncKeyState(vk) & 0x8000) != 0;
private static bool IsEmulatorWindowFocused()
{ {
var foregroundWindow = GetForegroundWindow(); uint result = 0;
if (foregroundWindow == 0) if ((buttons & HostGamepadButtons.Up) != 0) result |= OrbisPadButton.Up;
{ if ((buttons & HostGamepadButtons.Down) != 0) result |= OrbisPadButton.Down;
return false; if ((buttons & HostGamepadButtons.Left) != 0) result |= OrbisPadButton.Left;
} if ((buttons & HostGamepadButtons.Right) != 0) result |= OrbisPadButton.Right;
if ((buttons & HostGamepadButtons.Cross) != 0) result |= OrbisPadButton.Cross;
GetWindowThreadProcessId(foregroundWindow, out var processId); if ((buttons & HostGamepadButtons.Circle) != 0) result |= OrbisPadButton.Circle;
return processId == (uint)Environment.ProcessId; if ((buttons & HostGamepadButtons.Square) != 0) result |= OrbisPadButton.Square;
if ((buttons & HostGamepadButtons.Triangle) != 0) result |= OrbisPadButton.Triangle;
if ((buttons & HostGamepadButtons.L1) != 0) result |= OrbisPadButton.L1;
if ((buttons & HostGamepadButtons.R1) != 0) result |= OrbisPadButton.R1;
if ((buttons & HostGamepadButtons.L2) != 0) result |= OrbisPadButton.L2;
if ((buttons & HostGamepadButtons.R2) != 0) result |= OrbisPadButton.R2;
if ((buttons & HostGamepadButtons.L3) != 0) result |= OrbisPadButton.L3;
if ((buttons & HostGamepadButtons.R3) != 0) result |= OrbisPadButton.R3;
if ((buttons & HostGamepadButtons.Options) != 0) result |= OrbisPadButton.Options;
if ((buttons & HostGamepadButtons.TouchPad) != 0) result |= OrbisPadButton.TouchPad;
return result;
} }
private static uint ReadKeyboardButtons() private static uint ReadKeyboardButtons(IHostInput input)
{ {
uint buttons = 0; uint buttons = 0;
// D-pad // D-pad
if (IsKeyDown(0x25)) buttons |= 0x0080; // Left if (input.IsKeyDown(0x25)) buttons |= OrbisPadButton.Left;
if (IsKeyDown(0x27)) buttons |= 0x0020; // Right if (input.IsKeyDown(0x27)) buttons |= OrbisPadButton.Right;
if (IsKeyDown(0x26)) buttons |= 0x0010; // Up if (input.IsKeyDown(0x26)) buttons |= OrbisPadButton.Up;
if (IsKeyDown(0x28)) buttons |= 0x0040; // Down if (input.IsKeyDown(0x28)) buttons |= OrbisPadButton.Down;
// Face buttons // Face buttons
if (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross if (input.IsKeyDown(0x5A) || input.IsKeyDown(0x0D)) buttons |= OrbisPadButton.Cross; // Z / Enter
if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle if (input.IsKeyDown(0x58) || input.IsKeyDown(0x1B)) buttons |= OrbisPadButton.Circle; // X / Escape
if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square if (input.IsKeyDown(0x43)) buttons |= OrbisPadButton.Square; // C
if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle if (input.IsKeyDown(0x56)) buttons |= OrbisPadButton.Triangle; // V
// Shoulder buttons // Shoulder buttons
if (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1 if (input.IsKeyDown(0x51)) buttons |= OrbisPadButton.L1; // Q
if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1 if (input.IsKeyDown(0x45)) buttons |= OrbisPadButton.R1; // E
if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital) if (input.IsKeyDown(0x52)) buttons |= OrbisPadButton.L2; // R (digital)
if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital) if (input.IsKeyDown(0x46)) buttons |= OrbisPadButton.R2; // F (digital)
// Options (Start) // Options (Start)
if (IsKeyDown(0x09) || IsKeyDown(0x08)) buttons |= 0x0008; // Tab / Backspace = Options if (input.IsKeyDown(0x09) || input.IsKeyDown(0x08)) buttons |= OrbisPadButton.Options; // Tab / Backspace
return buttons; return buttons;
} }
+4
View File
@@ -8,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" /> <ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Silk.NET.Vulkan" /> <PackageReference Include="Silk.NET.Vulkan" />
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" /> <PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
@@ -185,6 +185,31 @@ public static class UserServiceExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport(
Nid = "woNpu+45RLk",
ExportName = "sceUserServiceGetAgeLevel",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetAgeLevel(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var ageLevelAddress = ctx[CpuRegister.Rsi];
if (userId != 1000)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
}
if (ageLevelAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
}
// Report an adult account so titles skip parental-restriction paths.
return ctx.TryWriteInt32(ageLevelAddress, 21)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static void TraceUserService(string message) private static void TraceUserService(string message)
{ {
if (_traceUserService) if (_traceUserService)
+137 -37
View File
@@ -2,9 +2,11 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.Audio; using SharpEmu.Libs.Audio;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using SharpEmu.Logging; using SharpEmu.Logging;
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@@ -67,7 +69,7 @@ public static class VideoOutExports
return; return;
} }
HostTimerResolution.Request(); HostPlatform.Current.Threading.RequestTimerResolution();
_vblankPumpThread = new Thread(VblankPumpLoop) _vblankPumpThread = new Thread(VblankPumpLoop)
{ {
@@ -180,6 +182,10 @@ public static class VideoOutExports
} }
} }
// Only ever touched by the vblank pump thread; reused across edges so the 60 Hz
// pump does not allocate a fresh snapshot per edge.
private static readonly List<VideoOutPortState> _vblankPumpPorts = new();
private static void PumpVblanks() private static void PumpVblanks()
{ {
lock (_vblankEdgeGate) lock (_vblankEdgeGate)
@@ -188,7 +194,7 @@ public static class VideoOutExports
Monitor.PulseAll(_vblankEdgeGate); Monitor.PulseAll(_vblankEdgeGate);
} }
VideoOutPortState[] ports; _vblankPumpPorts.Clear();
lock (_stateGate) lock (_stateGate)
{ {
if (_ports.Count == 0) if (_ports.Count == 0)
@@ -198,10 +204,16 @@ public static class VideoOutExports
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global // Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads. // flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray(); foreach (var port in _ports.Values)
{
if (port.VblankEvents.Count != 0)
{
_vblankPumpPorts.Add(port);
}
}
} }
foreach (var port in ports) foreach (var port in _vblankPumpPorts)
{ {
SignalVblank(port); SignalVblank(port);
} }
@@ -221,6 +233,12 @@ public static class VideoOutExports
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"), Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
"1", "1",
StringComparison.Ordinal); StringComparison.Ordinal);
// Call sites must check this before building the interpolated message; the trace
// strings would otherwise be allocated on the per-frame flip path even with tracing off.
private static readonly bool _logVideoOut = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"),
"1",
StringComparison.Ordinal);
private static readonly bool _dumpVideoOut = string.Equals( private static readonly bool _dumpVideoOut = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"), Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
"1", "1",
@@ -403,6 +421,34 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
[SysAbiExport(
Nid = "+I4K03i3EL0",
ExportName = "sceVideoOutInitializeOutputOptions",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutInitializeOutputOptions(CpuContext ctx)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "w0hLuNarQxY",
ExportName = "sceVideoOutConfigureOutput",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutConfigureOutput(CpuContext ctx)
{
// Accept the requested output configuration; the presenter always renders
// at the display buffer's native size.
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!TryGetPort(handle, out _))
{
return OrbisVideoOutErrorInvalidHandle;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport( [SysAbiExport(
Nid = "utPrVdxio-8", Nid = "utPrVdxio-8",
ExportName = "sceVideoOutGetOutputStatus", ExportName = "sceVideoOutGetOutputStatus",
@@ -558,7 +604,10 @@ public static class VideoOutExports
// Some engines wait on this queue before issuing their first flip. Provide a first // Some engines wait on this queue before issuing their first flip. Provide a first
// edge now; later calls to WaitVblank advance the same notification sequence. // edge now; later calls to WaitVblank advance the same notification sequence.
SignalVblank(port); SignalVblank(port);
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}"); if (_logVideoOut)
{
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -595,7 +644,10 @@ public static class VideoOutExports
} }
} }
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}"); if (_logVideoOut)
{
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -656,7 +708,10 @@ public static class VideoOutExports
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg)); KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer); KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}"); if (_logVideoOut)
{
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -1049,33 +1104,53 @@ public static class VideoOutExports
private static void SignalVblank(VideoOutPortState port) private static void SignalVblank(VideoOutPortState port)
{ {
List<FlipEventRegistration> vblankEvents; // Snapshot the registrations into a pooled rental so the triggers can run outside
// _stateGate without copying the list into a fresh allocation on every edge.
// A per-port reusable buffer would race: the pump thread and a guest thread's
// first-edge signal (AddVblankEvent) can signal the same port concurrently.
FlipEventRegistration[]? vblankEvents = null;
int vblankEventCount;
ulong eventHint; ulong eventHint;
lock (_stateGate) lock (_stateGate)
{ {
port.VblankCount++; port.VblankCount++;
eventHint = SceVideoOutInternalEventVblank | eventHint = SceVideoOutInternalEventVblank |
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16); ((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
vblankEvents = new List<FlipEventRegistration>(port.VblankEvents); vblankEventCount = port.VblankEvents.Count;
if (vblankEventCount != 0)
{
vblankEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(vblankEventCount);
port.VblankEvents.CopyTo(vblankEvents);
}
} }
var signalCount = Interlocked.Increment(ref _vblankSignalCount); var signalCount = Interlocked.Increment(ref _vblankSignalCount);
foreach (var vblankEvent in vblankEvents) if (vblankEvents is not null)
{ {
_ = KernelEventQueueCompatExports.TriggerDisplayEvent( try
vblankEvent.Equeue, {
SceVideoOutInternalEventVblank, for (var i = 0; i < vblankEventCount; i++)
OrbisKernelEventFilterVideoOut, {
eventHint, _ = KernelEventQueueCompatExports.TriggerDisplayEvent(
vblankEvent.UserData); vblankEvents[i].Equeue,
SceVideoOutInternalEventVblank,
OrbisKernelEventFilterVideoOut,
eventHint,
vblankEvents[i].UserData);
}
}
finally
{
ArrayPool<FlipEventRegistration>.Shared.Return(vblankEvents);
}
} }
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0)) if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " + $"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
$"queues={vblankEvents.Count} hint=0x{eventHint:X16}"); $"queues={vblankEventCount} hint=0x{eventHint:X16}");
} }
} }
@@ -1097,8 +1172,11 @@ public static class VideoOutExports
return OrbisVideoOutErrorInvalidIndex; return OrbisVideoOutErrorInvalidIndex;
} }
// Pooled snapshot for the same reason as SignalVblank: triggers run outside
// _stateGate, and SubmitFlip is per-frame so a fresh List copy is steady churn.
ulong eventHint; ulong eventHint;
List<FlipEventRegistration> flipEvents; FlipEventRegistration[]? flipEvents = null;
int flipEventCount;
lock (_stateGate) lock (_stateGate)
{ {
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0) if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
@@ -1110,7 +1188,12 @@ public static class VideoOutExports
port.FlipCount++; port.FlipCount++;
eventHint = SceVideoOutInternalEventFlip | eventHint = SceVideoOutInternalEventFlip |
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16); ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
flipEvents = new List<FlipEventRegistration>(port.FlipEvents); flipEventCount = port.FlipEvents.Count;
if (flipEventCount != 0)
{
flipEvents = ArrayPool<FlipEventRegistration>.Shared.Rent(flipEventCount);
port.FlipEvents.CopyTo(flipEvents);
}
} }
var guestImageSubmitted = false; var guestImageSubmitted = false;
@@ -1132,14 +1215,24 @@ public static class VideoOutExports
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg); _ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
} }
foreach (var flipEvent in flipEvents) if (flipEvents is not null)
{ {
_ = KernelEventQueueCompatExports.TriggerDisplayEvent( try
flipEvent.Equeue, {
SceVideoOutInternalEventFlip, for (var i = 0; i < flipEventCount; i++)
OrbisKernelEventFilterVideoOut, {
eventHint, _ = KernelEventQueueCompatExports.TriggerDisplayEvent(
flipEvent.UserData); flipEvents[i].Equeue,
SceVideoOutInternalEventFlip,
OrbisKernelEventFilterVideoOut,
eventHint,
flipEvents[i].UserData);
}
}
finally
{
ArrayPool<FlipEventRegistration>.Shared.Return(flipEvents);
}
} }
var flipCount = Interlocked.Increment(ref _flipSubmitCount); var flipCount = Interlocked.Increment(ref _flipSubmitCount);
@@ -1148,10 +1241,13 @@ public static class VideoOutExports
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " + $"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " + $"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
$"flipQueues={flipEvents.Count}"); $"flipQueues={flipEventCount}");
} }
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}"); if (_logVideoOut)
{
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEventCount}");
}
ReportFrameRate(presented: false); ReportFrameRate(presented: false);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -1233,8 +1329,11 @@ public static class VideoOutExports
slot.AddressRight = 0; slot.AddressRight = 0;
} }
TraceVideoOut( if (_logVideoOut)
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}"); {
TraceVideoOut(
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
}
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height); VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat); var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
@@ -1400,7 +1499,10 @@ public static class VideoOutExports
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex); var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb); WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint); WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}"); if (_logVideoOut)
{
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
}
return true; return true;
} }
@@ -1439,7 +1541,10 @@ public static class VideoOutExports
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex); var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
File.WriteAllBytes(basePath + ".raw", bytes); File.WriteAllBytes(basePath + ".raw", bytes);
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint); WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}"); if (_logVideoOut)
{
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
}
return true; return true;
} }
@@ -1666,11 +1771,6 @@ public static class VideoOutExports
private static void TraceVideoOut(string message) private static void TraceVideoOut(string message)
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] {message}"); Console.Error.WriteLine($"[LOADER][TRACE] {message}");
} }
} }
+478 -188
View File
@@ -104,14 +104,17 @@ internal readonly record struct VulkanGuestBlendState(
} }
internal sealed record VulkanGuestRenderState( internal sealed record VulkanGuestRenderState(
VulkanGuestBlendState Blend, IReadOnlyList<VulkanGuestBlendState> Blends,
VulkanGuestRect? Scissor, VulkanGuestRect? Scissor,
VulkanGuestViewport? Viewport) VulkanGuestViewport? Viewport)
{ {
public static VulkanGuestRenderState Default { get; } = new( public static VulkanGuestRenderState Default { get; } = new(
VulkanGuestBlendState.Default, [VulkanGuestBlendState.Default],
Scissor: null, Scissor: null,
Viewport: null); Viewport: null);
public VulkanGuestBlendState Blend =>
Blends.Count == 0 ? VulkanGuestBlendState.Default : Blends[0];
} }
internal sealed record VulkanGuestRenderTarget( internal sealed record VulkanGuestRenderTarget(
@@ -122,6 +125,13 @@ internal sealed record VulkanGuestRenderTarget(
uint NumberType, uint NumberType,
uint MipLevels = 1); uint MipLevels = 1);
internal readonly record struct VulkanRenderTargetFormat(
Format Format,
Gen5PixelOutputKind OutputKind)
{
public bool IsInteger => OutputKind is Gen5PixelOutputKind.Uint or Gen5PixelOutputKind.Sint;
}
internal sealed record VulkanTranslatedGuestDraw( internal sealed record VulkanTranslatedGuestDraw(
byte[] VertexSpirv, byte[] VertexSpirv,
byte[] PixelSpirv, byte[] PixelSpirv,
@@ -137,7 +147,7 @@ internal sealed record VulkanTranslatedGuestDraw(
internal sealed record VulkanOffscreenGuestDraw( internal sealed record VulkanOffscreenGuestDraw(
VulkanTranslatedGuestDraw Draw, VulkanTranslatedGuestDraw Draw,
VulkanGuestRenderTarget Target, IReadOnlyList<VulkanGuestRenderTarget> Targets,
bool PublishTarget); bool PublishTarget);
internal sealed record VulkanComputeGuestDispatch( internal sealed record VulkanComputeGuestDispatch(
@@ -463,20 +473,70 @@ internal static unsafe class VulkanVideoPresenter
VulkanGuestIndexBuffer? indexBuffer = null, VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null, IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null) VulkanGuestRenderState? renderState = null)
{
SubmitOffscreenTranslatedDraw(
pixelSpirv,
textures,
globalMemoryBuffers,
attributeCount,
[target],
vertexSpirv,
vertexCount,
instanceCount,
primitiveType,
indexBuffer,
vertexBuffers,
renderState);
}
public static void SubmitOffscreenTranslatedDraw(
byte[] pixelSpirv,
IReadOnlyList<VulkanGuestDrawTexture> textures,
IReadOnlyList<VulkanGuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
IReadOnlyList<VulkanGuestRenderTarget> targets,
byte[]? vertexSpirv = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
VulkanGuestIndexBuffer? indexBuffer = null,
IReadOnlyList<VulkanGuestVertexBuffer>? vertexBuffers = null,
VulkanGuestRenderState? renderState = null)
{ {
if (pixelSpirv.Length == 0 || if (pixelSpirv.Length == 0 ||
target.Address == 0 || targets.Count == 0 ||
target.Width == 0 || targets.Count > 8 ||
target.Height == 0) targets.Any(target =>
target.Address == 0 || target.Width == 0 || target.Height == 0))
{ {
return; return;
} }
var firstTarget = targets[0];
if (targets.Any(target =>
target.Width != firstTarget.Width || target.Height != firstTarget.Height) ||
targets.Select(target => target.Address).Distinct().Count() != targets.Count)
{
Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets.");
return;
}
if (ShouldTracePresentedGuestImageContentsForDiagnostics()) if (ShouldTracePresentedGuestImageContentsForDiagnostics())
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " + $"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
$"target=0x{target.Address:X16} {target.Width}x{target.Height} textures={textures.Count}"); $"targets={targets.Count} first=0x{firstTarget.Address:X16} " +
$"{firstTarget.Width}x{firstTarget.Height} textures={textures.Count}");
}
var effectiveRenderState = renderState ?? VulkanGuestRenderState.Default;
if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1)
{
effectiveRenderState = effectiveRenderState with
{
Blends = Enumerable.Repeat(effectiveRenderState.Blends[0], targets.Count).ToArray(),
};
} }
lock (_gate) lock (_gate)
@@ -486,12 +546,15 @@ internal static unsafe class VulkanVideoPresenter
return; return;
} }
var guestTextureFormat = GetGuestTextureFormat( foreach (var target in targets)
target.Format,
target.NumberType);
if (guestTextureFormat != 0)
{ {
_availableGuestImages[target.Address] = guestTextureFormat; var guestTextureFormat = GetGuestTextureFormat(
target.Format,
target.NumberType);
if (guestTextureFormat != 0)
{
_availableGuestImages[target.Address] = guestTextureFormat;
}
} }
EnqueueGuestWorkLocked( EnqueueGuestWorkLocked(
@@ -507,8 +570,8 @@ internal static unsafe class VulkanVideoPresenter
instanceCount, instanceCount,
primitiveType, primitiveType,
indexBuffer, indexBuffer,
renderState ?? VulkanGuestRenderState.Default), effectiveRenderState),
target, targets.ToArray(),
PublishTarget: true)); PublishTarget: true));
} }
} }
@@ -550,12 +613,12 @@ internal static unsafe class VulkanVideoPresenter
4, 4,
null, null,
VulkanGuestRenderState.Default), VulkanGuestRenderState.Default),
new VulkanGuestRenderTarget( [new VulkanGuestRenderTarget(
Address: 0, Address: 0,
width, width,
height, height,
Format: 12, Format: 12,
NumberType: 7), NumberType: 7)],
PublishTarget: false)); PublishTarget: false));
} }
} }
@@ -785,25 +848,100 @@ internal static unsafe class VulkanVideoPresenter
return true; return true;
} }
private static uint GetGuestTextureFormat(uint format, uint numberType) => private static uint GetGuestTextureFormat(uint format, uint numberType)
(format, numberType) switch {
if (!TryDecodeRenderTargetFormat(format, numberType, out var decoded))
{ {
(9, _) => 9, return 0;
(4, 4) => GuestFormatR32Uint, }
(4, 5) => GuestFormatR32Sint,
(4, 7) => GuestFormatR32Sfloat, return decoded.Format switch
(5, 4) => GuestFormatR16G16Uint, {
(5, 5) => GuestFormatR16G16Sint, Format.R8Unorm => 36,
(5, 7) => GuestFormatR16G16Sfloat, Format.R8Uint => 49,
(10, 4) => GuestFormatR8G8B8A8Uint, Format.R8G8Unorm => 3,
(10, 5) => GuestFormatR8G8B8A8Sint, Format.A2R10G10B10UnormPack32 => 9,
(10, _) => 56, Format.B10G11R11UfloatPack32 => 7,
(12, 4) => GuestFormatR16G16B16A16Uint, Format.R32Uint => GuestFormatR32Uint,
(12, 5) => GuestFormatR16G16B16A16Sint, Format.R32Sint => GuestFormatR32Sint,
(12, 7) => 71, Format.R32Sfloat => GuestFormatR32Sfloat,
(_, 0) when IsKnownGuestTextureFormat(format) => format, Format.R16G16Unorm => 5,
Format.R16G16Uint => GuestFormatR16G16Uint,
Format.R16G16Sint => GuestFormatR16G16Sint,
Format.R16G16Sfloat => GuestFormatR16G16Sfloat,
Format.R32G32Sfloat => 75,
Format.R8G8B8A8Unorm => 56,
Format.R8G8B8A8Uint => GuestFormatR8G8B8A8Uint,
Format.R8G8B8A8Sint => GuestFormatR8G8B8A8Sint,
Format.R16G16B16A16Unorm => 12,
Format.R16G16B16A16Uint => GuestFormatR16G16B16A16Uint,
Format.R16G16B16A16Sint => GuestFormatR16G16B16A16Sint,
Format.R16G16B16A16Sfloat => 71,
Format.R32G32B32A32Sfloat => 14,
_ => 0, _ => 0,
}; };
}
internal static bool TryDecodeRenderTargetFormat(
uint dataFormat,
uint numberType,
out VulkanRenderTargetFormat result)
{
var format = (dataFormat, numberType) switch
{
(4, 4) => Format.R32Uint,
(4, 5) => Format.R32Sint,
(4, 7) => Format.R32Sfloat,
(5, 4) => Format.R16G16Uint,
(5, 5) => Format.R16G16Sint,
(5, 7) => Format.R16G16Sfloat,
(9, _) => Format.A2R10G10B10UnormPack32,
(10, 4) => Format.R8G8B8A8Uint,
(10, 5) => Format.R8G8B8A8Sint,
(10, _) => Format.R8G8B8A8Unorm,
(12, 4) => Format.R16G16B16A16Uint,
(12, 5) => Format.R16G16B16A16Sint,
(12, 7) => Format.R16G16B16A16Sfloat,
(GuestFormatR32Uint, _) or (20, 0) => Format.R32Uint,
(GuestFormatR32Sint, _) => Format.R32Sint,
(GuestFormatR32Sfloat, _) or (29, 0) or (4, 0) => Format.R32Sfloat,
(GuestFormatR16G16Uint, _) => Format.R16G16Uint,
(GuestFormatR16G16Sint, _) => Format.R16G16Sint,
(GuestFormatR16G16Sfloat, _) => Format.R16G16Sfloat,
(GuestFormatR8G8B8A8Uint, _) => Format.R8G8B8A8Uint,
(GuestFormatR8G8B8A8Sint, _) => Format.R8G8B8A8Sint,
(GuestFormatR16G16B16A16Uint, _) => Format.R16G16B16A16Uint,
(GuestFormatR16G16B16A16Sint, _) => Format.R16G16B16A16Sint,
(1, 0) or (36, 0) => Format.R8Unorm,
(49, 0) => Format.R8Uint,
(3, 0) => Format.R8G8Unorm,
(5, 0) => Format.R16G16Unorm,
(7, 0) => Format.B10G11R11UfloatPack32,
(12, 0) => Format.R16G16B16A16Unorm,
(13, 0) or (14, 0) => Format.R32G32B32A32Sfloat,
(22, 0) or (71, 0) => Format.R16G16B16A16Sfloat,
(56, 0) or (62, 0) or (64, 0) => Format.R8G8B8A8Unorm,
(75, 0) => Format.R32G32Sfloat,
_ => Format.Undefined,
};
if (format == Format.Undefined)
{
result = default;
return false;
}
var outputKind = format switch
{
Format.R8Uint or Format.R32Uint or Format.R16G16Uint or
Format.R8G8B8A8Uint or Format.R16G16B16A16Uint => Gen5PixelOutputKind.Uint,
Format.R32Sint or Format.R16G16Sint or Format.R8G8B8A8Sint or
Format.R16G16B16A16Sint => Gen5PixelOutputKind.Sint,
_ => Gen5PixelOutputKind.Float,
};
result = new VulkanRenderTargetFormat(format, outputKind);
return true;
}
private static bool IsKnownGuestTextureFormat(uint format) => private static bool IsKnownGuestTextureFormat(uint format) =>
format is 4 or 5 or 7 or 9 or 13 or 14 or 22 or 29 or 36 or 56 or 62 or 64 or 71; format is 4 or 5 or 7 or 9 or 13 or 14 or 22 or 29 or 36 or 56 or 62 or 64 or 71;
@@ -948,6 +1086,8 @@ internal static unsafe class VulkanVideoPresenter
private DebugUtilsMessengerEXT _debugMessenger; private DebugUtilsMessengerEXT _debugMessenger;
private ExtDebugUtils? _debugUtils; private ExtDebugUtils? _debugUtils;
private PhysicalDevice _physicalDevice; private PhysicalDevice _physicalDevice;
private bool _supportsIndependentBlend;
private uint _maxColorAttachments;
private Device _device; private Device _device;
private PipelineCache _pipelineCache; private PipelineCache _pipelineCache;
private string? _pipelineCachePath; private string? _pipelineCachePath;
@@ -1024,9 +1164,9 @@ internal static unsafe class VulkanVideoPresenter
private readonly record struct GraphicsPipelineKey( private readonly record struct GraphicsPipelineKey(
string VertexShader, string VertexShader,
string FragmentShader, string FragmentShader,
Format RenderTargetFormat, string RenderTargetLayout,
PrimitiveTopology Topology, PrimitiveTopology Topology,
VulkanGuestBlendState Blend, string BlendLayout,
string ResourceLayout, string ResourceLayout,
string VertexLayout); string VertexLayout);
@@ -1066,9 +1206,11 @@ internal static unsafe class VulkanVideoPresenter
public uint VertexCount = 3; public uint VertexCount = 3;
public uint InstanceCount = 1; public uint InstanceCount = 1;
public PrimitiveTopology Topology = PrimitiveTopology.TriangleList; public PrimitiveTopology Topology = PrimitiveTopology.TriangleList;
public VulkanGuestBlendState Blend = VulkanGuestBlendState.Default; public VulkanGuestBlendState[] Blends = [VulkanGuestBlendState.Default];
public VulkanGuestRect? Scissor; public VulkanGuestRect? Scissor;
public VulkanGuestViewport? Viewport; public VulkanGuestViewport? Viewport;
public RenderPass TransientRenderPass;
public Framebuffer TransientFramebuffer;
} }
private sealed class TextureResource private sealed class TextureResource
@@ -1604,6 +1746,7 @@ internal static unsafe class VulkanVideoPresenter
} }
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected); _vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
_maxColorAttachments = selected.Limits.MaxColorAttachments;
var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown"; var selectedName = SilkMarshal.PtrToString((nint)selected.DeviceName) ?? "unknown";
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})"); $"[LOADER][INFO] Vulkan device: {selectedName} ({selected.DeviceType})");
@@ -1650,8 +1793,10 @@ internal static unsafe class VulkanVideoPresenter
PQueuePriorities = &priority, PQueuePriorities = &priority,
}; };
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures); _vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
var enabledFeatures = new PhysicalDeviceFeatures var enabledFeatures = new PhysicalDeviceFeatures
{ {
IndependentBlend = supportedFeatures.IndependentBlend,
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics, VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics, FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
ShaderInt64 = supportedFeatures.ShaderInt64, ShaderInt64 = supportedFeatures.ShaderInt64,
@@ -2150,6 +2295,14 @@ internal static unsafe class VulkanVideoPresenter
} }
} }
private void WaitForAllGuestSubmissions()
{
while (_pendingGuestSubmissions.Count != 0)
{
CollectCompletedGuestSubmissions(waitForOldest: true);
}
}
private void CollectCompletedGuestSubmissions(bool waitForOldest) private void CollectCompletedGuestSubmissions(bool waitForOldest)
{ {
if (waitForOldest && _pendingGuestSubmissions.TryPeek(out var oldest)) if (waitForOldest && _pendingGuestSubmissions.TryPeek(out var oldest))
@@ -2193,13 +2346,15 @@ internal static unsafe class VulkanVideoPresenter
private IReadOnlyList<GuestImageResource> GetTraceImages( private IReadOnlyList<GuestImageResource> GetTraceImages(
TranslatedDrawResources resources, TranslatedDrawResources resources,
GuestImageResource? renderTarget = null) IReadOnlyList<GuestImageResource>? renderTargets = null)
{ {
var images = new HashSet<GuestImageResource>(); var images = new HashSet<GuestImageResource>();
if (renderTarget is not null && foreach (var renderTarget in renderTargets ?? [])
ShouldTraceGuestImageContents(renderTarget))
{ {
images.Add(renderTarget); if (ShouldTraceGuestImageContents(renderTarget))
{
images.Add(renderTarget);
}
} }
foreach (var texture in resources.Textures) foreach (var texture in resources.Textures)
@@ -2430,10 +2585,16 @@ internal static unsafe class VulkanVideoPresenter
private TranslatedDrawResources CreateTranslatedDrawResources( private TranslatedDrawResources CreateTranslatedDrawResources(
VulkanTranslatedGuestDraw draw, VulkanTranslatedGuestDraw draw,
RenderPass renderPass, RenderPass renderPass,
Format renderTargetFormat, IReadOnlyList<Format> renderTargetFormats,
Extent2D extent) Extent2D extent)
{ {
var vertexSpirv = draw.VertexSpirv; var vertexSpirv = draw.VertexSpirv;
if (draw.RenderState.Blends.Count != renderTargetFormats.Count)
{
throw new InvalidOperationException(
"color attachment formats and blend states must have matching counts");
}
if (vertexSpirv.Length == 0 && if (vertexSpirv.Length == 0 &&
!TryCompileFullscreenVertexShader( !TryCompileFullscreenVertexShader(
draw.AttributeCount, draw.AttributeCount,
@@ -2453,7 +2614,7 @@ internal static unsafe class VulkanVideoPresenter
VertexCount = GetDrawVertexCount(draw.PrimitiveType, draw.VertexCount, draw.IndexBuffer), VertexCount = GetDrawVertexCount(draw.PrimitiveType, draw.VertexCount, draw.IndexBuffer),
InstanceCount = Math.Max(draw.InstanceCount, 1), InstanceCount = Math.Max(draw.InstanceCount, 1),
Topology = GetPrimitiveTopology(draw.PrimitiveType), Topology = GetPrimitiveTopology(draw.PrimitiveType),
Blend = draw.RenderState.Blend, Blends = draw.RenderState.Blends.ToArray(),
Scissor = draw.RenderState.Scissor, Scissor = draw.RenderState.Scissor,
Viewport = draw.RenderState.Viewport, Viewport = draw.RenderState.Viewport,
}; };
@@ -2502,7 +2663,7 @@ internal static unsafe class VulkanVideoPresenter
vertexSpirv, vertexSpirv,
draw.PixelSpirv, draw.PixelSpirv,
renderPass, renderPass,
renderTargetFormat, renderTargetFormats,
extent); extent);
return resources; return resources;
} }
@@ -2799,15 +2960,18 @@ internal static unsafe class VulkanVideoPresenter
byte[] vertexSpirv, byte[] vertexSpirv,
byte[] fragmentSpirv, byte[] fragmentSpirv,
RenderPass renderPass, RenderPass renderPass,
Format renderTargetFormat, IReadOnlyList<Format> renderTargetFormats,
Extent2D extent) Extent2D extent)
{ {
var pipelineKey = new GraphicsPipelineKey( var pipelineKey = new GraphicsPipelineKey(
GetShaderDigest(vertexSpirv), GetShaderDigest(vertexSpirv),
GetShaderDigest(fragmentSpirv), GetShaderDigest(fragmentSpirv),
renderTargetFormat, string.Join(',', renderTargetFormats.Select(format => (uint)format)),
resources.Topology, resources.Topology,
resources.Blend, string.Join(';', resources.Blends.Select(blend =>
$"{(blend.Enable ? 1 : 0)}:{blend.ColorSrcFactor}:{blend.ColorDstFactor}:" +
$"{blend.ColorFunc}:{blend.AlphaSrcFactor}:{blend.AlphaDstFactor}:" +
$"{blend.AlphaFunc}:{(blend.SeparateAlphaBlend ? 1 : 0)}:{blend.WriteMask}")),
GetResourceLayoutKey(resources), GetResourceLayoutKey(resources),
GetVertexLayoutKey(resources)); GetVertexLayoutKey(resources));
if (_graphicsPipelines.TryGetValue(pipelineKey, out var cachedPipeline)) if (_graphicsPipelines.TryGetValue(pipelineKey, out var cachedPipeline))
@@ -2908,29 +3072,33 @@ internal static unsafe class VulkanVideoPresenter
SType = StructureType.PipelineMultisampleStateCreateInfo, SType = StructureType.PipelineMultisampleStateCreateInfo,
RasterizationSamples = SampleCountFlags.Count1Bit, RasterizationSamples = SampleCountFlags.Count1Bit,
}; };
var colorBlendAttachment = new PipelineColorBlendAttachmentState var colorBlendAttachments = stackalloc PipelineColorBlendAttachmentState[resources.Blends.Length];
for (var index = 0; index < resources.Blends.Length; index++)
{ {
BlendEnable = resources.Blend.Enable, var blend = resources.Blends[index];
SrcColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorSrcFactor), colorBlendAttachments[index] = new PipelineColorBlendAttachmentState
DstColorBlendFactor = ToVkBlendFactor(resources.Blend.ColorDstFactor), {
ColorBlendOp = ToVkBlendOp(resources.Blend.ColorFunc), BlendEnable = blend.Enable,
SrcAlphaBlendFactor = resources.Blend.SeparateAlphaBlend SrcColorBlendFactor = ToVkBlendFactor(blend.ColorSrcFactor),
? ToVkBlendFactor(resources.Blend.AlphaSrcFactor) DstColorBlendFactor = ToVkBlendFactor(blend.ColorDstFactor),
: ToVkBlendFactor(resources.Blend.ColorSrcFactor), ColorBlendOp = ToVkBlendOp(blend.ColorFunc),
DstAlphaBlendFactor = resources.Blend.SeparateAlphaBlend SrcAlphaBlendFactor = blend.SeparateAlphaBlend
? ToVkBlendFactor(resources.Blend.AlphaDstFactor) ? ToVkBlendFactor(blend.AlphaSrcFactor)
: ToVkBlendFactor(resources.Blend.ColorDstFactor), : ToVkBlendFactor(blend.ColorSrcFactor),
AlphaBlendOp = resources.Blend.SeparateAlphaBlend DstAlphaBlendFactor = blend.SeparateAlphaBlend
? ToVkBlendOp(resources.Blend.AlphaFunc) ? ToVkBlendFactor(blend.AlphaDstFactor)
: ToVkBlendOp(resources.Blend.ColorFunc), : ToVkBlendFactor(blend.ColorDstFactor),
ColorWriteMask = AlphaBlendOp = blend.SeparateAlphaBlend
ToVkColorWriteMask(resources.Blend.WriteMask), ? ToVkBlendOp(blend.AlphaFunc)
}; : ToVkBlendOp(blend.ColorFunc),
ColorWriteMask = ToVkColorWriteMask(blend.WriteMask),
};
}
var colorBlend = new PipelineColorBlendStateCreateInfo var colorBlend = new PipelineColorBlendStateCreateInfo
{ {
SType = StructureType.PipelineColorBlendStateCreateInfo, SType = StructureType.PipelineColorBlendStateCreateInfo,
AttachmentCount = 1, AttachmentCount = (uint)resources.Blends.Length,
PAttachments = &colorBlendAttachment, PAttachments = colorBlendAttachments,
}; };
var dynamicStateValues = stackalloc DynamicState[2]; var dynamicStateValues = stackalloc DynamicState[2];
dynamicStateValues[0] = DynamicState.Viewport; dynamicStateValues[0] = DynamicState.Viewport;
@@ -4372,6 +4540,12 @@ internal static unsafe class VulkanVideoPresenter
: checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes); : checked(((ulong)width + 3) / 4 * (((ulong)height + 3) / 4) * blockBytes);
} }
private bool SupportsColorAttachment(Format format)
{
_vk.GetPhysicalDeviceFormatProperties(_physicalDevice, format, out var properties);
return (properties.OptimalTilingFeatures & FormatFeatureFlags.ColorAttachmentBit) != 0;
}
private static Format GetTextureFormat(uint format, uint numberType) => private static Format GetTextureFormat(uint format, uint numberType) =>
(format, numberType) switch (format, numberType) switch
{ {
@@ -4434,26 +4608,6 @@ internal static unsafe class VulkanVideoPresenter
_ => Format.R8G8B8A8Unorm, _ => Format.R8G8B8A8Unorm,
}; };
private static Format GetRenderTargetFormat(uint format, uint numberType) =>
(format, numberType) switch
{
(4, 4) => Format.R32Uint,
(4, 5) => Format.R32Sint,
(4, 7) => Format.R32Sfloat,
(5, 4) => Format.R16G16Uint,
(5, 5) => Format.R16G16Sint,
(5, 7) => Format.R16G16Sfloat,
(9, _) => Format.A2R10G10B10UnormPack32,
(10, 4) => Format.R8G8B8A8Uint,
(10, 5) => Format.R8G8B8A8Sint,
(10, _) => Format.R8G8B8A8Unorm,
(12, 4) => Format.R16G16B16A16Uint,
(12, 5) => Format.R16G16B16A16Sint,
(12, 7) => Format.R16G16B16A16Sfloat,
(_, 0) => GetTextureFormat(format, numberType),
_ => Format.Undefined,
};
private static bool IsBlockCompressedFormat(Format format) => private static bool IsBlockCompressedFormat(Format format) =>
format is Format.BC1RgbaUnormBlock or format is Format.BC1RgbaUnormBlock or
Format.BC1RgbaSrgbBlock or Format.BC1RgbaSrgbBlock or
@@ -4694,47 +4848,114 @@ internal static unsafe class VulkanVideoPresenter
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work) private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
{ {
if (_deviceLost) if (_deviceLost || work.Targets.Count == 0)
{ {
return; return;
} }
var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType); if (work.Targets.Count > _maxColorAttachments)
if (format == Format.Undefined)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped unsupported render target " + $"[LOADER][WARN] Vulkan skipped MRT draw requesting {work.Targets.Count} color attachments; " +
$"addr=0x{work.Target.Address:X16} format={work.Target.Format} " + $"the selected device supports {_maxColorAttachments}.");
$"number={work.Target.NumberType}");
return; return;
} }
var targetFormats = new VulkanRenderTargetFormat[work.Targets.Count];
for (var index = 0; index < targetFormats.Length; index++)
{
var target = work.Targets[index];
if (!TryDecodeRenderTargetFormat(target.Format, target.NumberType, out targetFormats[index]) ||
!SupportsColorAttachment(targetFormats[index].Format))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped MRT draw with unsupported color target " +
$"format={target.Format} number_type={target.NumberType}.");
return;
}
}
if (work.Draw.RenderState.Blends.Count != targetFormats.Length)
{
Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw with mismatched attachment/blend counts.");
return;
}
if (targetFormats
.Select((format, index) => format.IsInteger && work.Draw.RenderState.Blends[index].Enable)
.Any(invalid => invalid))
{
Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw with blending enabled for an integer attachment.");
return;
}
if (!_supportsIndependentBlend &&
work.Draw.RenderState.Blends.Skip(1).Any(blend => blend != work.Draw.RenderState.Blends[0]))
{
Console.Error.WriteLine(
"[LOADER][WARN] Vulkan skipped MRT draw requiring unsupported independentBlend.");
return;
}
var formats = targetFormats.Select(target => target.Format).ToArray();
var targetAddresses = work.Targets
.Where(target => target.Address != 0)
.Select(target => target.Address)
.ToHashSet();
if (work.Draw.Textures.Any(texture => if (work.Draw.Textures.Any(texture =>
texture.Address == work.Target.Address && targetAddresses.Contains(texture.Address)))
texture.Address != 0))
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][WARN] Vulkan skipped render-target feedback loop " + $"[LOADER][WARN] Vulkan skipped render-target feedback loop " +
$"addr=0x{work.Target.Address:X16}"); $"targets={string.Join(',', targetAddresses.Select(address => $"0x{address:X16}"))}");
return; return;
} }
var target = GetOrCreateGuestImage(work.Target, format); var targets = new GuestImageResource[work.Targets.Count];
EnsureGuestSubmissionCapacity();
for (var index = 0; index < targets.Length; index++)
{
targets[index] = GetOrCreateGuestImage(work.Targets[index], formats[index]);
}
var firstTarget = targets[0];
TranslatedDrawResources? resources = null; TranslatedDrawResources? resources = null;
CommandBuffer commandBuffer = default; CommandBuffer commandBuffer = default;
var submitted = false; var submitted = false;
RenderPass transientRenderPass = default;
Framebuffer transientFramebuffer = default;
try try
{ {
EnsureGuestSubmissionCapacity(); var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
var extent = new Extent2D(target.Width, target.Height); var renderPass = firstTarget.RenderPass;
var framebuffer = firstTarget.Framebuffer;
if (targets.Length > 1)
{
(renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
formats,
targets.Select(target => target.MipViews[0]).ToArray(),
firstTarget.Width,
firstTarget.Height);
transientRenderPass = renderPass;
transientFramebuffer = framebuffer;
}
resources = CreateTranslatedDrawResources( resources = CreateTranslatedDrawResources(
work.Draw, work.Draw,
target.RenderPass, renderPass,
target.Format, formats,
extent); extent);
resources.TransientRenderPass = transientRenderPass;
resources.TransientFramebuffer = transientFramebuffer;
transientRenderPass = default;
transientFramebuffer = default;
resources.DebugName = resources.DebugName =
$"SharpEmu offscreen rt=0x{work.Target.Address:X16} " + $"SharpEmu offscreen mrt={targets.Length} " +
$"{work.Target.Width}x{work.Target.Height} fmt{work.Target.Format}"; $"first=0x{work.Targets[0].Address:X16} " +
$"{firstTarget.Width}x{firstTarget.Height}";
commandBuffer = AllocateGuestCommandBuffer(); commandBuffer = AllocateGuestCommandBuffer();
_commandBuffer = commandBuffer; _commandBuffer = commandBuffer;
@@ -4751,24 +4972,30 @@ internal static unsafe class VulkanVideoPresenter
RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit); RecordTextureUploads(resources, PipelineStageFlags.FragmentShaderBit);
RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit); RecordStorageImagesForWrite(resources, PipelineStageFlags.FragmentShaderBit);
var targetHasPriorContents = target.Initialized || target.InitialUploadPending; var toColorAttachments = stackalloc ImageMemoryBarrier[targets.Length];
var toColorAttachment = new ImageMemoryBarrier var anyPriorContents = false;
for (var index = 0; index < targets.Length; index++)
{ {
SType = StructureType.ImageMemoryBarrier, var hasPriorContents = targets[index].Initialized || targets[index].InitialUploadPending;
SrcAccessMask = targetHasPriorContents ? AccessFlags.ShaderReadBit : 0, anyPriorContents |= hasPriorContents;
DstAccessMask = AccessFlags.ColorAttachmentWriteBit, toColorAttachments[index] = new ImageMemoryBarrier
OldLayout = targetHasPriorContents {
? ImageLayout.ShaderReadOnlyOptimal SType = StructureType.ImageMemoryBarrier,
: ImageLayout.Undefined, SrcAccessMask = hasPriorContents ? AccessFlags.ShaderReadBit : 0,
NewLayout = ImageLayout.ColorAttachmentOptimal, DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, OldLayout = hasPriorContents
DstQueueFamilyIndex = Vk.QueueFamilyIgnored, ? ImageLayout.ShaderReadOnlyOptimal
Image = target.Image, : ImageLayout.Undefined,
SubresourceRange = ColorSubresourceRange(), NewLayout = ImageLayout.ColorAttachmentOptimal,
}; SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Image = targets[index].Image,
SubresourceRange = ColorSubresourceRange(),
};
}
_vk.CmdPipelineBarrier( _vk.CmdPipelineBarrier(
_commandBuffer, _commandBuffer,
targetHasPriorContents anyPriorContents
? PipelineStageFlags.AllCommandsBit ? PipelineStageFlags.AllCommandsBit
: PipelineStageFlags.TopOfPipeBit, : PipelineStageFlags.TopOfPipeBit,
PipelineStageFlags.ColorAttachmentOutputBit, PipelineStageFlags.ColorAttachmentOutputBit,
@@ -4777,28 +5004,32 @@ internal static unsafe class VulkanVideoPresenter
null, null,
0, 0,
null, null,
1, (uint)targets.Length,
&toColorAttachment); toColorAttachments);
RecordTranslatedGraphicsPass( RecordTranslatedGraphicsPass(
resources, resources,
target.RenderPass, renderPass,
target.Framebuffer, framebuffer,
extent); extent);
RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit); RecordStorageImagesForRead(resources, PipelineStageFlags.FragmentShaderBit);
var toShaderRead = new ImageMemoryBarrier var toShaderRead = stackalloc ImageMemoryBarrier[targets.Length];
for (var index = 0; index < targets.Length; index++)
{ {
SType = StructureType.ImageMemoryBarrier, toShaderRead[index] = new ImageMemoryBarrier
SrcAccessMask = AccessFlags.ColorAttachmentWriteBit, {
DstAccessMask = AccessFlags.ShaderReadBit, SType = StructureType.ImageMemoryBarrier,
OldLayout = ImageLayout.ColorAttachmentOptimal, SrcAccessMask = AccessFlags.ColorAttachmentWriteBit,
NewLayout = ImageLayout.ShaderReadOnlyOptimal, DstAccessMask = AccessFlags.ShaderReadBit,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, OldLayout = ImageLayout.ColorAttachmentOptimal,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored, NewLayout = ImageLayout.ShaderReadOnlyOptimal,
Image = target.Image, SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
SubresourceRange = ColorSubresourceRange(), DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
}; Image = targets[index].Image,
SubresourceRange = ColorSubresourceRange(),
};
}
_vk.CmdPipelineBarrier( _vk.CmdPipelineBarrier(
_commandBuffer, _commandBuffer,
PipelineStageFlags.ColorAttachmentOutputBit, PipelineStageFlags.ColorAttachmentOutputBit,
@@ -4808,55 +5039,70 @@ internal static unsafe class VulkanVideoPresenter
null, null,
0, 0,
null, null,
1, (uint)targets.Length,
&toShaderRead); toShaderRead);
EndDebugLabel(_commandBuffer); EndDebugLabel(_commandBuffer);
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(offscreen)"); Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(offscreen)");
SubmitGuestCommandBuffer( SubmitGuestCommandBuffer(
commandBuffer, commandBuffer,
resources, resources,
GetTraceImages(resources, target)); GetTraceImages(resources, targets));
submitted = true; submitted = true;
target.Initialized = true; foreach (var target in targets)
{
target.Initialized = true;
}
MarkSampledImagesInitialized(resources); MarkSampledImagesInitialized(resources);
MarkStorageImagesInitialized(resources, traceContents: false); MarkStorageImagesInitialized(resources, traceContents: false);
var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat( if (work.PublishTarget)
work.Target.Format,
work.Target.NumberType);
if (work.PublishTarget && guestTextureFormat != 0)
{ {
lock (_gate) for (var index = 0; index < targets.Length; index++)
{ {
_availableGuestImages[target.Address] = guestTextureFormat; var guestTextureFormat = VulkanVideoPresenter.GetGuestTextureFormat(
_gpuGuestImages[target.Address] = guestTextureFormat; work.Targets[index].Format,
work.Targets[index].NumberType);
if (guestTextureFormat == 0)
{
continue;
}
lock (_gate)
{
_availableGuestImages[targets[index].Address] = guestTextureFormat;
_gpuGuestImages[targets[index].Address] = guestTextureFormat;
}
} }
} }
if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
foreach (var target in targets)
{ {
var writeCount = _tracedGuestWriteCounts.TryGetValue( if (ShouldTraceGuestImageWriteForDiagnostics(target.Address))
target.Address,
out var previousCount)
? previousCount + 1
: 1;
_tracedGuestWriteCounts[target.Address] = writeCount;
if (writeCount <= 3)
{ {
_commandBuffer = _presentationCommandBuffer; var writeCount = _tracedGuestWriteCounts.TryGetValue(
Check( target.Address,
_vk.QueueWaitIdle(_queue), out var previousCount)
"vkQueueWaitIdle(guest write trace)"); ? previousCount + 1
Console.Error.WriteLine( : 1;
$"[LOADER][TRACE] vk.guest_write_sample " + _tracedGuestWriteCounts[target.Address] = writeCount;
$"addr=0x{target.Address:X16} write={writeCount} " + if (writeCount <= 3)
$"ps_bytes={work.Draw.PixelSpirv.Length}"); {
TraceGuestImageContents(target); _commandBuffer = _presentationCommandBuffer;
Check(
_vk.QueueWaitIdle(_queue),
"vkQueueWaitIdle(guest write trace)");
Console.Error.WriteLine(
$"[LOADER][TRACE] vk.guest_write_sample " +
$"addr=0x{target.Address:X16} write={writeCount} " +
$"ps_bytes={work.Draw.PixelSpirv.Length}");
TraceGuestImageContents(target);
}
} }
} }
TraceVulkanShader( TraceVulkanShader(
$"vk.offscreen_draw addr=0x{target.Address:X16} " + $"vk.offscreen_draw mrt={targets.Length} " +
$"size={target.Width}x{target.Height} format={target.Format} " + $"size={firstTarget.Width}x{firstTarget.Height} " +
$"textures={work.Draw.Textures.Count}"); $"textures={work.Draw.Textures.Count}");
} }
catch (Exception exception) catch (Exception exception)
@@ -4866,19 +5112,22 @@ internal static unsafe class VulkanVideoPresenter
return; return;
} }
if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) || lock (_gate)
!failedTarget.Initialized)
{ {
lock (_gate) foreach (var target in work.Targets)
{ {
_availableGuestImages.Remove(work.Target.Address); if (!_guestImages.TryGetValue(target.Address, out var failedTarget) ||
_gpuGuestImages.Remove(work.Target.Address); !failedTarget.Initialized)
{
_availableGuestImages.Remove(target.Address);
_gpuGuestImages.Remove(target.Address);
}
} }
} }
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][ERROR] Vulkan offscreen draw failed " + $"[LOADER][ERROR] Vulkan offscreen draw failed " +
$"addr=0x{work.Target.Address:X16}: {exception.Message}"); $"mrt={work.Targets.Count}: {exception.Message}");
} }
finally finally
{ {
@@ -4896,6 +5145,16 @@ internal static unsafe class VulkanVideoPresenter
{ {
DestroyTranslatedDrawResources(resources); DestroyTranslatedDrawResources(resources);
} }
if (transientFramebuffer.Handle != 0)
{
_vk.DestroyFramebuffer(_device, transientFramebuffer, null);
}
if (transientRenderPass.Handle != 0)
{
_vk.DestroyRenderPass(_device, transientRenderPass, null);
}
} }
} }
@@ -4934,6 +5193,8 @@ internal static unsafe class VulkanVideoPresenter
return existing; return existing;
} }
CompletePendingPresentation(wait: true);
WaitForAllGuestSubmissions();
DestroyGuestImage(existing); DestroyGuestImage(existing);
_guestImages.Remove(target.Address); _guestImages.Remove(target.Address);
lock (_gate) lock (_gate)
@@ -5052,35 +5313,55 @@ internal static unsafe class VulkanVideoPresenter
Format format, Format format,
ImageView attachmentView, ImageView attachmentView,
uint width, uint width,
uint height) =>
CreateRenderPassAndFramebuffer([format], [attachmentView], width, height);
private (RenderPass RenderPass, Framebuffer Framebuffer) CreateRenderPassAndFramebuffer(
IReadOnlyList<Format> formats,
IReadOnlyList<ImageView> attachmentViews,
uint width,
uint height) uint height)
{ {
var colorAttachment = new AttachmentDescription if (formats.Count == 0 || formats.Count != attachmentViews.Count)
{ {
Format = format, throw new InvalidOperationException("render target formats and views must have matching counts");
Samples = SampleCountFlags.Count1Bit, }
LoadOp = AttachmentLoadOp.Load,
StoreOp = AttachmentStoreOp.Store, var colorAttachments = stackalloc AttachmentDescription[formats.Count];
StencilLoadOp = AttachmentLoadOp.DontCare, var colorReferences = stackalloc AttachmentReference[formats.Count];
StencilStoreOp = AttachmentStoreOp.DontCare, var views = stackalloc ImageView[formats.Count];
InitialLayout = ImageLayout.ColorAttachmentOptimal, for (var index = 0; index < formats.Count; index++)
FinalLayout = ImageLayout.ColorAttachmentOptimal,
};
var colorReference = new AttachmentReference
{ {
Attachment = 0, colorAttachments[index] = new AttachmentDescription
Layout = ImageLayout.ColorAttachmentOptimal, {
}; Format = formats[index],
Samples = SampleCountFlags.Count1Bit,
LoadOp = AttachmentLoadOp.Load,
StoreOp = AttachmentStoreOp.Store,
StencilLoadOp = AttachmentLoadOp.DontCare,
StencilStoreOp = AttachmentStoreOp.DontCare,
InitialLayout = ImageLayout.ColorAttachmentOptimal,
FinalLayout = ImageLayout.ColorAttachmentOptimal,
};
colorReferences[index] = new AttachmentReference
{
Attachment = (uint)index,
Layout = ImageLayout.ColorAttachmentOptimal,
};
views[index] = attachmentViews[index];
}
var subpass = new SubpassDescription var subpass = new SubpassDescription
{ {
PipelineBindPoint = PipelineBindPoint.Graphics, PipelineBindPoint = PipelineBindPoint.Graphics,
ColorAttachmentCount = 1, ColorAttachmentCount = (uint)formats.Count,
PColorAttachments = &colorReference, PColorAttachments = colorReferences,
}; };
var renderPassInfo = new RenderPassCreateInfo var renderPassInfo = new RenderPassCreateInfo
{ {
SType = StructureType.RenderPassCreateInfo, SType = StructureType.RenderPassCreateInfo,
AttachmentCount = 1, AttachmentCount = (uint)formats.Count,
PAttachments = &colorAttachment, PAttachments = colorAttachments,
SubpassCount = 1, SubpassCount = 1,
PSubpasses = &subpass, PSubpasses = &subpass,
}; };
@@ -5088,13 +5369,12 @@ internal static unsafe class VulkanVideoPresenter
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass), _vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
"vkCreateRenderPass(offscreen)"); "vkCreateRenderPass(offscreen)");
var attachment = attachmentView;
var framebufferInfo = new FramebufferCreateInfo var framebufferInfo = new FramebufferCreateInfo
{ {
SType = StructureType.FramebufferCreateInfo, SType = StructureType.FramebufferCreateInfo,
RenderPass = renderPass, RenderPass = renderPass,
AttachmentCount = 1, AttachmentCount = (uint)formats.Count,
PAttachments = &attachment, PAttachments = views,
Width = width, Width = width,
Height = height, Height = height,
Layers = 1, Layers = 1,
@@ -5632,7 +5912,7 @@ internal static unsafe class VulkanVideoPresenter
translatedResources = CreateTranslatedDrawResources( translatedResources = CreateTranslatedDrawResources(
translatedDraw, translatedDraw,
_renderPass, _renderPass,
_swapchainFormat, [_swapchainFormat],
_extent); _extent);
if (ShouldTracePresentedGuestImageContentsForDiagnostics() && if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
!_firstGuestDrawPresented) !_firstGuestDrawPresented)
@@ -6527,6 +6807,16 @@ internal static unsafe class VulkanVideoPresenter
private void DestroyTranslatedDrawResources(TranslatedDrawResources resources) private void DestroyTranslatedDrawResources(TranslatedDrawResources resources)
{ {
if (resources.TransientFramebuffer.Handle != 0)
{
_vk.DestroyFramebuffer(_device, resources.TransientFramebuffer, null);
}
if (resources.TransientRenderPass.Handle != 0)
{
_vk.DestroyRenderPass(_device, resources.TransientRenderPass, null);
}
foreach (var texture in resources.Textures) foreach (var texture in resources.Textures)
{ {
if (texture is null) if (texture is null)
@@ -0,0 +1,108 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using Xunit;
namespace SharpEmu.Libs.Tests.Ampr;
// PakDirectoryTracker reconstructs the absolute pak offset for a "next sequential chunk" read
// (offset -1) from the requested byte count. When several archived files share that byte count,
// picking by directory order alone mis-resolves out-of-order reads: Quake requested progs/h_ogre.mdl
// (0x3A34 bytes) but the tracker returned bots/navigation/death32c.nav, which shares the size and
// sits earlier in the directory, so the guest parsed NAV2 data as a brush model and aborted.
public sealed class PakDirectoryTrackerTests
{
private const int EntrySize = 64;
private const int NameLength = 56;
private readonly record struct PakEntry(string Name, uint FilePos, uint FileLen);
[Fact]
public void ResolveSequentialOffset_SizeCollision_PicksEntryNearestReadCursor()
{
const uint fileId = 0x5AA5_0001;
const uint collidingLen = 0x3A34;
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
var ctx = new CpuContext(memory, Generation.Gen5);
// "far" collides in size but lives early in the pak; "near" is what the guest actually wants.
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
{
new PakEntry("far.dat", FilePos: 0x1000, FileLen: collidingLen),
new PakEntry("near.dat", FilePos: 0x9000, FileLen: collidingLen),
});
// Advance the read cursor next to the "near" entry, mimicking a burst of nearby asset reads.
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: 0x8800, bytesRead: 0x400);
Assert.Equal(0x9000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
// Once the near entry is consumed, the same size resolves to the remaining match.
Assert.Equal(0x1000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
}
[Fact]
public void ResolveSequentialOffset_ContiguousSameSizeRun_StaysInOrder()
{
const uint fileId = 0x5AA5_0002;
const uint runLen = 0x608;
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
var ctx = new CpuContext(memory, Generation.Gen5);
// A contiguous run of equal-size lumps (like Quake's gfx/weapons/ww_*.lmp) must still resolve
// in packed order when the guest streams them sequentially.
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
{
new PakEntry("lump_a", FilePos: 0x1000, FileLen: runLen),
new PakEntry("lump_b", FilePos: 0x1000 + runLen, FileLen: runLen),
new PakEntry("lump_c", FilePos: 0x1000 + (runLen * 2), FileLen: runLen),
});
var first = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: first, bytesRead: runLen);
var second = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: second, bytesRead: runLen);
var third = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
Assert.Equal(0x1000UL, first);
Assert.Equal(0x1000UL + runLen, second);
Assert.Equal(0x1000UL + (runLen * 2), third);
}
// Feeds the tracker a synthetic PACK header + directory table exactly as the AMPR read path does:
// first the 12-byte header (which arms directory parsing), then the directory records themselves.
private static void LoadDirectory(
CpuContext ctx,
uint fileId,
FakeCpuMemory memory,
ulong destination,
ulong dirFileOffset,
PakEntry[] entries)
{
Span<byte> header = stackalloc byte[12];
header[0] = (byte)'P';
header[1] = (byte)'A';
header[2] = (byte)'C';
header[3] = (byte)'K';
BinaryPrimitives.WriteUInt32LittleEndian(header[4..8], (uint)dirFileOffset);
BinaryPrimitives.WriteUInt32LittleEndian(header[8..12], (uint)(entries.Length * EntrySize));
memory.TryWrite(destination, header);
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset: 0, bytesRead: 12);
var table = new byte[entries.Length * EntrySize];
for (var i = 0; i < entries.Length; i++)
{
var record = table.AsSpan(i * EntrySize, EntrySize);
var name = Encoding.ASCII.GetBytes(entries[i].Name);
name.AsSpan(0, Math.Min(name.Length, NameLength)).CopyTo(record);
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(56, 4), entries[i].FilePos);
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(60, 4), entries[i].FileLen);
}
memory.TryWrite(destination, table);
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, dirFileOffset, (ulong)table.Length);
}
}
@@ -0,0 +1,68 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Tests;
// A single contiguous guest region backed by a byte[]. Enough to hand C strings and small
// structures to HLE exports under test without a live guest.
internal sealed class FakeCpuMemory : ICpuMemory
{
private readonly ulong _base;
private readonly byte[] _storage;
public FakeCpuMemory(ulong baseAddress, int size)
{
_base = baseAddress;
_storage = new byte[size];
}
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
if (!TryResolve(virtualAddress, destination.Length, out var offset))
{
return false;
}
_storage.AsSpan(offset, destination.Length).CopyTo(destination);
return true;
}
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{
if (!TryResolve(virtualAddress, source.Length, out var offset))
{
return false;
}
source.CopyTo(_storage.AsSpan(offset, source.Length));
return true;
}
public ulong WriteCString(ulong virtualAddress, string text)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(text);
TryWrite(virtualAddress, bytes);
TryWrite(virtualAddress + (ulong)bytes.Length, stackalloc byte[] { 0 });
return virtualAddress;
}
private bool TryResolve(ulong virtualAddress, int length, out int offset)
{
offset = 0;
if (virtualAddress < _base)
{
return false;
}
var relative = virtualAddress - _base;
if (relative + (ulong)length > (ulong)_storage.Length)
{
return false;
}
offset = (int)relative;
return true;
}
}
@@ -0,0 +1,80 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Json;
using Xunit;
namespace SharpEmu.Libs.Tests.Json;
// These NIDs came back "unresolved" in the Quake (PPSA01880) import log right before its
// access violation. This asserts they now resolve to the Json handlers and dispatch cleanly,
// which is the plumbing the direct-call tests cannot cover.
[Collection("JsonObjectHeap")]
public sealed class JsonExportRegistrationTests
{
private static readonly (string Nid, string Name)[] ExpectedExports =
{
("qBMjqyBn3OM", "_ZN3sce4Json5ValueC1Ev"),
("5yHuiWXo2gg", "_ZN3sce4Json5Value3setEb"),
("QxVVYhP-mvg", "_ZN3sce4Json5Value3setEl"),
("SIe1ZmW7e7s", "_ZN3sce4Json5Value3setEm"),
("BSmWDIkV4w4", "_ZN3sce4Json5Value3setEd"),
("IKQimvG9Wqs", "_ZN3sce4Json5Value3setENS0_9ValueTypeE"),
("6l3Bv2gysNc", "_ZN3sce4Json5Value3setERKNS0_6StringE"),
("9KUZFjI1IxA", "_ZN3sce4Json6StringC1EPKc"),
("cG1VE2HMl6c", "_ZN3sce4Json6StringD1Ev"),
("+drDFyAS6u4", "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_"),
};
private static ModuleManager CreateRegisteredManager()
{
var manager = new ModuleManager();
manager.RegisterFromAssembly(typeof(JsonValueExports).Assembly, Generation.Gen5);
return manager;
}
[Fact]
public void QuakeUnresolvedJsonNids_ResolveToJsonExports()
{
var manager = CreateRegisteredManager();
foreach (var (nid, name) in ExpectedExports)
{
Assert.True(manager.TryGetExport(nid, out var export), $"NID {nid} did not register.");
Assert.Equal(name, export.Name);
Assert.Equal("libSceJson", export.LibraryName);
}
}
[Fact]
public void SetGlobalNullAccessCallback_StoresHookAndReturnsOk()
{
JsonObjectHeap.ResetForTests();
var manager = CreateRegisteredManager();
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
ctx[CpuRegister.Rdi] = 0x1_0000_0000; // Initializer instance
ctx[CpuRegister.Rsi] = 0x8_0012_3456; // guest callback
ctx[CpuRegister.Rdx] = 0x1_0000_0800; // user context
Assert.True(manager.TryDispatch("+drDFyAS6u4", ctx, out var result));
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
Assert.Equal(0UL, ctx[CpuRegister.Rax]);
Assert.Equal(0x8_0012_3456UL, JsonObjectHeap.GlobalNullAccessCallback);
Assert.Equal(0x1_0000_0800UL, JsonObjectHeap.GlobalNullAccessCallbackContext);
}
[Fact]
public void DispatchValueConstructor_RunsHandlerAndReturnsThis()
{
JsonObjectHeap.ResetForTests();
var manager = CreateRegisteredManager();
var ctx = new CpuContext(new FakeCpuMemory(0x1_0000_0000, 0x1000), Generation.Gen5);
ctx[CpuRegister.Rdi] = 0x1_0000_0000;
Assert.True(manager.TryDispatch("qBMjqyBn3OM", ctx, out var result));
Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result);
Assert.Equal(0x1_0000_0000UL, ctx[CpuRegister.Rax]);
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[0x1_0000_0000].Kind);
}
}
@@ -0,0 +1,188 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Json;
using Xunit;
namespace SharpEmu.Libs.Tests.Json;
// JsonObjectHeap is shared static state; both Json test classes join one collection so xUnit
// does not run them in parallel against it.
[Collection("JsonObjectHeap")]
public sealed class JsonValueExportsTests
{
private const ulong ThisAddress = 0x1_0000_0000;
private const ulong StringAddress = 0x1_0000_1000;
private const ulong TextAddress = 0x1_0000_2000;
private readonly FakeCpuMemory _memory = new(0x1_0000_0000, 0x10000);
private readonly CpuContext _ctx;
public JsonValueExportsTests()
{
JsonObjectHeap.ResetForTests();
_ctx = new CpuContext(_memory, Generation.Gen5);
}
[Fact]
public void ValueDefaultConstructor_RegistersNullAndReturnsThis()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
JsonValueExports.ValueDefaultConstructor(_ctx);
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
Assert.Equal(JsonValueKind.Null, JsonObjectHeap.Values[ThisAddress].Kind);
}
[Theory]
[InlineData(0UL, false)]
[InlineData(1UL, true)]
[InlineData(0xFFFF_FF00UL, false)] // only the low byte is the bool; 0x00 low byte => false
public void ValueSetBoolean_StoresLowByte(ulong raw, bool expected)
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = raw;
JsonValueExports.ValueSetBoolean(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.Boolean, state.Kind);
Assert.Equal(expected, state.Boolean);
Assert.Equal(ThisAddress, _ctx[CpuRegister.Rax]);
}
[Fact]
public void ValueSetInteger_RoundTripsSignedValue()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = unchecked((ulong)-42L);
JsonValueExports.ValueSetInteger(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.Integer, state.Kind);
Assert.Equal(-42L, state.Integer);
}
[Fact]
public void ValueSetUnsigned_RoundTripsFullWidth()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = ulong.MaxValue;
JsonValueExports.ValueSetUnsigned(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.UInteger, state.Kind);
Assert.Equal(ulong.MaxValue, state.UnsignedInteger);
}
[Fact]
public void ValueSetReal_ReadsDoubleFromXmm0()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx.SetXmmRegister(0, unchecked((ulong)BitConverter.DoubleToInt64Bits(3.14159)), 0);
JsonValueExports.ValueSetReal(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.Real, state.Kind);
Assert.Equal(3.14159, state.Real, precision: 10);
}
[Fact]
public void ValueSetCString_ReadsGuestString()
{
_memory.WriteCString(TextAddress, "hello json");
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = TextAddress;
JsonValueExports.ValueSetCString(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.String, state.Kind);
Assert.Equal("hello json", state.Text);
}
[Fact]
public void ValueSetType_KeepsRawGuestEnumValue()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = 7;
JsonValueExports.ValueSetType(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.ExplicitType, state.Kind);
Assert.Equal(7u, state.ExplicitType);
}
[Fact]
public void StringConstructThenValueSetString_CopiesText()
{
_memory.WriteCString(TextAddress, "from string object");
_ctx[CpuRegister.Rdi] = StringAddress;
_ctx[CpuRegister.Rsi] = TextAddress;
JsonValueExports.StringCStringConstructor(_ctx);
Assert.Equal("from string object", JsonObjectHeap.Strings[StringAddress]);
Assert.Equal(StringAddress, _ctx[CpuRegister.Rax]);
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = StringAddress;
JsonValueExports.ValueSetString(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.String, state.Kind);
Assert.Equal("from string object", state.Text);
}
[Fact]
public void ValueSetString_MissingStringShadow_DegradesToEmpty()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = StringAddress; // never constructed
JsonValueExports.ValueSetString(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.String, state.Kind);
Assert.Equal(string.Empty, state.Text);
}
[Fact]
public void Destructors_RemoveShadowState()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
JsonValueExports.ValueDefaultConstructor(_ctx);
_ctx[CpuRegister.Rdi] = StringAddress;
JsonValueExports.StringDefaultConstructor(_ctx);
Assert.True(JsonObjectHeap.Values.ContainsKey(ThisAddress));
Assert.True(JsonObjectHeap.Strings.ContainsKey(StringAddress));
_ctx[CpuRegister.Rdi] = ThisAddress;
JsonValueExports.ValueDestructor(_ctx);
_ctx[CpuRegister.Rdi] = StringAddress;
JsonValueExports.StringDestructor(_ctx);
Assert.False(JsonObjectHeap.Values.ContainsKey(ThisAddress));
Assert.False(JsonObjectHeap.Strings.ContainsKey(StringAddress));
Assert.Equal(0UL, _ctx[CpuRegister.Rax]);
}
[Fact]
public void ValueSetCString_FaultingPointer_DegradesToEmptyString()
{
_ctx[CpuRegister.Rdi] = ThisAddress;
_ctx[CpuRegister.Rsi] = 0xDEAD_0000_0000; // outside the mapped region
JsonValueExports.ValueSetCString(_ctx);
var state = JsonObjectHeap.Values[ThisAddress];
Assert.Equal(JsonValueKind.String, state.Kind);
Assert.Equal(string.Empty, state.Text);
}
}
@@ -0,0 +1,254 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Memory;
using SharpEmu.Core.Loader;
using SharpEmu.HLE.Host;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory;
public sealed class GuestMemoryAllocatorTests
{
[Fact]
public void FreedRangesAreReusedAndCoalesced()
{
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize - 0xC000, 0x1000, out var third));
Assert.False(memory.TryAllocateGuestMemory(1, 1, out _));
Assert.True(memory.TryFreeGuestMemory(second));
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var reused));
Assert.Equal(second, reused);
Assert.True(memory.TryFreeGuestMemory(first));
Assert.True(memory.TryFreeGuestMemory(reused));
Assert.True(memory.TryFreeGuestMemory(third));
Assert.False(memory.TryFreeGuestMemory(third));
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize, 0x1000, out var coalesced));
Assert.Equal(first, coalesced);
}
[Fact]
public void SegmentProtectionIsAppliedInContiguousRuns()
{
const ulong pageSize = 0x1000;
using var host = new RecordingHostMemory(3 * pageSize);
using var memory = new PhysicalVirtualMemory(host);
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Read);
Assert.Equal(
[
(host.Address, 3 * pageSize, HostPageProtection.ReadWrite),
(host.Address, 3 * pageSize, HostPageProtection.ReadOnly),
],
host.ProtectionCalls);
host.ProtectionCalls.Clear();
memory.Map(host.Address + pageSize, pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Write);
host.ProtectionCalls.Clear();
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Execute);
Assert.Equal(
[
(host.Address, 3 * pageSize, HostPageProtection.ReadWriteExecute),
(host.Address, pageSize, HostPageProtection.ReadExecute),
(host.Address + pageSize, pageSize, HostPageProtection.ReadWriteExecute),
(host.Address + (2 * pageSize), pageSize, HostPageProtection.ReadExecute),
],
host.ProtectionCalls);
}
[Fact]
public unsafe void GetPointerCommitsLazyPageBeforeReturningIt()
{
const ulong address = 0x00005000_0000_0000;
const ulong pageSize = 0x1000;
using var host = new LazyHostMemory(address);
using var memory = new PhysicalVirtualMemory(host);
memory.AllocateAt(address, (4UL << 30) + pageSize, executable: false, allowAlternative: false);
host.CommitCalls.Clear();
var pointer = memory.GetPointer(address + 0x123);
Assert.Equal(address + 0x123, (ulong)pointer);
Assert.Equal([(address, pageSize, HostPageProtection.ReadWrite)], host.CommitCalls);
}
[Fact]
public unsafe void GetPointerReturnsNullWhenLazyCommitFails()
{
const ulong address = 0x00005000_0000_0000;
using var host = new LazyHostMemory(address);
using var memory = new PhysicalVirtualMemory(host);
memory.AllocateAt(address, (4UL << 30) + 0x1000, executable: false, allowAlternative: false);
host.CommitCalls.Clear();
host.CommitSucceeds = false;
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
}
private sealed class FakeHostMemory : IHostMemory
{
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
Allocate(desiredAddress, size, protection);
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
public bool Free(ulong address) => true;
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
info = default;
return false;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
}
private sealed class RecordingHostMemory : IHostMemory, IDisposable
{
private readonly nint _allocation;
private bool _freed;
public RecordingHostMemory(ulong size)
{
_allocation = System.Runtime.InteropServices.Marshal.AllocHGlobal(checked((nint)(size + 0xFFF)));
Address = (unchecked((ulong)_allocation) + 0xFFF) & ~0xFFFUL;
}
public ulong Address { get; }
public List<(ulong Address, ulong Size, HostPageProtection Protection)> ProtectionCalls { get; } = [];
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
desiredAddress == Address ? Address : 0;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
public bool Free(ulong address)
{
if (address != Address || _freed)
{
return false;
}
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
_freed = true;
return true;
}
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
ProtectionCalls.Add((address, size, protection));
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong address, out HostRegionInfo info)
{
info = default;
return false;
}
public void FlushInstructionCache(ulong address, ulong size)
{
}
public void Dispose()
{
if (!_freed)
{
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
_freed = true;
}
}
}
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
{
public bool CommitSucceeds { get; set; } = true;
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
desiredAddress == address ? address : 0;
public bool Commit(ulong commitAddress, ulong size, HostPageProtection protection)
{
CommitCalls.Add((commitAddress, size, protection));
return CommitSucceeds;
}
public bool Free(ulong freeAddress) => freeAddress == address;
public bool Protect(ulong protectAddress, ulong size, HostPageProtection protection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool ProtectRaw(ulong protectAddress, ulong size, uint rawProtection, out uint rawOldProtection)
{
rawOldProtection = 0;
return true;
}
public bool Query(ulong queryAddress, out HostRegionInfo info)
{
var pageAddress = queryAddress & ~0xFFFUL;
info = new HostRegionInfo(
pageAddress,
address,
0x1000,
HostRegionState.Reserved,
0,
HostPageProtection.NoAccess,
0,
0);
return true;
}
public void FlushInstructionCache(ulong flushAddress, ulong size)
{
}
public void Dispose()
{
}
}
}
@@ -0,0 +1,87 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using Xunit;
namespace SharpEmu.Libs.Tests.Memory;
public sealed class VirtualMemoryTests
{
[Fact]
public void OutOfOrderMappingsRemainSortedAndResolveAtBoundaries()
{
var memory = new VirtualMemory();
memory.Map(0x3000, 0x100, 0, [3], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
memory.Map(0x1000, 0x100, 0, [1], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
memory.Map(0x2000, 0x100, 0, [2], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
Assert.Equal([0x1000UL, 0x2000UL, 0x3000UL], memory.SnapshotRegions().Select(region => region.VirtualAddress));
Span<byte> value = stackalloc byte[1];
Assert.True(memory.TryRead(0x1000, value));
Assert.Equal(1, value[0]);
Assert.True(memory.TryRead(0x2000, value));
Assert.Equal(2, value[0]);
Assert.True(memory.TryRead(0x3000, value));
Assert.Equal(3, value[0]);
Assert.False(memory.TryRead(0x1100, value));
Assert.False(memory.TryRead(0x0FFF, value));
}
[Fact]
public void MappingRejectsOverlapWithEitherNeighbor()
{
var memory = new VirtualMemory();
memory.Map(0x2000, 0x100, 0, [], ProgramHeaderFlags.Read);
memory.Map(0x4000, 0x100, 0, [], ProgramHeaderFlags.Read);
Assert.Throws<InvalidOperationException>(() =>
memory.Map(0x1FFF, 2, 0, [], ProgramHeaderFlags.Read));
Assert.Throws<InvalidOperationException>(() =>
memory.Map(0x3FFF, 2, 0, [], ProgramHeaderFlags.Read));
memory.Map(0x2100, 0x1F00, 0, [], ProgramHeaderFlags.Read);
Assert.Equal(3, memory.SnapshotRegions().Count);
}
[Fact]
public void ReadAndWriteSpanAdjacentRegions()
{
var memory = new VirtualMemory();
const ProgramHeaderFlags protection = ProgramHeaderFlags.Read | ProgramHeaderFlags.Write;
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], protection);
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], protection);
Span<byte> read = stackalloc byte[4];
Assert.True(memory.TryRead(0x1002, read));
Assert.Equal([3, 4, 5, 6], read.ToArray());
Assert.True(memory.TryWrite(0x1002, [9, 10, 11, 12]));
Assert.True(memory.TryRead(0x1000, read));
Assert.Equal([1, 2, 9, 10], read.ToArray());
Assert.True(memory.TryRead(0x1004, read));
Assert.Equal([11, 12, 7, 8], read.ToArray());
}
[Fact]
public void AccessRequiresPermissionAcrossEntireRange()
{
var memory = new VirtualMemory();
memory.Map(0x1000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
memory.Map(0x1004, 4, 0, [5, 6, 7, 8], ProgramHeaderFlags.Read);
memory.Map(0x2000, 4, 0, [1, 2, 3, 4], ProgramHeaderFlags.Execute);
memory.Map(0x3000, 4, 0, [], ProgramHeaderFlags.Write);
Assert.False(memory.TryWrite(0x1002, [9, 9, 9, 9]));
Span<byte> unchanged = stackalloc byte[4];
Assert.True(memory.TryRead(0x1000, unchanged));
Assert.Equal([1, 2, 3, 4], unchanged.ToArray());
Assert.False(memory.TryRead(0x2000, unchanged));
Assert.False(memory.TryWrite(0x2000, [9]));
Assert.False(memory.TryRead(0x3000, unchanged));
Assert.True(memory.TryWrite(0x3000, [9]));
}
}
@@ -0,0 +1,24 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.Libs\SharpEmu.Libs.csproj" />
<ProjectReference Include="..\..\src\SharpEmu.HLE\SharpEmu.HLE.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
</Project>
@@ -0,0 +1,227 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
"requested": "[17.14.1, )",
"resolved": "17.14.1",
"contentHash": "HJKqKOE+vshXra2aEHpi2TlxYX7Z9VFYkr+E5rwEvHC8eIXiyO+K9kNm8vmNom3e2rA56WqxU+/N9NJlLGXsJQ==",
"dependencies": {
"Microsoft.CodeCoverage": "17.14.1",
"Microsoft.TestPlatform.TestHost": "17.14.1"
}
},
"xunit": {
"type": "Direct",
"requested": "[2.9.3, )",
"resolved": "2.9.3",
"contentHash": "TlXQBinK35LpOPKHAqbLY4xlEen9TBafjs0V5KnA4wZsoQLQJiirCR4CbIXvOH8NzkW4YeJKP5P/Bnrodm0h9Q==",
"dependencies": {
"xunit.analyzers": "1.18.0",
"xunit.assert": "2.9.3",
"xunit.core": "[2.9.3]"
}
},
"xunit.runner.visualstudio": {
"type": "Direct",
"requested": "[3.1.1, )",
"resolved": "3.1.1",
"contentHash": "gNu2zhnuwjq5vQlU4S7yK/lfaKZDLmtcu+vTjnhfTlMAUYn+Hmgu8IIX0UCwWepYkk+Szx03DHx1bDnc9Fd+9w=="
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "17.14.1",
"contentHash": "pmTrhfFIoplzFVbhVwUquT+77CbGH+h4/3mBpdmIlYtBi9nAB+kKI6dN3A/nV4DFi3wLLx/BlHIPK+MkbQ6Tpg=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "17.14.1",
"contentHash": "xTP1W6Mi6SWmuxd3a+jj9G9UoC850WGwZUps1Wah9r1ZxgXhdJfj1QqDLJkFjHDCvN42qDL2Ps5KjQYWUU0zcQ=="
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
"resolved": "17.14.1",
"contentHash": "d78LPzGKkJwsJXAQwsbJJ7LE7D1wB+rAyhHHAaODF+RDSQ0NgMjDFkSA1Djw18VrxO76GlKAjRUhl+H8NL8Z+Q==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "17.14.1",
"Newtonsoft.Json": "13.0.3"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.3",
"contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"xunit.abstractions": {
"type": "Transitive",
"resolved": "2.0.3",
"contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg=="
},
"xunit.analyzers": {
"type": "Transitive",
"resolved": "1.18.0",
"contentHash": "OtFMHN8yqIcYP9wcVIgJrq01AfTxijjAqVDy/WeQVSyrDC1RzBWeQPztL49DN2syXRah8TYnfvk035s7L95EZQ=="
},
"xunit.assert": {
"type": "Transitive",
"resolved": "2.9.3",
"contentHash": "/Kq28fCE7MjOV42YLVRAJzRF0WmEqsmflm0cfpMjGtzQ2lR5mYVj1/i0Y8uDAOLczkL3/jArrwehfMD0YogMAA=="
},
"xunit.core": {
"type": "Transitive",
"resolved": "2.9.3",
"contentHash": "BiAEvqGvyme19wE0wTKdADH+NloYqikiU0mcnmiNyXaF9HyHmE6sr/3DC5vnBkgsWaE6yPyWszKSPSApWdRVeQ==",
"dependencies": {
"xunit.extensibility.core": "[2.9.3]",
"xunit.extensibility.execution": "[2.9.3]"
}
},
"xunit.extensibility.core": {
"type": "Transitive",
"resolved": "2.9.3",
"contentHash": "kf3si0YTn2a8J8eZNb+zFpwfoyvIrQ7ivNk5ZYA5yuYk1bEtMe4DxJ2CF/qsRgmEnDr7MnW1mxylBaHTZ4qErA==",
"dependencies": {
"xunit.abstractions": "2.0.3"
}
},
"xunit.extensibility.execution": {
"type": "Transitive",
"resolved": "2.9.3",
"contentHash": "yMb6vMESlSrE3Wfj7V6cjQ3S4TXdXpRqYeNEI3zsX31uTsGMJjEw6oD5F5u1cHnMptjhEECnmZSsPxB6ChZHDQ==",
"dependencies": {
"xunit.extensibility.core": "[2.9.3]"
}
},
"sharpemu.core": {
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
"SharpEmu.HLE": "[1.0.0, )",
"SharpEmu.Libs": "[1.0.0, )",
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
"Silk.NET.Windowing": "[2.23.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"Silk.NET.Core": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.EXT": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.KHR": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
}
}
}
}
+155 -2
View File
@@ -6,7 +6,7 @@
// Feeds hand-assembled Gen5 (gfx10) instruction words through the real // Feeds hand-assembled Gen5 (gfx10) instruction words through the real
// decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via // decode -> SPIR-V pipeline (Gen5ShaderTranslator / Gen5SpirvTranslator, via
// reflection so no emulator source changes are required) and writes the // reflection so no emulator source changes are required) and writes the
// resulting vertex and compute SPIR-V blobs to disk. The blobs can then be // resulting vertex, pixel, and compute SPIR-V blobs to disk. The blobs can then be
// checked with spirv-val / spirv-dis. // checked with spirv-val / spirv-dis.
// //
// Programs that contain buffer_store_dword automatically get a single // Programs that contain buffer_store_dword automatically get a single
@@ -14,7 +14,7 @@
// guestBuffers[0] (descriptor set 0, binding 0). // guestBuffers[0] (descriptor set 0, binding 0).
// //
// Each program carries an expectation: ExpectTranslate=true programs must // Each program carries an expectation: ExpectTranslate=true programs must
// decode and emit both stages; ExpectTranslate=false programs pin a decode // decode and emit the requested stages; ExpectTranslate=false programs pin a decode
// failure that must stay loud. Any unexpected outcome makes the tool exit // failure that must stay loud. Any unexpected outcome makes the tool exit
// non-zero, so it can gate scripts/CI. // non-zero, so it can gate scripts/CI.
// //
@@ -43,6 +43,63 @@ const ulong ProgramAddress = 0x100000;
0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2 0xD56C0005, 0x00020501, // v_mul_hi_i32 v5, v1, v2
0xBF810000, // s_endpgm 0xBF810000, // s_endpgm
]), ]),
("mrt", true, [
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f
0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f
0x7E0802FF, 0x00000001, // v_mov_b32 v4, 1u
0x7E0A02FF, 0x00000002, // v_mov_b32 v5, 2u
0x7E0C02FF, 0x00000003, // v_mov_b32 v6, 3u
0x7E0E02FF, 0x00000004, // v_mov_b32 v7, 4u
0x7E1002FF, 0xFFFFFFFF, // v_mov_b32 v8, -1
0x7E1202FF, 0x00000002, // v_mov_b32 v9, 2
0x7E1402FF, 0xFFFFFFFD, // v_mov_b32 v10, -3
0x7E1602FF, 0x00000004, // v_mov_b32 v11, 4
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
0xF800003F, 0x07060504, // exp mrt3 v4, v5, v6, v7
0xF800086F, 0x0B0A0908, // exp mrt6 v8, v9, v10, v11 done
0xBF810000, // s_endpgm
]),
("mrt-float2", true, [
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
0x7E0202FF, 0x3E800000, // v_mov_b32 v1, 0.25f
0x7E0402FF, 0x3E800000, // v_mov_b32 v2, 0.25f
0x7E0602FF, 0x3F000000, // v_mov_b32 v3, 0.5f
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
0xF800081F, 0x03020100, // exp mrt1 v0, v1, v2, v3 done
0xBF810000, // s_endpgm
]),
("mrt8", true, [
0x7E0002FF, 0x3F800000, // v_mov_b32 v0, 1.0f
0x7E0202FF, 0x00000000, // v_mov_b32 v1, 0.0f
0x7E0402FF, 0x00000000, // v_mov_b32 v2, 0.0f
0x7E0602FF, 0x3F800000, // v_mov_b32 v3, 1.0f
0xF800000F, 0x03020100, // exp mrt0 v0, v1, v2, v3
0xF800001F, 0x03020100, // exp mrt1 v0, v1, v2, v3
0xF800002F, 0x03020100, // exp mrt2 v0, v1, v2, v3
0xF800003F, 0x03020100, // exp mrt3 v0, v1, v2, v3
0xF800004F, 0x03020100, // exp mrt4 v0, v1, v2, v3
0xF800005F, 0x03020100, // exp mrt5 v0, v1, v2, v3
0xF800006F, 0x03020100, // exp mrt6 v0, v1, v2, v3
0xF800087F, 0x03020100, // exp mrt7 v0, v1, v2, v3 done
0xBF810000, // s_endpgm
]),
("mrt-partial", true, [
0x7E0002FF, 0x3F4CCCCD, // v_mov_b32 v0, 0.8f
0x7E0202FF, 0x3F333333, // v_mov_b32 v1, 0.7f
0xF8000803, 0x03020100, // exp mrt0 v0, v1, off, off done
0xBF810000, // s_endpgm
]),
("mrt-partial-merge", true, [
0x7E0002FF, 0x3DCCCCCD, // v_mov_b32 v0, 0.1f
0x7E0202FF, 0x3E4CCCCD, // v_mov_b32 v1, 0.2f
0x7E0C02FF, 0x3E99999A, // v_mov_b32 v6, 0.3f
0x7E0E02FF, 0x3ECCCCCD, // v_mov_b32 v7, 0.4f
0xF8000003, 0x03020100, // exp mrt0 v0, v1, off, off
0xF800080C, 0x07060504, // exp mrt0 off, off, v6, v7 done
0xBF810000, // s_endpgm
]),
("sopp-hints", true, [ ("sopp-hints", true, [
0xBFA10001, // s_clause 0x1 0xBFA10001, // s_clause 0x1
0xBFA30000, // s_waitcnt_depctr 0x0 0xBFA30000, // s_waitcnt_depctr 0x0
@@ -102,10 +159,18 @@ var imageBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5ImageBinding")
?? throw new InvalidOperationException("Gen5ImageBinding not found"); ?? throw new InvalidOperationException("Gen5ImageBinding not found");
var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding") var globalBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5GlobalMemoryBinding")
?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found"); ?? throw new InvalidOperationException("Gen5GlobalMemoryBinding not found");
var pixelOutputBindingType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputBinding")
?? throw new InvalidOperationException("Gen5PixelOutputBinding not found");
var pixelOutputKindType = assembly.GetType("SharpEmu.Libs.Agc.Gen5PixelOutputKind")
?? throw new InvalidOperationException("Gen5PixelOutputKind not found");
var tryCompile = spirvTranslator.GetMethod( var tryCompile = spirvTranslator.GetMethod(
"TryCompileVertexShader", "TryCompileVertexShader",
BindingFlags.Public | BindingFlags.Static) BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found"); ?? throw new InvalidOperationException("Gen5SpirvTranslator.TryCompileVertexShader not found");
var tryCompilePixel = spirvTranslator.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Single(method =>
method.Name == "TryCompilePixelShader" &&
method.GetParameters()[2].ParameterType.IsGenericType);
var tryCompileCompute = spirvTranslator.GetMethod( var tryCompileCompute = spirvTranslator.GetMethod(
"TryCompileComputeShader", "TryCompileComputeShader",
BindingFlags.Public | BindingFlags.Static) BindingFlags.Public | BindingFlags.Static)
@@ -230,6 +295,94 @@ foreach (var (name, expectTranslate, words) in testPrograms)
failures++; failures++;
Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})"); Console.WriteLine($"[{name}] compute emit: FAILED ({computeArgs[6]})");
} }
if (name.StartsWith("mrt", StringComparison.Ordinal))
{
(uint GuestSlot, uint HostLocation, string Kind)[] outputSpecs = name switch
{
"mrt" => new (uint GuestSlot, uint HostLocation, string Kind)[]
{
(0, 0, "Float"),
(3, 1, "Uint"),
(6, 2, "Sint"),
},
"mrt-float2" => [(0, 0, "Float"), (1, 1, "Float")],
"mrt8" => Enumerable.Range(0, 8)
.Select(index => ((uint)index, (uint)index, "Float"))
.ToArray(),
_ => [(0, 0, "Float")],
};
var pixelOutputs = Array.CreateInstance(pixelOutputBindingType, outputSpecs.Length);
for (var index = 0; index < outputSpecs.Length; index++)
{
var spec = outputSpecs[index];
pixelOutputs.SetValue(
Activator.CreateInstance(
pixelOutputBindingType,
spec.GuestSlot,
spec.HostLocation,
Enum.Parse(pixelOutputKindType, spec.Kind)),
index);
}
var pixelArgs = PadWithDefaults(
tryCompilePixel,
[state, evaluation, pixelOutputs, null, null]);
if ((bool)tryCompilePixel.Invoke(
null,
BindingFlags.OptionalParamBinding,
null,
pixelArgs,
null)!)
{
var shader = pixelArgs[3]!;
var spirv = (byte[])shader.GetType().GetProperty("Spirv")!.GetValue(shader)!;
var path = Path.Combine(outputDirectory, $"{name}-ps.spv");
File.WriteAllBytes(path, spirv);
Console.WriteLine($"[{name}] pixel emit: success, {spirv.Length} bytes -> {path}");
}
else
{
failures++;
Console.WriteLine($"[{name}] pixel emit: FAILED ({pixelArgs[4]})");
}
if (name == "mrt")
{
var invalidOutputs = Array.CreateInstance(pixelOutputBindingType, 2);
invalidOutputs.SetValue(
Activator.CreateInstance(
pixelOutputBindingType,
0u,
0u,
Enum.Parse(pixelOutputKindType, "Float")),
0);
invalidOutputs.SetValue(
Activator.CreateInstance(
pixelOutputBindingType,
3u,
7u,
Enum.Parse(pixelOutputKindType, "Float")),
1);
var invalidPixelArgs = PadWithDefaults(
tryCompilePixel,
[state, evaluation, invalidOutputs, null, null]);
if ((bool)tryCompilePixel.Invoke(
null,
BindingFlags.OptionalParamBinding,
null,
invalidPixelArgs,
null)!)
{
failures++;
Console.WriteLine("[mrt] FAILED: sparse host locations were accepted");
}
else
{
Console.WriteLine($"[mrt] sparse host locations rejected as expected ({invalidPixelArgs[4]})");
}
}
}
} }
Console.WriteLine(failures == 0 Console.WriteLine(failures == 0