Compare commits

...

59 Commits

Author SHA1 Message Date
Berk c4313acec3 Update contribution guidelines to enforce pull request template
Emphasize the importance of using the pull request template and outline consequences for non-compliance.
2026-07-17 13:52:07 +03:00
kuba 24b82a7f1c ci: rebuild the website when a release is published (#312)
* ci: rebuild the website when a release is published

* ci: add REUSE license header to notify-site workflow
2026-07-17 05:36:59 +02:00
Berk a9a4f511f0 Create pull request template for contributions
Added a pull request template to guide contributors in the SharpEmu Emulator Project.
2026-07-17 05:17:18 +03:00
Berk a44ce34a0a Add release process documentation to RELEASE-USE.md
Document the release process for SharpEmu, including preparation and publishing steps.
2026-07-17 05:00:22 +03:00
Berk 41ec56f813 chore: bump version to 0.0.2-beta.2 (#310) 2026-07-17 04:51:09 +03:00
Berk aa32d4bad0 added a release script to automate the release process (#309) 2026-07-17 04:48:46 +03:00
Hadi Abdulrahman faf3a397a8 [VideoOut] Fall back to 8-bit RGBA for unknown pixel formats instead of silently failing (#294)
Changes MapPixelFormatToGuestTextureFormat to default to format 56 (8-bit
RGBA) when the game uses a pixel format not yet in the known list, with a
stderr warning that reports the exact format value for project issue reports.

Previously, unknown formats returned 0, which caused RegisterKnownDisplayBuffer
to skip registration entirely. The GPU backend then couldn't find the buffer
during flip, producing vk.flip_capture_failed, and some games later hit a
Debug.Assert in ExecuteOrderedGuestFlipWait.

The fallback produces wrong colors for the affected games but lets them render
and display output, which is strictly better than a black screen or access
violation crash. The pixel format is printed to stderr so developers can
identify it and add proper support.

Co-authored-by: meowman <haadii2005@gamil.com>
2026-07-17 04:35:57 +03:00
samto6 488b285ecb [SaveData] Implement save data memory2 exports (#297)
* [SaveData] Implement save data memory2 exports

Astro Bot calls sceSaveDataSetupSaveDataMemory2 during boot and asserts
and null-writes when it fails, so the missing import surfaces as a
named crash. This implements setup plus the companion get, set, and
sync operations that make it useful. The store is one zero-filled file
per user and title at sce_sdmemory/memory.dat under the save root,
readiness is the backing file's existence, and get, set, and sync
return MEMORY_NOT_READY before setup. Struct offsets follow the
publicly documented homebrew savedata headers.

* [SaveData] Write setup result before mutating the memory backing file
2026-07-17 04:35:00 +03:00
samto6 1a4a2902d4 [HLE] Stub ContentExport, Font, and Pad calls Astro Bot needs to boot (#298)
Astro Bot asserts and null-writes when a subsystem init call fails, so
each missing import surfaces as a named crash. This stubs the blockers
observed during bring-up: content export init, eight font calls, and
pad tilt correction. sceFontGetHorizontalLayout writes the same
invented geometry as sceFontGetRenderCharGlyphMetrics and the rest
report success. Together with save data memory2 these take the title
to its splash image and font glyph rendering path.
2026-07-17 04:34:48 +03:00
kuba f544146d6d AGC: reduce inactive trace and draw allocations (#308) 2026-07-17 04:25:26 +03:00
kuba dbd74654c4 Vulkan: trim disabled draw diagnostics overhead (#307) 2026-07-17 04:18:06 +03:00
kuba 28485b60e2 CPU: avoid continuation emitter closure allocations (#306) 2026-07-17 04:17:58 +03:00
999sian 6b1abc1a38 perf(shader): cache pixel export masks (#288) 2026-07-17 04:17:39 +03:00
kostyaff 7494792249 [Tests] Add VirtualMemory and PhysicalVirtualMemory edge case tests (#266)
VirtualMemory: 4 new tests covering unmapped address returns false,
zero-length operations, page-boundary access, and cross-gap failures.

PhysicalVirtualMemory: 5 new tests covering lazy commit on demand,
reserve-only GetPointer, unmapped GetPointer returns null, free-list
first-fit reuse, and coalescing both neighbours on middle-range free.

Total: 9 new tests, 18 passed (8 VirtualMemory + 5 PhysicalVirtualMemory
+ 5 GuestMemoryAllocator). No production code changes.
2026-07-17 03:13:38 +03:00
jimmyjumbo 3585519007 [Audio] Correct float PCM endpoint conversion (#291) 2026-07-17 03:13:06 +03:00
Berk 8ee0fe8e44 Add files via upload 2026-07-17 03:03:05 +03:00
Tell-Shanks 0755ca15f7 core: report what occupies a fixed-address allocation on failure (#278)
* Implement DescribeAddressForDiagnostics method

Added a method to describe the state of a memory address for diagnostics.

* core: enrich allocation failure exception with host region diagnostic
2026-07-17 02:58:31 +03:00
jimmyjumbo 74c70cc2c1 [CI] Make versioned releases immutable (#289) 2026-07-17 02:54:35 +03:00
Berk 73e0ebf446 Update README order (#303) 2026-07-17 02:47:53 +03:00
Berk 459ae7e3f7 Docs/readme images (#302)
* [readme] update showcase images

* [readme] update readme images and change version to 0.0.2
2026-07-17 02:44:28 +03:00
kuba 8f405caebe Vulkan: reduce guest buffer upload allocations (#301) 2026-07-17 02:36:39 +03:00
kuba dabf723b3e [Vulkan] Honor guest depth clear state (#290) 2026-07-17 02:29:53 +03:00
AlexC 2129a12684 [GUI] Last commit SHA (id) displayed on about section (#279)
* Latest commit info axamal structure

* Link latest commit to text

* Added localization for About>Last commit info

* Made the commit hash a button that redirects you to the commit in github

* Reorder about section

* Commit and update icon in about section to have consistency in section
2026-07-17 02:25:01 +03:00
kuba 2db1fae282 [Tests/HLE] Cover APR resolve, stat, and streaming flow (#272) 2026-07-16 19:57:45 +03:00
kuba 33f96252da [CPU] Reject context transfers to unmapped guest addresses (#273) 2026-07-16 19:57:34 +03:00
kuba f7981a7ed7 [Tests/CPU] Verify import trampoline volatile-state ABI (#274) 2026-07-16 19:57:23 +03:00
kuba e10efa3ae1 [HLE] Make guest printf formatting locale-invariant (#271) 2026-07-16 18:39:48 +02:00
Peter Bonanni 16a2131b67 [PlayGo] Reject authoritative unknown chunk loci (#242) 2026-07-16 19:22:33 +03:00
Mees van den Kieboom 9883a9445d [AGC] Implement RDNA2 buffer/image/DS 32-bit atomic instructions (#222)
Adds decode and SPIR-V translation for the missing MUBUF, MIMG and DS
atomic instructions in the Gen5 shader translator, generalizing the
existing BufferAtomicAdd path. Covers swap, cmpswap, add, sub,
smin/smax, umin/umax, and/or/xor, inc and dec, plus the DS RTN
variants. Image atomics go through OpImageTexelPointer on the storage
image binding.

Notable: DS_CMPST operand order (DATA0 = comparator, DATA1 = new
value) is reversed relative to buffer/image cmpswap, which a dedicated
test locks in. ATOMIC_INC/DEC are approximated with
OpAtomicIIncrement/IDecrement, exact for the common 0xFFFFFFFF clamp.

Verified with 9 new synthetic decoder and end-to-end SPIR-V tests
(part of the #36 test corpus effort); full suite passes 35/35.

Signed-off-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
Co-authored-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
2026-07-16 17:28:39 +03:00
Peter Bonanni f2d9051358 Fix macOS fixed-address allocation collisions (#246) 2026-07-16 16:45:08 +03:00
Dafenx 53d414e096 Bound Vulkan host buffer pool memory (#201)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-16 16:44:29 +03:00
MikeyLITE69 52d2874fa8 cpu: emulate BMI1/BMI2/ABM instructions in software when the host lacks them (#249)
## What

The native backend runs guest code directly on the host CPU. When the host doesn't
implement a BMI1/BMI2/ABM instruction that the PS5's Zen 2 cores do, it raises #UD
(STATUS_ILLEGAL_INSTRUCTION). Today the vectored handler just logs the faulting bytes
and gives up, so the title dies.

This adds a software fallback. On an illegal-instruction fault we decode the opcode
with Iced (the decoder already used elsewhere in the backend), evaluate it against the
trapped register/memory state, write the result and flags back into the CONTEXT record,
step RIP past the instruction, and resume.

Instructions covered (32- and 64-bit): ANDN, BLSI, BLSMSK, BLSR, BEXTR, BZHI, TZCNT,
LZCNT, RORX, SARX, SHLX, SHRX, PDEP, PEXT.

## Why

Users on CPUs without these extensions currently can't get past code that uses them.
This is a generic fix (no game-specific hacks) that improves compatibility on older
hosts. MULX is intentionally left out for now — its dest_hi/dest_lo operand ordering
is easy to get subtly wrong, so I'd rather add it separately with its own tests.

## How it's structured

- `BmiInstructionEmulator` holds the pure bit/flag semantics with no dependency on the
  unsafe CONTEXT plumbing, so it can be unit-tested directly.
- `DirectExecutionBackend.IllegalInstruction.cs` is the thin unsafe adapter (decode →
  read operands → emulate → write back → advance RIP). Anything it doesn't fully model
  returns false and falls through to the existing diagnostics unchanged, so it can never
  mis-handle an opcode it doesn't recognize.
- One hook in `DirectExecutionBackend.Exceptions.cs`, next to the other TryRecover* calls.
- Emits a single one-time "emulating in software" log line, not per-instruction spam.

## How I verified

- Added xUnit tests covering every instruction in both widths plus the CF/ZF/SF/OF
  edge cases (src == 0, shift-count masking, index beyond operand width, etc.).
- Cross-checked all the expected values against an independent reference implementation
  written from the Intel/AMD definitions; results match.
- `dotnet build` + `dotnet test` pass locally.

## Notes

New files follow .editorconfig (4-space, SPDX headers, REUSE-compliant).
2026-07-16 16:38:27 +03:00
Gutemberg Ribeiro c5a82c1065 [Perf] Behavior-preserving hot-path allocation/LINQ wins (#264)
* [Perf] Gate event-flag tracing so it allocates nothing when disabled

TraceEventFlag built its interpolated argument (and, on the wait path,
FormatFrameChain + a new StringBuilder(256) in FormatGuestWaitObject plus
~12 guest-memory reads) on every call, then checked the env var inside the
method — so every sceKernelSetEventFlag/Clear/Poll/Wait paid a string
allocation and an Environment.GetEnvironmentVariable P/Invoke even with
tracing off. Cache the flag once in a static readonly bool and gate every
call site, matching the semaphore/event-queue pattern. Behavior is
unchanged when SHARPEMU_LOG_EVENT_FLAG=1.

* [Perf] Hoist IsNoBlockLeaf classification to import-stub setup

The leaf-dispatch path called IsNoBlockLeafImport(nid) — a ~30-literal
string pattern match — on every leaf import. IsLeaf/NidHash were already
precomputed on ImportStubEntry at stub setup; add IsNoBlockLeaf alongside
them and read the field in the hot path. Behavior unchanged.

* [Perf] Cache trace env-var flags read on hot paths

Two SHARPEMU_LOG_* env vars were read via Environment.GetEnvironmentVariable
(a P/Invoke + transient string) on hot paths: SHARPEMU_LOG_FIBER on every
fiber context transfer, and SHARPEMU_LOG_DIRECT_MEMORY on every direct-memory
op (~8 sites). Cache both once — _logFiber alongside the other backend _log*
flags, and _traceDirectMemory behind the existing ShouldTraceDirectMemory
helper. Behavior unchanged.

* [Perf] Avoid per-iteration thread snapshot in the idle pump loop

PumpUntilGuestThreadsIdle allocated a full GuestThreadState[] snapshot
(via LINQ Values.ToArray()) on every spin just to tally three run-state
booleans. Tally them under the lock with an allocation-free helper, and
only materialize the snapshot inside the gated (default-off) diagnostic
dump. SnapshotGuestThreads now uses Values.CopyTo instead of LINQ.
Behavior unchanged.

* [Perf] De-LINQ GetPixelColorExportMask on the per-draw path

GetPixelColorExportMask ran a Select/OfType/Where/Aggregate chain over all
shader instructions, allocating iterators + closures, and is called per
render target (twice per draw via CreateRenderState and HasPixelColorExport).
Replace with a manual scan producing the identical mask — no allocation, and
it removes an authored-LINQ use the repo bans.

* [Perf] De-LINQ per-draw render-target selection in AgcExports

The bound-render-target selection used Where(...).OrderBy(...).ToArray()
(plus a second Where/ToArray fallback) on every translated draw, allocating
LINQ iterators + closures. Replace with an explicit filter into a pre-sized
list + List.Sort by slot; slots are distinct so this matches the stable
OrderBy. Same result, no per-draw LINQ allocations.

* [Perf] De-LINQ per-draw render-target validation in the Vulkan presenter

SubmitOffscreenTranslatedDraw validated its render targets with
targets.Any(...) twice plus targets.Select(a=>a.Address).Distinct().Count()
(a per-draw HashSet), and broadcast a single blend with
Enumerable.Repeat(...).ToArray(). Targets are <= 8, so replace with manual
scans (invalid-target check; combined dimension-mismatch + pairwise
aliasing check) and Array.Fill. Same results, no per-draw LINQ allocations.

* [Perf] Binary-search VirtualQuery region lookup (SortedList)

_mappedRegions was an unordered Dictionary, so TryFindVirtualQueryRegionLocked
scanned every region for containment/next — O(n) per sceKernelVirtualQuery and
O(n^2) when an allocator walks the address space with the findNext flag. Store
regions in a SortedList keyed by base address (every write already uses the
region's own address as the key) and find the containing/next region with a
binary search over the sorted keys. Also drops a now-redundant Values.OrderBy.

Non-overlapping regions assumed (mmap semantics), so only the floor region can
contain the query. Behavior-preserving; worth spot-checking VirtualQuery-heavy
titles.

* [Perf] Remove stray CLI packages.lock.json committed by mistake

git add -A in an earlier commit swept in a regenerated
src/SharpEmu.CLI/packages.lock.json. main tracks no lock files (central
package management, no RestorePackagesWithLockFile), and REUSE.toml no
longer covers packages.lock.json, so the committed file failed the REUSE
Compliance check. Remove it.
2026-07-16 16:37:28 +03:00
can a8be1daca4 Fix sceNetHtonl/Htons/Ntohl/Ntohs discarding their converted result (#211)
Each of the four byte-order helpers computed the swap into Rax and then
returned via ctx.SetReturn(0). SetReturn writes its argument into Rax, so it
immediately overwrote the converted value with 0 and the guest saw every
sceNetHtonl / sceNetHtons / sceNetNtohl / sceNetNtohs call return 0.

Leave the swapped value in Rax and return ORBIS_GEN2_OK as the dispatch status
instead, matching how the value-returning exports in this file (e.g.
sceNetPoolCreate) already work.

Add a NetExports test suite covering the swaps, the 16-bit width masking, an
htonl/ntohl round-trip, and a regression guard that a non-zero input never
converts to 0.
2026-07-16 16:24:34 +03:00
999sian 26dbad8ac8 fix(input): map POSIX stick endpoints exactly (#244) 2026-07-16 16:19:33 +03:00
Dafenx 2196a1f786 Gate Vulkan texture upload traces (#220)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-16 16:19:11 +03:00
kostyaff 8db2c0622d [VideoOut] Implement sceVideoOutIsOutputSupported stub (fixes #257) (#270)
Quake (PPSA01880, Kex Engine) crashes at startup because
sceVideoOutIsOutputSupported (NID Nv8c-Kb+DUM) is unimplemented.
The game calls it to check video output capabilities before
initializing rendering.

Add HLE export stub: returns 1 (supported) for SceVideoOutBusTypeMain,
0 otherwise. The emulator renders via Vulkan and supports any pixel
format or aspect ratio on the main bus.

NID verified via Ps5Nid.Compute SHA1 algorithm.
Export name confirmed in scripts/ps5_names.txt.
2026-07-16 16:07:21 +03:00
mskomek 53da00fa89 GUI: add cross-platform release updater (#243) 2026-07-16 14:01:42 +03:00
Spooks b4b95014f1 Fix/deadcells crash (#262)
* Boot compatibility fixes for UE titles, GUI toggles, and DeS render/boot work

Checkpoint of the Monster Truck Championship and Demon's Souls boot work.
Each piece is independently useful and verified against the titles.

- playgo: scePlayGoGetLocus now returns BAD_CHUNK_ID for chunk ids outside
  the known set, matching real firmware. Titles enumerate chunk ids until
  that error; answering OK for every id made the scan wrap the ushort range
  and spin forever. Missing-sidecar and no-app0 fallbacks report a
  fully-installed single chunk 0 so scePlayGoOpen keeps succeeding.
- kernel: restore the SHARPEMU_WRITABLE_APP0 opt-in. Unpackaged UE dumps
  write their Saved tree under /app0 during PS5 component init and treat
  the denial as a fatal boot error.
- pad: accept handle 0 as the primary pad across all pad calls. Real
  firmware hands out small non-negative handles and some titles read state
  with handle 0.
- bthid: env-gated experiment hooks for the Thrustmaster wheel middleware
  investigation (fail-only-RegisterCallback modes and a synthetic
  enumeration callback with a zeroed event struct). All default off.
- gui: add SHARPEMU_LOG_IO and SHARPEMU_WRITABLE_APP0 toggles to the
  Environment tab.
- videoout: per-swapchain-image render-finished semaphores (the shared
  semaphore raced the swapchain); whole-mip-chain layout init for offscreen
  guest images (sampled binds read mips stuck in Undefined); GPU-resident
  texture availability now canonicalizes through the texture format table
  and accepts compatibility-class aliases, cutting per-frame CPU texture
  re-reads (143 GB -> 55 GB per 300 s in Demon's Souls, 0.2 -> 0.5 fps).
- hle: add sceSystemServiceGetNoticeScreenSkipFlag,
  sceSystemServiceGetMainAppTitleId (title id published from the runtime),
  and sceNpWebApi2CreateUserContext (refuses so the online layer backs off).
- rtc: SHARPEMU_RTC_PROBE_RANGE diagnostic dumps the code around a busy-wait
  caller of sceRtcGetCurrentTick once; costs nothing when unset.

* [ShaderCompiler] Fix VReadlaneB32 scalar destination field

The scalar destination lives in the low vdst byte (bits 0-7); it was read
from bits 8-14, the VOP3B carry-out field readlane does not have, sending
every readlane result to s0. Verified against raw gfx10 encodings and
LLVM's assembler tests (v_readlane_b32 s5, v1, s2 -> low byte 0x05).

* [VideoOut] Survive device loss and flip-order asserts without dying

Two ways a frame could take down the whole presenter:

- Device loss between any two Vulkan calls in a frame unwound the window
  thread, and the Dispose-time fence check then threw again, masking the
  original error. Catch the loss at the frame boundary, retire
  presentations and guest submissions whose fences can never signal, and
  keep the window loop pumping so the game (audio, logic) carries on.
- The ordered-flip capture invariant is violated ~100 times per run by
  Demon's Souls (PPSA01342); on debug builds the Debug.Assert fail-fasts
  the process with nothing in the log. Downgrade it to a once-per-version
  warning until the capture/wait ordering is understood.

* Fix Dead Cells shader cache regression

---------

Co-authored-by: StealUrKill <35749471+StealUrKill@users.noreply.github.com>
2026-07-16 13:57:16 +03:00
ParantezTech 7b91c964fc [VulkanVideoPresenter] revert back icon 2026-07-16 04:51:25 +03:00
Spooks 9bacb883f1 Fix Linux aligned mapping retention (#247)
* Fix Linux aligned mapping retention

* Cover Linux aligned mapping retention
2026-07-15 19:34:35 -06:00
Miguel Cruz 864cbb0fa0 [AGC/Vulkan] Extend PS5 runtime and rendering compatibility (#216)
* [Core] Add POSIX native execution and PS5 SELF support

Extend the native backend, guest TLS, fixed-address memory, and loader paths needed by PS5 titles on Windows, Linux, and macOS. Keep workstation GC so high-core-count hosts do not reserve over fixed guest image bases.

* [HLE] Expand PS5 service and media compatibility

Add the kernel, threading, save-data, networking, audio, video-codec, font, dialog, and service exports required by newer PS5 software. Preserve every SysAbi NID currently registered by main while adding the compatibility surface used by ASTRO BOT.

* [AGC/Vulkan] Extend Gen5 shader and presentation support

Expand PM4 handling, Gen5 shader translation, MRT and packed export support, guest image tracking, depth initialization, texture aliasing, and Vulkan presentation. Add the performance overlay and address-filtered diagnostics used to validate ASTRO BOT with original shaders.

* [Core] Align static TLS reservation across hosts

* [Pad] Align primary user ID with UserService

* [Gpu] Preserve runtime scalar buffers across renderer seam

* [AGC] Restore omitted command helper exports

* [Vulkan] Reuse primary views for promoted MRT targets

* [Vulkan] Preserve scratch storage bindings in compute dispatches
2026-07-16 02:02:34 +03:00
Berk ad5a7d3799 [CI] fix asset (#245) 2026-07-16 01:05:28 +03:00
en-he f69fdd4027 Fix PNG chunk CRC validation (#241) 2026-07-16 00:46:43 +03:00
Cloudy 9eef39549a hle: implement offline NP reachability state (#238) 2026-07-16 00:35:18 +03:00
Nicola Pomarico 90651f26a9 Fix macOS exact-address mmap falling back to MAP_FIXED (#214)
The PS5 image loader requires guest images to land at fixed virtual
addresses (e.g. the 32 GiB main image base). On macOS the exact-address
mmap path only ever passed that address as a hint, never MAP_FIXED, to
avoid clobbering untracked host mappings (dyld, JIT heap, Rosetta).

On Apple Silicon under Rosetta 2 the kernel does not reliably honor
that hint, so the allocation failed deterministically before a single
guest instruction ran (reported in #194). Retry with true MAP_FIXED
as a fallback only when the hint-only attempt doesn't land at the
requested address, so the safer hint path is still tried first.
2026-07-16 00:28:39 +03:00
kostyaff b5930465f2 [AGC] Correct VReadlaneB32 decode and emission (#237)
VReadlaneB32 (VOP3 0x360) had two bugs:

1. Decode: VOP3 decode always set destinations = Vector(word & 0xFF).
   For VReadlaneB32, bits 0-7 are unused — the scalar destination is
   in bits 8-14. Now decodes as Scalar((word >> 8) & 0x7F).

2. Emission: VReadlaneB32 was in the VMovB32 fall-through group,
   just returning GetRawSource(instruction, 0) — reading the current
   lane's value. By ISA, sdst = vsrc0[lane(src1)], which requires
   reading a different lane's value. Now uses SPIR-V
   GroupNonUniformBroadcast(scope=Subgroup, value=src0, lane=src1)
   when subgroup operations are available, with a fallback to the
   current-lane simplification when not.

3. Routing: TryEmitVectorAlu called TryGetVectorDestination first,
   which checks for VectorRegister kind. With the scalar destination
   fix, VReadlaneB32 now routes to a new TryEmitReadlane handler
   before the vector destination check.

4. Subgroup capability: UsesSubgroupShuffle now includes VReadlaneB32
   so GroupNonUniform capability is enabled when needed.

Verified: dotnet build 0 errors/0 warnings, 26/26 tests pass,
ShaderDump all programs behaved as expected.
2026-07-16 00:27:26 +03:00
brbrhuehue-matrix 83303e953e Update Brazilian Portuguese translation (#233) 2026-07-16 00:26:08 +03:00
kostyaff 92497689ab [AGC] Fix VOP3 decode for V_READLANE_B32 and V_WRITELANE_B32 (#232)
PR #200 moved shader files from SharpEmu.Libs/Agc/ to new projects
SharpEmu.ShaderCompiler and SharpEmu.ShaderCompiler.Vulkan. This
re-ports the VOP3 decode fix from PR #226 to the new file paths.

Decode table (Gen5ShaderTranslator.cs):
- 0x360: VMadU32U16 -> VReadlaneB32 (per RDNA2 ISA)
- 0x361: VMulLoU32 -> VWritelaneB32 (0x361 was a duplicate of 0x169)
- 0x373: added VMadU32U16 at its correct opcode

Emission (Gen5SpirvTranslator.Alu.cs):
- VWritelaneB32: per-lane conditional write via IEqual+Select,
  stores with guardWithExec:false (writelane bypasses exec mask)
- VReadlaneB32: kept as GetRawSource(instruction, 0) simplification
  (correct emission with src1 lane select is a follow-up)

Verified: dotnet build 0 errors/0 warnings, 26/26 tests pass,
ShaderDump all programs behaved as expected.
2026-07-16 00:25:59 +03:00
Gutemberg Ribeiro de7973af55 [CI] Fix PR artifact-links comment for fork PRs (#227)
The workflow resolved the PR from workflow_run.pull_requests or the head
commit, both of which come back empty for PRs from forks — so it logged
"No open PR for this build" and never commented. Resolve the PR by its
head "owner:branch" (from workflow_run.head_repository/head_branch),
which works for forks, keeping the commit-association path as a fallback.
2026-07-16 00:25:21 +03:00
s1099 3da7ae7d70 Update repo URL (#224) 2026-07-16 00:24:14 +03:00
Jack Del Aguila 1be009ce40 [HLE] Fix POSIX condition variable semantics (#113) (#223)
* [HLE] Trigger AGC graphics events by filter instead of exact ident (#173)

The PM4 EVENT_WRITE packet carries a 6-bit hardware EVENT_TYPE, but the
guest registers AGC events via sceAgcDriverAddEqEvent with a full guest
eventId. These two values are not the same numbering scheme, so the exact
ident lookup in TriggerRegisteredEvents never matched and the AGC
interrupt thread hung forever.

Add TriggerRegisteredEventsByFilter, which wakes every graphics event
registration on every queue. This is a compatibility workaround for
issue #173 while the real PS5 mapping remains unknown.

Includes unit tests covering the mismatched ident/eventType case.

* [HLE] Fix POSIX condition variable semantics (#113)

Remove PendingSignals from PthreadCondState. POSIX condition signals are edges,
not semaphore credits - a signal with no waiter must have no effect. The previous
implementation persisted signals, causing lock inversions and predicate bypasses.

Changes:
- Remove PendingSignals property and TryConsumePendingSignal method
- Remove pending signal consumption logic from PthreadCondWaitCore
- Remove PendingSignals increment from PthreadCondSignalCore
- Add regression tests verifying POSIX-correct behavior

Fixes #113
2026-07-16 00:24:05 +03:00
999sian ad92ab30fd fix(gui): respect case-sensitive Linux paths (#218) 2026-07-16 00:17:31 +03:00
can 5f97d2edc4 Fix sceKernelGetTscFrequency disagreeing with the counter on non-Windows hosts (#213)
sceKernelReadTsc only returns the CPU's RDTSC when the host RDTSC reader is
available (currently 64-bit Windows); on Linux and macOS it falls back to the
QPC-based Stopwatch. ResolveKernelTscFrequency, however, still consulted the
CPUID-reported hardware TSC frequency in that case, so sceKernelGetTscFrequency
reported a multi-GHz rate while ReadTsc was ticking at the Stopwatch frequency.
A guest computing elapsed = readTscDelta / frequency then gets the wrong time
on those platforms.

Gate the calibrated/CPUID frequencies on RDTSC actually being available (the
calibration path was already self-gated; the CPUID path was not) and otherwise
report the Stopwatch frequency, keeping ReadTsc and GetTscFrequency consistent.

The selection logic is extracted into a pure, host-independent helper so both
branches can be unit tested, including a regression test asserting that a host
without RDTSC reports the Stopwatch frequency rather than the hardware TSC.
2026-07-16 00:11:46 +03:00
Eyoatam Wubeshet 5c8ace82c0 Add missing French translations for Environment and About sections (#206)
Fills in the untranslated keys under Options.Env.* and About.* so the
French locale matches en.json. Tried to keep the wording consistent with
the rest of the file.

Co-authored-by: Rick21-bit <Rick21-bit@users.noreply.github.com>
2026-07-16 00:09:16 +03:00
can 341c2a0cb6 Fix sceRtcConvertLocalTimeToUtc failing on non-UTC hosts (#210)
The guest local tick was decoded into a DateTimeKind.Utc DateTime and then
passed to TimeZoneInfo.ConvertTimeToUtc together with TimeZoneInfo.Local.
That overload throws ArgumentException when a Utc-kind value is paired with a
source zone other than UTC, so on any machine whose local zone is not UTC the
export caught the exception and always returned INVALID_ARGUMENT.

Re-tag the decoded value as DateTimeKind.Unspecified so it is interpreted as
local wall-clock time and converted correctly. The reverse direction
(sceRtcConvertUtcToLocalTime) was already correct because ConvertTimeFromUtc
accepts a Utc-kind input.

Add a RtcExports unit-test suite covering the tick/calendar conversions, DOS
time packing, Win32 file time, leap-year and validation error codes, and a
regression test that round-trips a UTC tick through local time and back.
2026-07-16 00:04:35 +03:00
Gutemberg Ribeiro 320dbcacba [SourceGenerators] Compile-time SysAbi export registry, analyzers, and build-generated aerolib.bin (#204)
* [SourceGenerators] Add the SysAbi export generator and analyzers (phase 0)

New SharpEmu.SourceGenerators Roslyn component, complete and tested but
consumed by nothing yet — the emulator projects adopt it in the
following commits.

Ps5Nid ports the PS NID derivation (base64 of the byte-reversed first
eight SHA1 bytes of name + fixed suffix) from
scripts/generate_aerolib_binary.py to C#, so what has always been a
manual, out-of-band computation becomes a compile-time capability.

SysAbiExportGenerator emits a per-assembly SysAbiExportRegistry whose
CreateExports(Generation) reproduces ModuleManager's reflection scan
exactly — same generation inheritance and filtering, same method-name
fallback, same libKernel default — with attribute-omitted NIDs derived
algorithmically (equivalent to the runtime catalog lookup, which is
built from the same computation). Parameterless handlers are adapted to
the SysAbiFunction shape; invalid declarations are skipped here because
the analyzer rejects them as build errors, so nothing drops silently.

SysAbiExportAnalyzer turns the runtime failure modes into diagnostics:
SHEM001 duplicate NID (across declared and derived forms), SHEM002
malformed NID, SHEM003 uncallable handler signature, SHEM004 NID
contradicting its export name (the class of drift previously fixed by
hand), SHEM005 unresolvable export, SHEM006 export name unknown to
ps5_names.txt when the catalog is wired as an AdditionalFile, SHEM007
handler not reachable by generated code.

The self-contained test suite drives both in-process against the real
SharpEmu.HLE metadata: known catalog NID pairs pin the algorithm, the
generated registry must itself compile, and each diagnostic has a
triggering fixture. Fittingly, the NID pinning test caught a wrong
pair in its own first draft — the exact mistake SHEM004 exists to stop.

* [SourceGenerators] Adopt the generated export registry in the emulator (phase 1)

SharpEmu.Libs consumes the generator and analyzers, with
scripts/ps5_names.txt wired as the AdditionalFile catalog. The runtime
now registers exports from the compile-time SysAbiExportRegistry
instead of the boot-time reflection scan; RegisterFromAssembly is
retained solely as the arbiter for a parity test that pins the two
tables identical — same NIDs, names, libraries, targets, and handler
methods — across Gen4, Gen5, and combined registration.

First contact between the analyzer and all 715 existing exports
surfaced real drift the old offline checker structurally missed
(scripts/check_sysabi_aerolib.py skipped any NID absent from
aerolib.bin): three exports whose friendly names collide with real
catalog symbols of different NIDs, now suppressed at-site with reasons
pending AGC API confirmation, alongside the established synthetic
Unknown* labels for uncatalogued NIDs, which prompted a rule
refinement — SHEM004 only hard-errors when the export name is a real
catalog symbol, since synthetic labels cannot be validated by hashing
and the NID is authoritative for them. The two allowlisted mismatches
in the python checker no longer trigger anything, and the checker is
deleted: the analyzer subsumes it with the semantic model instead of
regex, and validates every declared pair rather than only
catalog-known NIDs.

* [SourceGenerators] Generate aerolib.bin at build time from ps5_names.txt

The runtime NID -> name catalog is derived data and no longer lives in
the repository: a Framework-only MSBuild task (GenerateAerolibBinaryTask,
sharing the same Ps5Nid implementation the analyzers use) builds it
into the intermediate directory from scripts/ps5_names.txt — now the
single source of truth — and SharpEmu.HLE embeds it from there. The
output is byte-identical to the previously committed binary, verified
with cmp against git history; a new test pins that the embedded catalog
loads and resolves a known symbol both directions.

scripts/generate_aerolib_binary.py is deleted (its algorithm lives in
Ps5Nid, its invocation in the build); the REUSE annotation for the
binary goes with it. MSBuild's Inputs/Outputs check means the ~154k NID
hashes only recompute when the names file actually changes. The task
implements ITask against Microsoft.Build.Framework directly, keeping
the vulnerable-flagged Utilities.Core package out and the analyzer
project's file-IO ban suppressed only inside the task itself.

* [SourceGenerators] Emit typed-signature register thunks (phase 2)

[SysAbiExport] handlers can now be written with real signatures — a
CpuContext followed by up to six int/uint/long/ulong parameters — and
the generator emits the SysV unmarshalling thunk, mapping parameters
positionally to RDI/RSI/RDX/RCX/R8/R9 with the same unchecked-cast
idiom hand-written handlers use. SHEM003 accepts the new shape and
rejects register overflow and non-register-representable types. Both
shapes coexist, so migration is per-handler; sceKernelPollSema,
sceKernelSignalSema, and sceKernelCancelSema migrate as the
demonstration (the last showing raw ulong guest-address passthrough).

The reflection scan cannot represent typed handlers, so it retires
here: RegisterFromAssembly, its signature validation, and
ResolveExportInfo are deleted, and the parity test that pinned the
generated registry to the scan is replaced by content-invariant tests
(duplicate-free, full 715-export surface, catalog identity). Deleting
the scan surfaced a phase-1 latent regression — the pre-JIT warm sweep
enumerated only reflection-scanned assemblies, so the generated
registration path warmed nothing and re-exposed the guest-thread
fail-fast risk; the warm set is now derived from the registered
handler delegates themselves.

* [SourceGenerators] Marshal guest strings declaratively with [GuestCString] (phase 3)

A string parameter on a typed [SysAbiExport] handler, annotated
[GuestCString(maxLength)], now makes the generated thunk read the
null-terminated UTF-8 string from the argument register's guest
address before the handler runs, returning
ORBIS_GEN2_ERROR_MEMORY_FAULT to the guest when the read fails —
the exact prologue nearly every string-taking handler writes by hand.
The attribute lives in SharpEmu.HLE next to SysAbiExportAttribute;
SHEM008 rejects misuse (non-string parameter, non-positive MaxLength)
while a bare string parameter stays a SHEM003 signature error.

_open, open, and sceKernelOpen migrate as the demonstration; they were
chosen because their hand-written prologue faulted on a null pointer
the same way the thunk does (handlers that return INVALID_ARGUMENT for
null pointers, like sceKernelCreateSema, keep the raw shape so guest-
visible semantics stay untouched).

* [SourceGenerators] Apply review findings across the branch

Behavior: the open/_open/sceKernelOpen [GuestCString] demo migration is
reverted — the local compat reader falls back to host memory for paths
in loader-mapped regions that ctx.Memory cannot see, so the generated
thunk would have turned recoverable reads into MEMORY_FAULT. The
marshalling infrastructure stays, proven by generator/analyzer tests;
production migration waits for a handler whose semantics the thunk
reproduces exactly. A comment on the handler records why.

Build robustness: the aerolib target is skipped for design-time builds
(the IDE resolves project references without compiling them, so on a
fresh clone the task assembly does not exist yet), and the task/names
paths are centralized in properties. The generator now emits no
registry for export-free assemblies, so referencing the analyzer can
never mint a colliding SharpEmu.Generated type.

Cleanup and perf: the pragma-suppression sites left mis-indented by the
phase-1 relocation are reformatted and the restores moved after the
method body; the dead ExportsForTesting hook and its InternalsVisibleTo
are deleted; the aerolib task reuses one SHA1 instance across ~150k
names; the analyzer caches the parsed catalog per file snapshot instead
of re-parsing 150k lines every compilation start, shares the attribute
name constant with the generator, and computes the catalog-membership
check once.

* [CI] Run the test suites in the build workflow

The workflow compiled the test projects (they are in SharpEmu.slnx) but
never executed them. A solution-level dotnet test now runs between
build and publish, so any test failure fails the build — including the
AerolibCatalogTests/SysAbiRegistryTests that guard the build-generated
aerolib.bin and the generated export registry. Generation failures of
aerolib.bin itself already fail the build step: the MSBuild task logs
an error event and returns false, and a missing task assembly or
missing embedded output are hard MSBuild errors. The NuGet cache key
now also tracks the test projects' lock files.

* [SourceGenerators] Address review feedback

Multi-diagnostic analyzer tests no longer assume a stable diagnostic
order (analyzer execution is concurrent), and the aerolib task logs
the full exception instead of only its message so build failures keep
the type and stack trace.

* [SourceGenerators] Address second review round

Symbol-name comparisons in the shape rules and analyzer now pin an
explicit SymbolDisplayFormat.FullyQualifiedFormat instead of relying on
the display-format default, and the aerolib task fails loudly on a
symbol name that would overflow the format's ushort length prefix
instead of silently truncating it, with null-safe output-directory
handling made explicit.

* [SourceGenerators] Embed aerolib.bin via a target so design-time builds never reference it

The static EmbeddedResource item referenced the generated file even in
design-time builds, where the generation target is skipped — on a fresh
clone the IDE would try to embed a file that never existed. The item is
now created inside an EmbedAerolibBinary target gated on
DesignTimeBuild, separate from the generation target so an up-to-date
skip of GenerateAerolibBinary cannot drop the item with the rest of its
body, and hooked before AssignTargetPaths since dynamic resource items
added later miss the resource pipeline.

Verified fresh build, incremental rebuild (embedded catalog test both
times), and a simulated design-time compile with no artifacts present.

* [SourceGenerators] Regenerate test lock file after rebase onto main

Rebase fallout: main's package graph shifted under #200, so the
SourceGenerators.Tests lock file is re-evaluated to keep --locked-mode
restore green at the branch tip.

* [Build] Drop NuGet lock files; rely on central package management

Central package management was already in effect (ManagePackageVersionsCentrally
with all versions in Directory.Packages.props and no inline PackageReference
versions), so the per-project packages.lock.json files and the lock-mode
workflow only added maintenance overhead. This removes all eleven lock files,
drops RestorePackagesWithLockFile so restore no longer regenerates them, and
takes --locked-mode off the CI restore steps (re-keying the NuGet cache on the
central props files). Package versions remain centrally pinned in
Directory.Packages.props.
2026-07-16 00:00:32 +03:00
Gutemberg Ribeiro 30fdd8d6ed [Gpu] Backend-neutral shader compiler and guest-GPU renderer seam (#200)
* [ShaderCompiler] Extract the backend-neutral shader compiler project

Move the Gen5 (gfx10) microcode decoder, the scalar evaluator, the
shader IR, and the metadata reader out of SharpEmu.Libs/Agc into a new
SharpEmu.ShaderCompiler project — the half of shader compilation every
codegen backend (SPIR-V today; MSL and DXIL later) consumes. Types go
public: they are the contract now. Nothing in the project may depend on
a host graphics API; the SPIR-V-specific artifact types
(Gen5SpirvShader, Gen5SpirvStage) stay beside the emitter in Libs.

Three couplings surfaced by the move, each resolved at the right depth:
GuestDrawKind was defined inside VulkanVideoPresenter despite being a
guest-domain, decoder-produced concept — it moves to the shared project;
the evaluator's one HLE dependency (the tracked-libc-heap read
fallback) becomes an injectable hook that a Libs module initializer
installs before any caller can reach the evaluator; and the inline-
constant table is promoted to a shared Gen5InlineConstants so backends
cannot drift on constant semantics (the SPIR-V translator now delegates
to it).

The ShaderDump tool drops its reflection over the moved types in favor
of direct typed calls; only the SPIR-V emitter, still internal to Libs
until it moves to its own backend project, is reached via reflection.
Verified by a clean solution build, the existing test suite, and a full
ShaderDump conformance run.

* [ShaderCompiler] Move the SPIR-V emitter into SharpEmu.ShaderCompiler.Vulkan

Gen5SpirvTranslator (with its ALU partial), SpirvModuleBuilder,
SpirvFixedShaders, and the Gen5SpirvShader/Gen5SpirvStage artifact types
move whole from SharpEmu.Libs/Agc into the first per-backend codegen
project. Notably it needs no Vulkan bindings reference: emitters
produce bytes from the shared IR; renderers own graphics APIs. Types go
public as the backend's contract; AgcExports and the presenter consume
them exactly as before.

The ShaderDump tool drops its last reflection: with both halves of the
pipeline public it drives decode and all three emit entry points with
direct typed calls, retiring the PadWithDefaults invoke shim — and it
no longer references SharpEmu.Libs at all, making the conformance tool
emulator-independent by design. Verified by a clean solution build, the
test suite, a full ShaderDump conformance run, and a locked-mode
restore under the pinned SDK.

* [Gpu] Extract the guest-GPU backend seam (IGuestGpuBackend)

The AGC/VideoOut/SystemService export layers now reach the renderer
through IGuestGpuBackend via GuestGpu.Current (mirroring HostPlatform),
instead of calling VulkanVideoPresenter statics. The Vulkan backend is
a thin adapter over the existing presenter, so the extraction stays
mechanical; only the adapter and the presenter itself reference the
presenter now.

The types crossing the seam move to Gpu/GuestGpuTypes.cs and drop their
Vulkan prefixes, which an audit showed were misnomers: every field is a
neutral primitive or a raw guest value (guest addresses, format and
number-type codes, CB_BLEND register bitfields, verbatim sampler
descriptor dwords). The one genuine Vulkan value in the old surface —
the Silk.NET Format inside VulkanRenderTargetFormat, which callers
never read — stops crossing: TryDecodeRenderTargetFormat is replaced at
the seam by TryGetRenderTargetOutputKind, which surfaces only the
Gen5PixelOutputKind callers actually consume, keeping native formats a
backend-internal concern. ToVulkanSampler in AgcExports is renamed
ToGuestSampler to match what it always produced.

Seam rules are documented on the interface: no host-API value crosses,
and submission stays coarse-grained with synchronization internal to
backends. Interim exception, resolved next: shader parameters are still
SPIR-V blobs.

* [Gpu] Move shader compilation behind the guest-GPU backend

The seam's interim exception is gone: AgcExports no longer calls
Gen5SpirvTranslator or handles SPIR-V bytes. IGuestGpuBackend gains the
three TryCompile entry points, which take the backend-neutral
(Gen5ShaderState, Gen5ShaderEvaluation) contract plus the flat
per-role resource-slot bases a multi-stage draw needs, and return
opaque IGuestCompiledShader handles that only the producing backend can
submit — the Vulkan backend wraps its SPIR-V in
VulkanCompiledGuestShader and rejects foreign handles loudly. Draw and
dispatch submissions take handles instead of byte arrays; the shader
caches in AgcExports store handles.

IGuestCompiledShader.Payload exposes the backend-defined compiled bytes
for exactly two callers: the diagnostics dump and the size trace —
documented as never-interpret. The unused _pixelSpirvCache is deleted.
With this, a Metal or DX12 backend plugs in by implementing
IGuestGpuBackend with its own codegen; nothing in the export layers
knows which shader format exists.

Verified by a clean solution build, the test suite, and a full
ShaderDump conformance run under the pinned SDK.

* [Gpu] Fix rename collateral from the seam extraction

Address review findings: a doc comment picked up the mechanical
VulkanVideoPresenter -> GuestGpu.Current rewrite and ended up naming
members that do not exist on the interface, and CreateVulkanIndexBuffer
kept its Vulkan prefix while every sibling factory was de-Vulkanized —
it produces the neutral GuestIndexBuffer, so it is CreateGuestIndexBuffer.

* [Gpu] Label diagnostics dumps with the backend's payload extension

Address the review's altitude finding on DumpSpirv: the dump helper's
IR-disassembly half is backend-neutral and stays put, but writing the
opaque payload to a hardcoded .spv interpreted bytes the seam says
never to interpret. IGuestCompiledShader now declares its payload's
file extension, and the renamed DumpCompiledShader takes the handle and
writes honestly-labeled dumps whichever backend produced them.

* [Gpu] Make the shader-cache hit path allocation-free and lock-free

Every translated draw built its cache key with a LINQ Select feeding
string.Join plus one interpolated string per render target — steady
per-draw allocation whether or not the shaders were already cached. The
output layout is now packed exactly into a ulong (guest slot in 6 bits
+ output kind in 2 bits per target, host locations being the byte
positions, target count in the key beside it), and the
Gen5PixelOutputBinding array is only materialized on a cache miss,
where compilation dwarfs it.

The graphics/compute shader caches switch from Dictionary guarded by
_submitTraceGate to ConcurrentDictionary, making the per-draw and
per-dispatch hit paths lock-free and decoupling them from the tracing
gate they coincidentally shared. And the seam-shaped render-target list
is built once when a translated draw is created instead of a
Select/ToArray per submission of a cached draw.

* [Gpu] Replace LINQ with explicit loops in code this branch introduced

Project rule going forward: no LINQ — it allocates enumerators,
closures, and delegates, and this codebase is GC-pause-sensitive. The
pixel-output and guest-render-target array builds and the ShaderDump
store-PC collection become plain loops; pre-existing LINQ elsewhere is
left for changes that already touch those lines.

* [ShaderCompiler] Suppress CA2255 on the evaluator hook installer

The analyzer coverage that arrived with the rebase flags
ModuleInitializer in library code; this is the rule's intended advanced
scenario — the hook must be installed before any code path can reach
the evaluator, and every such path enters through this assembly — so
suppress with that justification rather than weaken the guarantee to a
static constructor's lazier timing.

* [Gpu] Resolve rebase artifacts onto main

Dedupe the System.Collections.Concurrent using in AgcExports that the
rebase merge duplicated (main and this branch each added it), and
regenerate the lock files for the new shader-compiler projects and
SharpEmu.Libs against main's current package graph so --locked-mode
restore matches at the branch tip.

* [CI] Comment per-platform build artifact links on PRs

Adds a workflow_run workflow that, after "Build and Release" finishes a
pull-request build, posts (and keeps updated in place) a single PR
comment linking the Windows, Linux, and macOS artifacts from that run.

It runs via workflow_run rather than in the build workflow because PRs
from forks build with a read-only token that cannot comment; the
follow-on run executes in the base-repo context with write access and
without checking out fork code. GitHub only triggers workflow_run from
the default branch, so this takes effect once merged to main.
2026-07-15 11:11:24 -06:00
Berk c69ac6ddab [CI] Update for cross-platform support (#209)
* [CI] Update for cross-platform support

* [CI] fix check
2026-07-15 16:07:36 +03:00
194 changed files with 48746 additions and 14237 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+20
View File
@@ -0,0 +1,20 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
## Before submitting
Please read our contribution guidelines before opening a pull request:
➡️ [**CONTRIBUTING.md**](CONTRIBUTING.md)
By opening this pull request, you confirm that you have read and agree to follow the contribution guidelines.
## Checklist
By submitting this PR, you confirm that:
- [ ] I have read `CONTRIBUTING.md`.
- [ ] I tested my changes.
- [ ] I wrote this description myself and did not copy AI-generated explanations.
- [ ] I listed the game(s) I tested, if applicable.
+31
View File
@@ -0,0 +1,31 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Tells the website repo to rebuild when a release is published, so
# sharpemu.app/downloads lists the new build within a minute.
name: Notify website
on:
release:
types: [published, released, edited, deleted]
workflow_dispatch:
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Trigger sharpemu-site rebuild
env:
TOKEN: ${{ secrets.SITE_DISPATCH_TOKEN }}
run: |
if [ -z "$TOKEN" ]; then
echo "SITE_DISPATCH_TOKEN is not set — skipping website rebuild."
exit 0
fi
curl -fsS -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/sharpemu/sharpemu-site/dispatches \
-d '{"event_type":"release-published"}'
echo "Website rebuild requested."
+120
View File
@@ -0,0 +1,120 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Posts (and keeps updated) a single PR comment linking the per-platform build
# artifacts once "Build and Release" finishes.
#
# This is a workflow_run workflow on purpose: PRs from forks run "Build and
# Release" with a read-only GITHUB_TOKEN and cannot comment. workflow_run runs
# afterwards in the base-repo context with a read-write token and does not check
# out untrusted fork code, so it can comment safely. Because of that, GitHub only
# triggers it from the copy on the default branch — it does nothing until merged
# to main.
name: PR Build Links
on:
workflow_run:
workflows: ["Build and Release"]
types:
- completed
permissions:
contents: read
actions: read
pull-requests: write
jobs:
comment:
name: Post artifact links
runs-on: ubuntu-latest
# Only for successful PR builds — artifacts exist only when the build passed.
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- name: Post or update the artifact-links comment
uses: actions/github-script@v7
with:
script: |
const run = context.payload.workflow_run;
const { owner, repo } = context.repo;
// Resolve the PR. Same-repo PRs are in workflow_run.pull_requests, but
// fork PRs leave it empty AND their head commit is not on a base-repo
// branch, so listPullRequestsAssociatedWithCommit misses them too. Match
// the open PR by its head "owner:branch" instead, which works for forks.
let prNumber;
if (run.pull_requests && run.pull_requests.length > 0) {
prNumber = run.pull_requests[0].number;
} else {
let candidates = [];
const headOwner = run.head_repository && run.head_repository.owner
? run.head_repository.owner.login
: null;
if (headOwner && run.head_branch) {
const byHead = await github.rest.pulls.list({
owner, repo, state: 'open',
head: `${headOwner}:${run.head_branch}`, per_page: 10,
});
candidates = byHead.data;
}
if (candidates.length === 0) {
const byCommit = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner, repo, commit_sha: run.head_sha,
});
candidates = byCommit.data.filter(pr => pr.state === 'open');
}
if (candidates.length === 0) {
core.info('No open PR for this build; nothing to comment.');
return;
}
prNumber = candidates[0].number;
}
// Collect the per-platform artifacts the build produced.
const artifacts = await github.paginate(
github.rest.actions.listWorkflowRunArtifacts,
{ owner, repo, run_id: run.id, per_page: 100 },
);
const platforms = [
{ key: 'win-x64', label: 'Windows (win-x64)' },
{ key: 'linux-x64', label: 'Linux (linux-x64)' },
{ key: 'osx-x64', label: 'macOS (osx-x64)' },
];
const rows = [];
for (const platform of platforms) {
const artifact = artifacts.find(a => a.name.includes(platform.key));
if (!artifact) {
continue;
}
const url = `https://github.com/${owner}/${repo}/actions/runs/${run.id}/artifacts/${artifact.id}`;
rows.push(`| ${platform.label} | [\`${artifact.name}\`](${url}) |`);
}
if (rows.length === 0) {
core.info('No platform artifacts on this run; nothing to comment.');
return;
}
const marker = '<!-- pr-build-links -->';
const body = [
marker,
`### 📦 Build artifacts — \`${run.head_sha.substring(0, 7)}\``,
'',
'| Platform | Download |',
'| --- | --- |',
...rows,
'',
`From [build run #${run.run_number}](${run.html_url}). ` +
'Downloads require a GitHub login and expire after 90 days.',
].join('\n');
// Upsert one comment so repeated builds refresh it in place.
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: prNumber, per_page: 100,
});
const existing = comments.find(c => c.body && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
}
+55 -72
View File
@@ -7,6 +7,8 @@ on:
push:
branches:
- "**"
tags:
- "v*"
paths-ignore:
- "**/*.md"
- "**/*.png"
@@ -34,31 +36,36 @@ jobs:
name: Init
runs-on: ubuntu-latest
outputs:
archive-name: ${{ steps.vars.outputs.archive-name }}
artifact-name: ${{ steps.vars.outputs.artifact-name }}
release-name: ${{ steps.vars.outputs.release-name }}
release-tag: ${{ steps.vars.outputs.release-tag }}
safe-ref: ${{ steps.vars.outputs.safe-ref }}
short-sha: ${{ steps.vars.outputs.short-sha }}
version: ${{ steps.vars.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Compute workflow variables
id: vars
shell: bash
run: |
short_sha="${GITHUB_SHA::7}"
safe_ref="$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed 's#[^a-z0-9._-]#-#g')"
archive_name="sharpemu-win64-${short_sha}.zip"
artifact_name="sharpemu-win64-${short_sha}"
release_tag="win64-${safe_ref}-${short_sha}"
release_name="SharpEmu win64 ${short_sha}"
version="$(python3 -c 'import sys, xml.etree.ElementTree as ET; version = ET.parse("Directory.Build.props").getroot().findtext(".//SharpEmuVersion"); sys.exit("Directory.Build.props is missing SharpEmuVersion") if not version or not version.strip() else print(version.strip())')"
release_tag="v${version}"
release_name="SharpEmu v${version}"
if [ "${GITHUB_REF_TYPE}" = "tag" ] && [ "${GITHUB_REF_NAME}" != "${release_tag}" ]; then
echo "Release tag ${GITHUB_REF_NAME} does not match project version ${release_tag}." >&2
exit 1
fi
{
echo "short-sha=${short_sha}"
echo "safe-ref=${safe_ref}"
echo "archive-name=${archive_name}"
echo "artifact-name=${artifact_name}"
echo "release-tag=${release_tag}"
echo "release-name=${release_name}"
echo "version=${version}"
} >> "$GITHUB_OUTPUT"
reuse:
@@ -94,14 +101,20 @@ jobs:
cache: true
cache-dependency-path: |
Directory.Packages.props
src/**/packages.lock.json
Directory.Build.props
- name: Restore solution
run: dotnet restore SharpEmu.slnx --locked-mode
run: dotnet restore SharpEmu.slnx
- name: Build solution
run: dotnet build SharpEmu.slnx -c Release --no-restore
# Runs every test project in the solution; a test failure fails the build, as
# does an aerolib.bin generation failure in the build step above (the MSBuild
# task logs an error and returns false).
- name: Run tests
run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal
- name: Validate synthetic shaders
run: dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
@@ -112,7 +125,8 @@ jobs:
run: |
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
$archivePath = Join-Path $env:RELEASE_DIR "${{ needs.init.outputs.archive-name }}"
$archiveName = "sharpemu-${{ needs.init.outputs.version }}-win-x64.zip"
$archivePath = Join-Path $env:RELEASE_DIR $archiveName
if (Test-Path $archivePath) {
Remove-Item $archivePath -Force
}
@@ -122,8 +136,8 @@ jobs:
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: ${{ needs.init.outputs.artifact-name }}
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
name: sharpemu-win-x64-${{ needs.init.outputs.short-sha }}
path: ${{ env.RELEASE_DIR }}\sharpemu-${{ needs.init.outputs.version }}-win-x64.zip
if-no-files-found: error
build-posix:
@@ -156,14 +170,19 @@ jobs:
cache: true
cache-dependency-path: |
Directory.Packages.props
src/**/packages.lock.json
Directory.Build.props
- name: Restore solution
run: dotnet restore SharpEmu.slnx --locked-mode
run: dotnet restore SharpEmu.slnx
- name: Build solution
run: dotnet build SharpEmu.slnx -c Release --no-restore
# Also runs on Linux/macOS: the aerolib.bin MSBuild task does platform-sensitive
# path handling, so a cross-platform generation regression surfaces here.
- name: Run tests
run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal
- name: Publish ${{ matrix.rid }} CLI
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r ${{ matrix.rid }} --self-contained true --no-restore -p:PublishDir="$PUBLISH_DIR"
@@ -175,14 +194,14 @@ jobs:
run: |
mkdir -p "$RELEASE_DIR"
# tar keeps the executable bit, which zip would drop.
tar -czf "$RELEASE_DIR/sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz" \
tar -czf "$RELEASE_DIR/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz" \
-C "$PUBLISH_DIR" .
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
path: ${{ env.RELEASE_DIR }}/sharpemu-${{ needs.init.outputs.version }}-${{ matrix.rid }}.tar.gz
if-no-files-found: error
release:
@@ -190,78 +209,42 @@ jobs:
needs:
- init
- build
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
- build-posix
# Versioned releases are immutable and tag-driven. Branch and manual runs
# still produce Actions artifacts without modifying a published release.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download build artifact
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
name: ${{ needs.init.outputs.artifact-name }}
path: release
- name: Create or update release
- name: Create release
shell: bash
env:
ARCHIVE_NAME: ${{ needs.init.outputs.archive-name }}
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NAME: ${{ needs.init.outputs.release-name }}
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
VERSION: ${{ needs.init.outputs.version }}
run: |
asset_path="release/${ARCHIVE_NAME}"
notes="Automated Windows build for commit ${GITHUB_SHA}."
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
gh release upload "${RELEASE_TAG}" "${asset_path}" --clobber
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
else
gh release create "${RELEASE_TAG}" "${asset_path}" \
--title "${RELEASE_NAME}" \
--notes "${notes}" \
--target "${GITHUB_SHA}"
mapfile -t assets < <(find release -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
if [ "${#assets[@]}" -eq 0 ]; then
echo "No release assets found." >&2
exit 1
fi
release-posix:
name: Publish GitHub Release (${{ matrix.rid }})
needs:
- init
- build-posix
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
fail-fast: false
matrix:
rid: [linux-x64, osx-x64]
steps:
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
path: release
- name: Create or update release
shell: bash
env:
ARCHIVE_NAME: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}.tar.gz
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_NAME: SharpEmu ${{ matrix.rid }} ${{ needs.init.outputs.short-sha }}
RELEASE_TAG: ${{ matrix.rid }}-${{ needs.init.outputs.safe-ref }}-${{ needs.init.outputs.short-sha }}
RID: ${{ matrix.rid }}
run: |
asset_path="release/${ARCHIVE_NAME}"
notes="Automated ${RID} build for commit ${GITHUB_SHA}."
notes="Automated SharpEmu v${VERSION} build for commit ${GITHUB_SHA}."
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
gh release upload "${RELEASE_TAG}" "${asset_path}" --clobber
gh release edit "${RELEASE_TAG}" --title "${RELEASE_NAME}" --notes "${notes}"
else
gh release create "${RELEASE_TAG}" "${asset_path}" \
--title "${RELEASE_NAME}" \
--notes "${notes}" \
--target "${GITHUB_SHA}"
echo "Release ${RELEASE_TAG} already exists and will not be modified." >&2
exit 1
fi
gh release create "${RELEASE_TAG}" "${assets[@]}" \
--verify-tag \
--title "${RELEASE_NAME}" \
--notes "${notes}"
+5
View File
@@ -5,6 +5,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
# Contributing
> [!IMPORTANT]
> The pull request template is mandatory.
>
> Pull requests that do not follow the template or leave the required checklist incomplete will be closed without review, even if the proposed code is technically correct or beneficial. Please review these contribution guidelines before submitting a pull request.
Contributions are always welcome!
Before opening a pull request, please keep the following in mind:
+2 -1
View File
@@ -9,7 +9,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<SharpEmuVersion>0.0.2-beta.2</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
+3
View File
@@ -12,6 +12,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
<PackageVersion Include="Iced" Version="1.21.0" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.14.8" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Silk.NET.Input" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
+39 -48
View File
@@ -23,11 +23,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<strong>Join our Discord for development updates, compatibility discussions, support, and community chat.</strong>
</p>
---
> [!NOTE]
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
> can run the macOS x64 build through Rosetta 2.
---
> [!NOTE]
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
> can run the macOS x64 build through Rosetta 2.
> [!WARNING]
> SharpEmu is an experimental PS5 emulator developed from scratch in C#. The current focus is on accuracy and infrastructure setup rather than game-specific compatibility.
@@ -41,6 +41,16 @@ This project is developed purely for research and educational purposes. There ar
SharpEmu focuses exclusively on the PlayStation 5.
Our goal is **not** to emulate PS4 games, as there is already an excellent emulator dedicated to that platform: **ShadPS4**.
## Games Tested
| Demons Souls Remake | Dreaming Sarah |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![Bloodborne screenshot](./.github/images/demons-souls.jpg) | ![Dreaming Sarah](./.github/images/dreaming-sarah.jpg) |
| Void Terrarium | Dead Cells |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Void Terrarium](./.github/images/void-terrarium.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
## Status
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle kernel-related functionality. However, several critical components are still missing.
@@ -60,52 +70,33 @@ Current capabilities include:
Some games have reached like `sceVideoOut` and AGC stages.
SharpEmu supports Windows, Linux, and macOS hosts. Video output uses Vulkan on
Windows and Linux, and MoltenVK on macOS. Platform support is still experimental,
so compatibility and performance vary by game, operating system, and GPU driver.
## Using
Download the release archive for your operating system, extract it, and launch
SharpEmu with the path to a legally obtained game's `eboot.bin`.
Windows PowerShell:
```powershell
.\SharpEmu.exe "C:\path\to\game\eboot.bin" 2>&1 |
Tee-Object -FilePath "SharpEmu.log"
```
Linux and macOS:
```bash
chmod +x ./SharpEmu
./SharpEmu "/path/to/game/eboot.bin" 2>&1 |
tee SharpEmu.log
```
A Vulkan-capable GPU and current graphics driver are required. The macOS
release includes the MoltenVK Vulkan implementation.
SharpEmu supports Windows, Linux, and macOS hosts. Video output uses Vulkan on
Windows and Linux, and MoltenVK on macOS. Platform support is still experimental,
so compatibility and performance vary by game, operating system, and GPU driver.
## Games Tested
## Using
* **Demon's Souls Remake**
* [Demon's Souls [PPSA01341]](https://github.com/par274/sharpemu/issues/2)
* Demon's Souls is now video loop. Shaders are ready to be converted to SPIR-V/Vulkan. We are continuing our work on this.
![DeS videoOut submit first frame](./.github/images/des-videoout-shaders.jpg)
Download the release archive for your operating system, extract it, and launch
SharpEmu with the path to a legally obtained game's `eboot.bin`.
* **Poppy Playtime Chapter 1**
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/par274/sharpemu/issues/3)
Windows PowerShell:
* **SILENT HILL: The Short Message**
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/par274/sharpemu/issues/4)
```powershell
.\SharpEmu.exe "C:\path\to\game\eboot.bin" 2>&1 |
Tee-Object -FilePath "SharpEmu.log"
```
* **Dreaming Sarah**
* [Dreaming Sarah [PPSA02929]](https://github.com/par274/sharpemu/issues/9)
* Real texture rendering for this game;
![Splash texture](./.github/images/dreaming-sarah.jpg)
Linux and macOS:
```bash
chmod +x ./SharpEmu
./SharpEmu "/path/to/game/eboot.bin" 2>&1 |
tee SharpEmu.log
```
A Vulkan-capable GPU and current graphics driver are required. The macOS
release includes the MoltenVK Vulkan implementation.
> [!IMPORTANT]
> This project does **not** support or condone piracy.
@@ -114,8 +105,8 @@ release includes the MoltenVK Vulkan implementation.
## Build
1. Install the .NET SDK version specified in [`global.json`](./global.json).
2. Clone the repository: `git clone https://github.com/par274/sharpemu.git`
1. Install the .NET SDK version specified in [`global.json`](./global.json).
2. Clone the repository: `git clone https://github.com/sharpemu/sharpemu.git`
3. Open the solution file (`SharpEmu.slnx`) in **VSCode**.
4. Build the project: `dotnet build` or `dotnet publish`
5. Build artifacts will be located in the `artifacts` directory.
@@ -141,7 +132,7 @@ Provided valuable references for filesystem handling and low-level C# implementa
# License
- [**GPL-2.0 license**](https://github.com/par274/sharpemu/blob/main/LICENSE)
- [**GPL-2.0 license**](https://github.com/sharpemu/sharpemu/blob/main/LICENSE)
## Contributing
-2
View File
@@ -5,9 +5,7 @@ path = [
"REUSE.toml",
"nuget.config",
"global.json",
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
"src/SharpEmu.GUI/Languages/**",
"_logs/**",
".github/images/**",
+4
View File
@@ -11,8 +11,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
<Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" />
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
</Folder>
</Solution>
Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+46
View File
@@ -0,0 +1,46 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Bink 2 bridge
Demon's Souls plays Bink 2 (.bk2) files through a Bink implementation linked
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
decoder cannot observe or replace those frames.
SharpEmu observes successful guest .bk2 opens and, when a Bink bridge is
available, presents its decoded BGRA frames at the normal guest-flip boundary.
This preserves the game's own timing and lets the host Vulkan presenter display
the movie without trying to execute the PS5-specific Bink GPU decode path.
Without an adapter, Bink movies are skipped by default: their open call returns
not-found so games that mark cinematics as optional progress to their next
state instead of waiting on an empty Bink GPU texture.
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
only; it does not decode the movie or alter its game logic. Set
SHARPEMU_BINK_MODE=native to force native bridge mode.
## Supplying the adapter
Bink 2 is proprietary. Obtain a compatible Mac Bink 2 SDK from RAD Game Tools,
then compile sharpemu_bink2_bridge.c against the SDK's bink.h and Mac library.
The adapter deliberately contains only a three-function C ABI so the managed
emulator never depends on RAD's private binary ABI.
Place the resulting libsharpemu_bink2_bridge.dylib next to the SharpEmu
executable, or point to it explicitly:
SHARPEMU_BINK2_BRIDGE=/absolute/path/libsharpemu_bink2_bridge.dylib \
./SharpEmu /path/to/eboot.bin
The expected exports are sharpemu_bink2_open_utf8,
sharpemu_bink2_decode_next_bgra, and sharpemu_bink2_close. The supplied
adapter opens one movie, exposes BGRA pixels, and advances after each decoded
frame. The managed side validates dimensions and retains ownership of the
destination buffer.
If the bridge is absent in native mode, SharpEmu logs one informational line
and retains the existing guest rendering path.
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2026 SharpEmu Emulator Project
* SPDX-License-Identifier: GPL-2.0-or-later
*
* Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its
* headers are not distributed by SharpEmu. See docs/bink2-bridge.md.
*/
#include <stdint.h>
#include "bink.h"
typedef struct sharpemu_bink2_info {
uint32_t width;
uint32_t height;
uint32_t frames_per_second_numerator;
uint32_t frames_per_second_denominator;
} sharpemu_bink2_info;
int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) {
HBINK bink;
if (!path || !movie || !info) return 0;
bink = BinkOpen(path, 0);
if (!bink) return 0;
*movie = bink;
info->width = bink->Width;
info->height = bink->Height;
info->frames_per_second_numerator = bink->FrameRate;
info->frames_per_second_denominator = bink->FrameRateDiv;
return 1;
}
int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination,
uint32_t stride, uint32_t destination_bytes) {
uint64_t needed;
if (!movie || !destination || stride < movie->Width * 4) return 0;
needed = (uint64_t)stride * movie->Height;
if (needed > destination_bytes) return 0;
/* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
if (BinkWait(movie)) return 0;
BinkDoFrame(movie);
BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA);
BinkNextFrame(movie);
return 1;
}
void sharpemu_bink2_close(HBINK movie) {
if (movie) BinkClose(movie);
}
+90
View File
@@ -0,0 +1,90 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
## Release Script
SharpEmu releases are prepared and published using `scripts/release.py`.
The release process consists of two steps:
1. Prepare the version bump through a pull request.
2. Create and push the release tag after the pull request has been merged.
### Preparing a Release
Run:
```bash
python scripts/release.py prepare 0.0.2-beta.2
```
This command will:
- Verify that the working tree is clean.
- Update the local `main` branch.
- Create a new branch named `release/0.0.2-beta.2`.
- Update `SharpEmuVersion` in `Directory.Build.props`.
- Create a version bump commit.
- Push the release branch to the remote repository.
Afterwards, open a pull request from:
```text
release/0.0.2-beta.2
```
into:
```text
main
```
### Publishing a Release
Once the pull request has been merged, update your local repository:
```bash
git switch main
git pull --ff-only
```
Then create and push the release tag:
```bash
python scripts/release.py tag 0.0.2-beta.2
```
This command will:
- Verify that the working tree is clean.
- Confirm that `Directory.Build.props` contains the requested version.
- Create an annotated Git tag (`v0.0.2-beta.2`).
- Push the tag to the remote repository.
Pushing the tag automatically triggers the GitHub Release workflow.
### Version Format
Specify the version **without** the `v` prefix.
Examples:
```text
0.0.2
0.0.2-alpha.1
0.0.2-beta.1
0.0.2-beta.2
0.0.2-rc.1
```
The script automatically prefixes the Git tag with `v`.
### Notes
- Run `prepare` only from the `main` branch.
- Run `tag` only after the version bump pull request has been merged.
- Do not create release tags manually before merging the version bump.
- Both commands require a clean working tree.
- The version in `Directory.Build.props` must exactly match the version passed to the `tag` command.
-172
View File
@@ -1,172 +0,0 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
#!/usr/bin/env python3
"""Offline check: SysAbiExport ExportName must hash to its Nid (name2nid).
NIDs absent from aerolib.bin are skipped (unknown/unresolved symbols).
Known historic mislabels may be allowlisted with a one-line reason.
Run from the repository root:
python scripts/check_sysabi_aerolib.py
python scripts/check_sysabi_aerolib.py --strict
"""
from __future__ import annotations
import argparse
import hashlib
import re
import struct
import sys
from base64 import b64encode as base64enc
from binascii import unhexlify as uhx
from pathlib import Path
SRC_ROOT = Path("src")
AEROLIB_BIN = Path("src/SharpEmu.HLE/Aerolib/aerolib.bin")
SYSABI_EXPORT_RE = re.compile(r"\[SysAbiExport\((.*?)\)\]", re.DOTALL)
NID_RE = re.compile(r'Nid\s*=\s*"([^"]+)"')
EXPORT_NAME_RE = re.compile(r'ExportName\s*=\s*"([^"]+)"')
# NID -> reason. Keep minimal; fix ExportName when safe instead of growing this list.
ALLOWLISTED_NIDS: dict[str, str] = {
"KMcEa+rHsIo": "Historic kernel MapMemory stub bound to sceAvPlayerAddSource NID; API rewrite deferred.",
"WV1GwM32NgY": "Historic WebApi2 init alias for PushEventCreateHandle NID; ABI rewrite deferred.",
}
def name2nid(name: str) -> str:
symbol = hashlib.sha1(name.encode() + uhx("518D64A635DED8C1E6B039B1C3E55230")).digest()
id_val = struct.unpack("<Q", symbol[:8])[0]
nid = base64enc(uhx("%016x" % id_val), b"+-").rstrip(b"=")
return nid.decode("utf-8")
def find_repo_root() -> Path:
cwd = Path.cwd()
if (cwd / SRC_ROOT).is_dir() and (cwd / "scripts").is_dir():
return cwd
script_root = Path(__file__).resolve().parent.parent
if (script_root / SRC_ROOT).is_dir():
return script_root
raise SystemExit("Run from the repository root (src/ and scripts/ expected).")
def load_aerolib_nids(aerolib_path: Path) -> set[str]:
data = aerolib_path.read_bytes()
if len(data) < 4:
raise SystemExit(f"Aerolib binary too small: {aerolib_path}")
count = struct.unpack_from("<I", data, 0)[0]
offset = 4
nids: set[str] = set()
for _ in range(count):
if offset >= len(data):
raise SystemExit(f"Truncated aerolib.bin while reading NIDs: {aerolib_path}")
nid_len = data[offset]
offset += 1
nid = data[offset : offset + nid_len].decode("utf-8")
offset += nid_len
if offset + 2 > len(data):
raise SystemExit(f"Truncated aerolib.bin name length: {aerolib_path}")
name_len = struct.unpack_from("<H", data, offset)[0]
offset += 2 + name_len
nids.add(nid)
return nids
def iter_sysabi_exports(cs_path: Path, text: str):
for match in SYSABI_EXPORT_RE.finditer(text):
block = match.group(1)
nid_match = NID_RE.search(block)
export_match = EXPORT_NAME_RE.search(block)
if nid_match is None or export_match is None:
continue
nid = nid_match.group(1)
export_name = export_match.group(1)
nid_attr = f'Nid = "{nid}"'
abs_pos = text.find(nid_attr, match.start(), match.end())
if abs_pos < 0:
abs_pos = match.start()
line = text.count("\n", 0, abs_pos) + 1
yield cs_path, line, nid, export_name
def scan(src_root: Path, catalog_nids: set[str]):
checked = 0
mismatches = []
skipped_no_catalog = 0
allowlisted = 0
for cs_path in sorted(src_root.rglob("*.cs")):
text = cs_path.read_text(encoding="utf-8")
for path, line, nid, export_name in iter_sysabi_exports(cs_path, text):
checked += 1
computed = name2nid(export_name)
if computed == nid:
continue
if nid not in catalog_nids:
skipped_no_catalog += 1
continue
if nid in ALLOWLISTED_NIDS:
allowlisted += 1
continue
mismatches.append((path, line, nid, export_name, computed))
return checked, mismatches, skipped_no_catalog, allowlisted
def main() -> int:
parser = argparse.ArgumentParser(
description="Check that SysAbiExport ExportName values hash to their Nid via name2nid."
)
parser.add_argument(
"--strict",
action="store_true",
help="Exit 1 when any non-skipped/non-allowlisted ExportName does not hash to its Nid.",
)
parser.add_argument(
"--quiet",
action="store_true",
help="Print only the summary line.",
)
args = parser.parse_args()
repo_root = find_repo_root()
aerolib_path = repo_root / AEROLIB_BIN
if not aerolib_path.is_file():
raise SystemExit(f"Missing Aerolib catalog: {aerolib_path.as_posix()}")
catalog_nids = load_aerolib_nids(aerolib_path)
checked, mismatches, skipped_no_catalog, allowlisted = scan(
repo_root / SRC_ROOT, catalog_nids
)
ok = checked - len(mismatches) - skipped_no_catalog - allowlisted
if not args.quiet:
for path, line, nid, export_name, computed in mismatches:
rel = path.relative_to(repo_root).as_posix()
print(
f"{rel}:{line}: NID={nid} ExportName={export_name!r} "
f"computed={computed}"
)
print(
f"checked={checked} ok={ok} fail={len(mismatches)} "
f"skipped_no_catalog={skipped_no_catalog} allowlisted={allowlisted} "
f"allowlist_size={len(ALLOWLISTED_NIDS)}"
)
if args.strict and mismatches:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
-54
View File
@@ -1,54 +0,0 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
#!/usr/bin/env python3
import hashlib
import struct
from base64 import b64encode as base64enc
from binascii import unhexlify as uhx
from pathlib import Path
NAMES = 'scripts/ps5_names.txt'
OUTPUT = 'src/SharpEmu.HLE/Aerolib/aerolib.bin'
def name2nid(name):
symbol = hashlib.sha1(name.encode() + uhx('518D64A635DED8C1E6B039B1C3E55230')).digest()
id_val = struct.unpack('<Q', symbol[:8])[0]
nid = base64enc(uhx('%016x' % id_val), b'+-').rstrip(b'=')
return nid.decode('utf-8')
def generate():
names_path = Path(NAMES)
output_path = Path(OUTPUT)
entries = []
with open(names_path, 'r', encoding='utf-8') as f:
for line in f:
name = line.strip()
if name:
nid = name2nid(name)
entries.append((nid, name))
print(f"Found {len(entries)} entries")
data = bytearray()
data.extend(struct.pack('<I', len(entries)))
for nid, name in entries:
nid_bytes = nid.encode('utf-8')
name_bytes = name.encode('utf-8')
data.append(len(nid_bytes))
data.extend(nid_bytes)
data.extend(struct.pack('<H', len(name_bytes)))
data.extend(name_bytes)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(data)
print(f"Generated: {output_path} ({len(data):,} bytes)")
print(f"Total entries: {len(entries)}")
if __name__ == "__main__":
generate()
+408
View File
@@ -0,0 +1,408 @@
#!/usr/bin/env python3
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
from __future__ import annotations
import argparse
import re
import subprocess
import sys
from pathlib import Path
VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$")
VERSION_ELEMENT_PATTERN = re.compile(
r"(<SharpEmuVersion>)([^<]+)(</SharpEmuVersion>)"
)
class ReleaseError(RuntimeError):
pass
def run_git(
*args: str,
cwd: Path,
capture_output: bool = False,
) -> str:
command = ["git", *args]
try:
result = subprocess.run(
command,
cwd=cwd,
check=True,
text=True,
capture_output=capture_output,
)
except FileNotFoundError:
raise ReleaseError("Git was not found in PATH.") from None
except subprocess.CalledProcessError as error:
stderr = error.stderr.strip() if error.stderr else ""
detail = f"\n{stderr}" if stderr else ""
raise ReleaseError(
f"Git command failed: {' '.join(command)}{detail}"
) from error
return result.stdout.strip() if capture_output else ""
def find_repository_root(script_path: Path) -> Path:
root = run_git(
"rev-parse",
"--show-toplevel",
cwd=script_path.resolve().parent,
capture_output=True,
)
return Path(root)
def get_status(repository_root: Path) -> str:
return run_git(
"status",
"--porcelain",
cwd=repository_root,
capture_output=True,
)
def ensure_clean_worktree(repository_root: Path) -> None:
status = get_status(repository_root)
if status:
raise ReleaseError(
"The working tree is not clean.\n\n"
f"{status}\n\n"
"Commit, stash, or remove these changes first."
)
def get_current_branch(repository_root: Path) -> str:
branch = run_git(
"branch",
"--show-current",
cwd=repository_root,
capture_output=True,
)
if not branch:
raise ReleaseError(
"HEAD is detached. Switch to a branch before continuing."
)
return branch
def ensure_branch_does_not_exist(
repository_root: Path,
branch: str,
remote: str,
) -> None:
local_branch = run_git(
"branch",
"--list",
branch,
cwd=repository_root,
capture_output=True,
)
if local_branch:
raise ReleaseError(f"Branch {branch} already exists locally.")
remote_branch = run_git(
"ls-remote",
"--heads",
remote,
branch,
cwd=repository_root,
capture_output=True,
)
if remote_branch:
raise ReleaseError(
f"Branch {branch} already exists on {remote}."
)
def ensure_tag_does_not_exist(
repository_root: Path,
tag: str,
remote: str,
) -> None:
local_tag = run_git(
"tag",
"--list",
tag,
cwd=repository_root,
capture_output=True,
)
if local_tag:
raise ReleaseError(f"Tag {tag} already exists locally.")
remote_tag = run_git(
"ls-remote",
"--tags",
remote,
f"refs/tags/{tag}",
cwd=repository_root,
capture_output=True,
)
if remote_tag:
raise ReleaseError(f"Tag {tag} already exists on {remote}.")
def read_version(props_path: Path) -> str:
if not props_path.exists():
raise ReleaseError(f"Version file not found: {props_path}")
content = props_path.read_text(encoding="utf-8")
match = VERSION_ELEMENT_PATTERN.search(content)
if match is None:
raise ReleaseError(
f"SharpEmuVersion was not found in {props_path.name}."
)
return match.group(2).strip()
def update_version(props_path: Path, version: str) -> str:
content = props_path.read_text(encoding="utf-8")
current_version = read_version(props_path)
if current_version == version:
raise ReleaseError(
f"SharpEmuVersion is already set to {version}."
)
updated_content, replacement_count = VERSION_ELEMENT_PATTERN.subn(
rf"\g<1>{version}\g<3>",
content,
count=1,
)
if replacement_count != 1:
raise ReleaseError(
"Expected exactly one SharpEmuVersion element."
)
props_path.write_text(
updated_content,
encoding="utf-8",
newline="\n",
)
return current_version
def prepare_release(
repository_root: Path,
props_path: Path,
version: str,
remote: str,
) -> None:
ensure_clean_worktree(repository_root)
current_branch = get_current_branch(repository_root)
if current_branch != "main":
raise ReleaseError(
f"Prepare must be run from main, not {current_branch}."
)
run_git(
"pull",
"--ff-only",
remote,
"main",
cwd=repository_root,
)
branch = f"release/{version}"
ensure_branch_does_not_exist(
repository_root,
branch,
remote,
)
previous_version = read_version(props_path)
run_git(
"switch",
"-c",
branch,
cwd=repository_root,
)
try:
update_version(props_path, version)
relative_props_path = props_path.relative_to(repository_root)
run_git(
"add",
relative_props_path.as_posix(),
cwd=repository_root,
)
run_git(
"commit",
"-m",
f"chore: bump version to {version}",
cwd=repository_root,
)
run_git(
"push",
"-u",
remote,
branch,
cwd=repository_root,
)
except Exception:
print(
"\nPrepare failed. The release branch may still exist locally.",
file=sys.stderr,
)
raise
print()
print(f"Prepared release {previous_version} -> {version}")
print(f"Branch pushed: {branch}")
print()
print("Open a pull request from:")
print(f" {branch}")
print("into:")
print(" main")
print()
print("After merging the PR, run:")
print(f" python scripts/release.py tag {version}")
def create_release_tag(
repository_root: Path,
props_path: Path,
version: str,
remote: str,
) -> None:
ensure_clean_worktree(repository_root)
current_branch = get_current_branch(repository_root)
if current_branch != "main":
raise ReleaseError(
f"Tagging must be run from main, not {current_branch}."
)
run_git(
"pull",
"--ff-only",
remote,
"main",
cwd=repository_root,
)
current_version = read_version(props_path)
if current_version != version:
raise ReleaseError(
"Version mismatch:\n"
f" Directory.Build.props: {current_version}\n"
f" Requested tag: {version}"
)
tag = f"v{version}"
ensure_tag_does_not_exist(
repository_root,
tag,
remote,
)
run_git(
"tag",
"-a",
tag,
"-m",
f"SharpEmu {version}",
cwd=repository_root,
)
run_git(
"push",
remote,
tag,
cwd=repository_root,
)
print()
print(f"Successfully pushed tag {tag}.")
print("The release workflow should start automatically.")
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Prepare or tag a SharpEmu release."
)
subparsers = parser.add_subparsers(
dest="command",
required=True,
)
for command in ("prepare", "tag"):
subparser = subparsers.add_parser(command)
subparser.add_argument(
"version",
help="Version without the v prefix, e.g. 0.0.2-beta.2.",
)
subparser.add_argument(
"--remote",
default="origin",
help="Git remote. Default: origin.",
)
arguments = parser.parse_args()
if not VERSION_PATTERN.fullmatch(arguments.version):
parser.error(
"Version must look like 0.0.2, "
"0.0.2-beta.2, or 0.0.2-rc.1."
)
return arguments
def main() -> int:
arguments = parse_arguments()
try:
repository_root = find_repository_root(Path(__file__))
props_path = repository_root / "Directory.Build.props"
if arguments.command == "prepare":
prepare_release(
repository_root,
props_path,
arguments.version,
arguments.remote,
)
else:
create_release_tag(
repository_root,
props_path,
arguments.version,
arguments.remote,
)
except ReleaseError as error:
print(f"Error: {error}", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
+34
View File
@@ -45,6 +45,11 @@ internal static partial class Program
[STAThread]
private static int Main(string[] args)
{
// Avoid blocking full collections while guest and render threads are
// running, and establish the GC mode before the runtime reserves the
// fixed guest address-space window.
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
try
{
return Run(args);
@@ -58,6 +63,11 @@ internal static partial class Program
private static int Run(string[] args)
{
if (Updater.TryApply(args, out var updateExitCode))
{
return updateExitCode;
}
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (args.Length == 0 && !isMitigatedChild)
{
@@ -84,6 +94,7 @@ internal static partial class Program
{
if (OperatingSystem.IsMacOS())
{
ConfigureMoltenVkDefaults();
PreloadMacVulkanLoader();
}
@@ -151,6 +162,26 @@ internal static partial class Program
return false;
}
/// <summary>
/// Applies MoltenVK performance defaults before the Vulkan loader is
/// loaded. Existing user-provided values always take precedence.
/// </summary>
private static void ConfigureMoltenVkDefaults()
{
try
{
_ = MacSetEnv("MVK_CONFIG_SYNCHRONOUS_QUEUE_SUBMITS", "0", 0);
_ = MacSetEnv("MVK_CONFIG_SHOULD_MAXIMIZE_CONCURRENT_COMPILATION", "1", 0);
_ = MacSetEnv("MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS", "1", 0);
_ = MacSetEnv("MVK_CONFIG_RESUME_LOST_DEVICE", "1", 0);
}
catch (Exception exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Failed to set MoltenVK defaults: {exception.Message}");
}
}
/// <summary>
/// Makes a Vulkan loader visible to GLFW's dlopen("libvulkan.1.dylib").
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
@@ -1319,4 +1350,7 @@ internal static partial class Program
uint creationDisposition,
uint flagsAndAttributes,
nint templateFile);
[DllImport("libSystem", EntryPoint = "setenv")]
private static extern int MacSetEnv(string name, string value, int overwrite);
}
+5 -2
View File
@@ -25,8 +25,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Version>0.0.1</Version>
<ServerGarbageCollection>true</ServerGarbageCollection>
<!-- Server GC reserves one large heap range per logical processor. On
high-core-count hosts those ranges can overlap fixed PS5 image bases
before the guest address space is established. Workstation GC avoids
that collision and keeps the emulator's required mappings available. -->
<ServerGarbageCollection>false</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
-572
View File
@@ -1,572 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "11.3.2",
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
},
"Avalonia.FreeDesktop": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
"dependencies": {
"Avalonia": "11.3.18",
"Tmds.DBus.Protocol": "0.21.3"
}
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
},
"Avalonia.Skia": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
"dependencies": {
"Avalonia": "11.3.18",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
}
},
"Avalonia.Win32": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
}
},
"Avalonia.X11": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.FreeDesktop": "11.3.18",
"Avalonia.Skia": "11.3.18"
}
},
"HarfBuzzSharp": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Input.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Silk.NET.Input.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.core": {
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
"SharpEmu.HLE": "[1.0.0, )",
"SharpEmu.Libs": "[1.0.0, )",
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.gui": {
"type": "Project",
"dependencies": {
"Avalonia": "[11.3.18, )",
"Avalonia.Desktop": "[11.3.18, )",
"Avalonia.Fonts.Inter": "[11.3.18, )",
"Avalonia.Themes.Fluent": "[11.3.18, )",
"SharpEmu.Logging": "[1.0.0, )",
"Tmds.DBus.Protocol": "[0.21.3, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )",
"Silk.NET.Input": "[2.23.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
"Silk.NET.Windowing": "[2.23.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Avalonia": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Input": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Input.Glfw": "2.23.0"
}
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"Silk.NET.Core": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.EXT": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.KHR": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Tmds.DBus.Protocol": {
"type": "CentralTransitive",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
}
},
"net10.0/linux-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-arm64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/win-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
}
}
}
+71 -42
View File
@@ -7,15 +7,11 @@ using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu;
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
private enum EntryFrameKind
{
ProcessEntry,
@@ -31,9 +27,9 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
private static readonly ulong TlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
private const ulong TlsSize = 0x0001_0000UL;
// The static TLS blocks live at negative offsets from the TCB (FreeBSD
// amd64 variant II); libc.prx alone reaches beyond -0x1700, so give the
// prefix a full 64KB on POSIX. Windows keeps its historical 4KB prefix.
private static readonly ulong TlsPrefixSize = OperatingSystem.IsWindows() ? 0x0000_1000UL : 0x0001_0000UL;
// amd64 variant II). Keep every host in sync with GuestTlsTemplate's
// startup reservation; PS5 modules routinely reach beyond one host page.
private const ulong TlsPrefixSize = GuestTlsTemplate.StartupStaticTlsReservation;
private static readonly ulong BootstrapStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_F000_0000UL : 0x6FFD_F000_0000UL;
private static readonly ulong BootstrapPayloadBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_E000_0000UL : 0x6FFD_E000_0000UL;
private static readonly ulong DynlibFallbackStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_D000_0000UL : 0x6FFD_D000_0000UL;
@@ -49,19 +45,16 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
];
private readonly IVirtualMemory _virtualMemory;
private readonly IModuleManager _moduleManager;
private readonly IHostPlatform? _hostPlatform;
private INativeCpuBackend? _nativeCpuBackend;
public CpuDispatcher(
IVirtualMemory virtualMemory,
IModuleManager moduleManager,
INativeCpuBackend? nativeCpuBackend = null,
IHostPlatform? hostPlatform = null)
INativeCpuBackend? nativeCpuBackend = null)
{
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
_moduleManager = moduleManager ?? throw new ArgumentNullException(nameof(moduleManager));
_nativeCpuBackend = nativeCpuBackend;
_hostPlatform = hostPlatform;
}
public ulong? LastEntryPoint { get; private set; }
@@ -94,8 +87,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string processImageName = "eboot.bin",
CpuExecutionOptions executionOptions = default)
{
Log.Debug("=== DispatchEntry START ===");
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
try
{
@@ -103,7 +96,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
catch (Exception ex)
{
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw;
}
}
@@ -116,8 +110,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string moduleName = "module",
CpuExecutionOptions executionOptions = default)
{
Log.Debug("=== DispatchModuleInitializer START ===");
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
try
{
@@ -132,7 +126,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
catch (Exception ex)
{
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw;
}
}
@@ -146,7 +141,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
CpuExecutionOptions executionOptions = default,
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
{
Log.Debug("DispatchEntryCore STARTING...");
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
LastEntryPoint = entryPoint;
LastTrapInfo = null;
@@ -277,7 +272,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
entryFrameDiagnostic,
Environment.NewLine,
"CpuEngine: native-only");
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager, _hostPlatform);
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
if (_nativeCpuBackend.TryExecute(
context,
entryPoint,
@@ -318,7 +313,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
LastMilestoneLog,
Environment.NewLine,
$"CpuEngine native-only failed: {backendError}");
Log.Error($"Native backend FAILED: {backendError}");
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
return FailEarly(
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
CpuExitReason.NativeBackendUnavailable);
@@ -377,11 +372,19 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
private static bool InitializeTls(CpuContext context, ulong tlsBase)
{
return context.TryWriteUInt64(tlsBase - 0xF0, 0) &&
context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL) &&
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
if (!context.TryWriteUInt64(tlsBase - 0xF0, 0) ||
!context.TryWriteUInt64(tlsBase + 0x00, tlsBase) ||
!context.TryWriteUInt64(tlsBase + 0x10, tlsBase) ||
!context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBA00UL) ||
!context.TryWriteUInt64(tlsBase + 0x60, tlsBase))
{
return false;
}
// Seed the static TLS block below the thread pointer with the main
// module's initialized thread-locals (variant II layout).
SharpEmu.HLE.GuestTlsTemplate.SeedThreadBlock(context, tlsBase);
return true;
}
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
@@ -407,35 +410,54 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
ulong programExitHandlerAddress)
{
var imageName = string.IsNullOrWhiteSpace(processImageName) ? "eboot.bin" : processImageName;
var encodedNameLength = Encoding.UTF8.GetByteCount(imageName);
Span<byte> argv0Buffer = encodedNameLength + 1 <= 512
? stackalloc byte[encodedNameLength + 1]
: new byte[encodedNameLength + 1];
if (Encoding.UTF8.GetBytes(imageName.AsSpan(), argv0Buffer) != encodedNameLength)
var arguments = new List<string>(3) { imageName };
var configuredArguments = Environment.GetEnvironmentVariable("SHARPEMU_GUEST_ARGS");
if (!string.IsNullOrWhiteSpace(configuredArguments))
{
return false;
// The PS5 entry-parameter ABI exposes three inline argv pointers.
// Two compatibility arguments are therefore safe without changing
// the fixed 0x20-byte structure expected by existing titles.
var compatibilityArguments = configuredArguments.Split(
(char[]?)null,
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
arguments.AddRange(compatibilityArguments.Take(2));
}
argv0Buffer[encodedNameLength] = 0;
var cursor = context[CpuRegister.Rsp];
var argv0Address = AlignDown(cursor - (ulong)argv0Buffer.Length, 16);
if (!context.Memory.TryWrite(argv0Address, argv0Buffer))
var argumentAddresses = new ulong[arguments.Count];
for (var index = arguments.Count - 1; index >= 0; index--)
{
return false;
var encoded = Encoding.UTF8.GetBytes(arguments[index] + '\0');
cursor = AlignDown(cursor - (ulong)encoded.Length, 16);
if (!context.Memory.TryWrite(cursor, encoded))
{
return false;
}
argumentAddresses[index] = cursor;
}
const ulong entryParamsSize = 0x20;
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
var entryParamsAddress = AlignDown(cursor - entryParamsSize, 16);
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, (uint)arguments.Count) ||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argumentAddresses[0]) ||
!context.TryWriteUInt64(
entryParamsAddress + 0x10,
argumentAddresses.Length > 1 ? argumentAddresses[1] : 0) ||
!context.TryWriteUInt64(
entryParamsAddress + 0x18,
argumentAddresses.Length > 2 ? argumentAddresses[2] : 0))
{
return false;
}
if (arguments.Count > 1)
{
Console.Error.WriteLine(
$"[DISPATCHER] Guest arguments: {string.Join(' ', arguments.Skip(1))}");
}
var entryStackPointer = entryParamsAddress - sizeof(ulong);
if (!context.TryWriteUInt64(entryStackPointer, 0))
{
@@ -468,6 +490,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return value & ~(alignment - 1);
}
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return context.Memory.TryWrite(address, buffer);
}
private static string BuildEntryFrameDiagnostic(
ulong entryPoint,
CpuContext context,
@@ -0,0 +1,305 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Numerics;
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// Pure software implementations of the BMI1, BMI2 and ABM general-purpose-register
/// bit-manipulation instructions.
///
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's
/// Zen 2 cores implement BMI1/BMI2/ABM, but a host CPU that predates those extensions raises
/// #UD (STATUS_ILLEGAL_INSTRUCTION) when it meets one of these opcodes. This class provides the
/// register-only arithmetic so the exception handler can finish the instruction in software and
/// resume, instead of aborting the title.
///
/// The methods deliberately operate on plain integers rather than on the OS CONTEXT record so the
/// semantics can be unit-tested in isolation; the unsafe register/memory plumbing lives in the
/// backend adapter. Each method returns the result already masked to the operand width and, where
/// the instruction is defined to touch flags, updates <paramref name="eflags"/> in place. Flags
/// documented as "undefined" by the vendor manuals are left untouched so behaviour stays
/// deterministic across hosts.
/// </summary>
public static class BmiInstructionEmulator
{
private const uint FlagCarry = 1u << 0;
private const uint FlagZero = 1u << 6;
private const uint FlagSign = 1u << 7;
private const uint FlagOverflow = 1u << 11;
private static ulong WidthMask(GprOperandSize size) =>
size == GprOperandSize.Bits64 ? ulong.MaxValue : 0xFFFF_FFFFUL;
private static int WidthBits(GprOperandSize size) => (int)size;
private static bool SignSet(ulong value, GprOperandSize size) =>
size == GprOperandSize.Bits64 ? (value >> 63) != 0 : (value & 0x8000_0000UL) != 0;
private static uint WithFlag(uint eflags, uint flag, bool set) =>
set ? eflags | flag : eflags & ~flag;
// Applies the CF/ZF/SF/OF set shared by ANDN/BLS*/BZHI: OF is always cleared, ZF and SF follow
// the result, and the caller supplies CF because each instruction defines it differently.
private static uint ApplyLogicFlags(uint eflags, ulong result, GprOperandSize size, bool carry)
{
eflags = WithFlag(eflags, FlagCarry, carry);
eflags = WithFlag(eflags, FlagZero, result == 0);
eflags = WithFlag(eflags, FlagSign, SignSet(result, size));
eflags = WithFlag(eflags, FlagOverflow, false);
return eflags;
}
/// <summary>ANDN: <c>dest = (~src1) &amp; src2</c>. CF and OF are cleared.</summary>
public static ulong Andn(ulong src1, ulong src2, GprOperandSize size, ref uint eflags)
{
var result = (~src1 & src2) & WidthMask(size);
eflags = ApplyLogicFlags(eflags, result, size, carry: false);
return result;
}
/// <summary>BLSI: isolate the lowest set bit, <c>dest = (-src) &amp; src</c>. CF = (src != 0).</summary>
public static ulong Blsi(ulong src, GprOperandSize size, ref uint eflags)
{
var mask = WidthMask(size);
var s = src & mask;
var result = ((0UL - s) & s) & mask;
eflags = ApplyLogicFlags(eflags, result, size, carry: s != 0);
return result;
}
/// <summary>BLSMSK: mask up to and including the lowest set bit, <c>dest = (src - 1) ^ src</c>. CF = (src == 0).</summary>
public static ulong Blsmsk(ulong src, GprOperandSize size, ref uint eflags)
{
var mask = WidthMask(size);
var s = src & mask;
var result = ((s - 1) ^ s) & mask;
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
return result;
}
/// <summary>BLSR: reset the lowest set bit, <c>dest = (src - 1) &amp; src</c>. CF = (src == 0).</summary>
public static ulong Blsr(ulong src, GprOperandSize size, ref uint eflags)
{
var mask = WidthMask(size);
var s = src & mask;
var result = ((s - 1) & s) & mask;
eflags = ApplyLogicFlags(eflags, result, size, carry: s == 0);
return result;
}
/// <summary>
/// BEXTR: extract <c>len</c> bits of <paramref name="src"/> starting at bit <c>start</c>, where
/// start = control[7:0] and len = control[15:8]. Only ZF (per result) and cleared CF/OF are defined.
/// </summary>
public static ulong Bextr(ulong src, ulong control, GprOperandSize size, ref uint eflags)
{
var bits = WidthBits(size);
var start = (int)(control & 0xFF);
var length = (int)((control >> 8) & 0xFF);
ulong result;
if (start >= bits)
{
result = 0;
}
else
{
var shifted = (src & WidthMask(size)) >> start;
if (length == 0)
{
result = 0;
}
else if (length >= bits)
{
result = shifted;
}
else
{
result = shifted & ((1UL << length) - 1);
}
}
result &= WidthMask(size);
eflags = WithFlag(eflags, FlagZero, result == 0);
eflags = WithFlag(eflags, FlagCarry, false);
eflags = WithFlag(eflags, FlagOverflow, false);
return result;
}
/// <summary>
/// BZHI: zero the bits of <paramref name="src"/> from bit position <c>index[7:0]</c> upward.
/// CF is set when the requested position is at or beyond the operand width.
/// </summary>
public static ulong Bzhi(ulong src, ulong index, GprOperandSize size, ref uint eflags)
{
var bits = WidthBits(size);
var mask = WidthMask(size);
var s = src & mask;
var n = (int)(index & 0xFF);
ulong result;
bool carry;
if (n >= bits)
{
result = s;
carry = true;
}
else
{
result = s & ((1UL << n) - 1);
carry = false;
}
eflags = ApplyLogicFlags(eflags, result, size, carry);
return result;
}
/// <summary>TZCNT: count trailing zero bits. If src == 0 the result is the operand width and CF is set.</summary>
public static ulong Tzcnt(ulong src, GprOperandSize size, ref uint eflags)
{
var bits = WidthBits(size);
var s = src & WidthMask(size);
ulong result;
bool carry;
if (s == 0)
{
result = (ulong)bits;
carry = true;
}
else
{
result = (ulong)(size == GprOperandSize.Bits64
? BitOperations.TrailingZeroCount(s)
: BitOperations.TrailingZeroCount((uint)s));
carry = false;
}
eflags = WithFlag(eflags, FlagCarry, carry);
eflags = WithFlag(eflags, FlagZero, result == 0);
return result;
}
/// <summary>LZCNT: count leading zero bits. If src == 0 the result is the operand width and CF is set.</summary>
public static ulong Lzcnt(ulong src, GprOperandSize size, ref uint eflags)
{
var bits = WidthBits(size);
var s = src & WidthMask(size);
ulong result;
bool carry;
if (s == 0)
{
result = (ulong)bits;
carry = true;
}
else
{
result = (ulong)(size == GprOperandSize.Bits64
? BitOperations.LeadingZeroCount(s)
: BitOperations.LeadingZeroCount((uint)s));
carry = false;
}
eflags = WithFlag(eflags, FlagCarry, carry);
eflags = WithFlag(eflags, FlagZero, result == 0);
return result;
}
/// <summary>RORX: rotate <paramref name="src"/> right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
public static ulong Rorx(ulong src, int count, GprOperandSize size)
{
var bits = WidthBits(size);
var mask = WidthMask(size);
var s = src & mask;
var rotate = count & (bits - 1);
if (rotate == 0)
{
return s;
}
return ((s >> rotate) | (s << (bits - rotate))) & mask;
}
/// <summary>SARX: arithmetic shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
public static ulong Sarx(ulong src, int count, GprOperandSize size)
{
var bits = WidthBits(size);
var mask = WidthMask(size);
var shift = count & (bits - 1);
if (size == GprOperandSize.Bits64)
{
return (ulong)((long)src >> shift);
}
return (ulong)(uint)((int)(uint)src >> shift) & mask;
}
/// <summary>SHLX: logical shift left by <paramref name="count"/> (masked to the operand width). No flags.</summary>
public static ulong Shlx(ulong src, int count, GprOperandSize size)
{
var bits = WidthBits(size);
var mask = WidthMask(size);
var shift = count & (bits - 1);
return (src << shift) & mask;
}
/// <summary>SHRX: logical shift right by <paramref name="count"/> (masked to the operand width). No flags.</summary>
public static ulong Shrx(ulong src, int count, GprOperandSize size)
{
var bits = WidthBits(size);
var mask = WidthMask(size);
var s = src & mask;
var shift = count & (bits - 1);
return s >> shift;
}
/// <summary>PDEP: deposit contiguous low bits of <paramref name="src"/> into the positions selected by <paramref name="mask"/>. No flags.</summary>
public static ulong Pdep(ulong src, ulong mask, GprOperandSize size)
{
var bits = WidthBits(size);
var selector = mask & WidthMask(size);
ulong result = 0;
var bit = 0;
for (var i = 0; i < bits; i++)
{
var position = 1UL << i;
if ((selector & position) != 0)
{
if (((src >> bit) & 1UL) != 0)
{
result |= position;
}
bit++;
}
}
return result & WidthMask(size);
}
/// <summary>PEXT: gather the bits of <paramref name="src"/> selected by <paramref name="mask"/> into contiguous low bits. No flags.</summary>
public static ulong Pext(ulong src, ulong mask, GprOperandSize size)
{
var bits = WidthBits(size);
var selector = mask & WidthMask(size);
ulong result = 0;
var bit = 0;
for (var i = 0; i < bits; i++)
{
if ((selector & (1UL << i)) != 0)
{
if (((src >> i) & 1UL) != 0)
{
result |= 1UL << bit;
}
bit++;
}
}
return result & WidthMask(size);
}
}
@@ -0,0 +1,14 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// Operand width for the emulated general-purpose-register instructions. The numeric value is the
/// bit width, so it can double as the "count leading/trailing zeros of an all-zero source" result.
/// </summary>
public enum GprOperandSize
{
Bits32 = 32,
Bits64 = 64,
}
@@ -7,8 +7,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu.Native;
@@ -17,6 +17,55 @@ public sealed partial class DirectExecutionBackend
{
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
private static readonly bool _perfHleHistogram =
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE"), "1", System.StringComparison.Ordinal);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, long> _perfHleCounts = new();
private static long _perfHleTotal;
private static long _perfHleDispatchTicks;
private static void RecordPerfHleDispatchTime(long ticks)
{
var total = System.Threading.Interlocked.Add(ref _perfHleDispatchTicks, ticks);
var calls = System.Threading.Interlocked.Read(ref _perfHleTotal);
if (calls > 0 && calls % 500000 == 0)
{
var avgUs = (double)total / System.Diagnostics.Stopwatch.Frequency * 1_000_000.0 / calls;
System.Console.Error.WriteLine($"[PERF][HLE] managed_dispatch_avg={avgUs:F3}us total_managed_s={(double)total / System.Diagnostics.Stopwatch.Frequency:F2}");
}
}
private static readonly bool _perfHleNoDict =
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE_NODICT"), "1", System.StringComparison.Ordinal);
private static void RecordPerfHleCall(string name)
{
var total = System.Threading.Interlocked.Increment(ref _perfHleTotal);
if (!_perfHleNoDict)
{
_perfHleCounts.AddOrUpdate(name, 1, static (_, v) => v + 1);
}
if (total % 500000 == 0 && !_perfHleNoDict)
{
// Snapshot via foreach (a safe moving enumerator) before sorting.
// LINQ over a ConcurrentDictionary uses ICollection.CopyTo, which
// throws ArgumentException if another thread adds a key between the
// Count read and the copy — that exception was being swallowed into
// a CPU_TRAP return and crashing the guest.
var snapshot = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, long>>(_perfHleCounts.Count + 16);
foreach (var kvp in _perfHleCounts)
{
snapshot.Add(kvp);
}
var top = snapshot
.OrderByDescending(kvp => kvp.Value)
.Take(20)
.Select(kvp => $"{kvp.Key}={kvp.Value}");
System.Console.Error.WriteLine($"[PERF][HLE] total={total} top: {string.Join(", ", top)}");
}
}
private void RecordRecentImportTrace(
long dispatchIndex,
string nid,
@@ -25,101 +74,40 @@ public sealed partial class DirectExecutionBackend
ulong arg1,
ulong arg2)
{
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
var trace = _recentImportTrace;
trace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
dispatchIndex,
nid,
returnRip,
arg0,
arg1,
arg2);
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
if (_recentImportTraceCount < _recentImportTrace.Length)
arg2,
GuestThreadExecution.CurrentGuestThreadHandle,
Environment.CurrentManagedThreadId);
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % trace.Length;
if (_recentImportTraceCount < trace.Length)
{
_recentImportTraceCount++;
}
}
private void RecordDeferredBootstrapTrace(
long dispatchIndex,
ulong op,
ulong symbolPointer,
ulong outputPointer,
ulong returnRip)
{
lock (_deferredBootstrapTraceGate)
{
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
dispatchIndex,
op,
symbolPointer,
outputPointer,
returnRip);
_deferredBootstrapTraceWriteIndex =
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
{
_deferredBootstrapTraceCount++;
}
}
}
private void DrainDeferredBootstrapTraces()
{
if (!_logBootstrap)
{
return;
}
DeferredBootstrapTraceEntry[] pending;
lock (_deferredBootstrapTraceGate)
{
if (_deferredBootstrapTraceCount == 0)
{
return;
}
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
{
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
}
_deferredBootstrapTraceCount = 0;
}
foreach (var entry in pending)
{
var symbolText = "<unreadable>";
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
{
symbolText = sym;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
}
}
private void DumpRecentImportTrace()
{
if (_recentImportTraceCount == 0)
var trace = _recentImportTrace;
if (trace is null || _recentImportTraceCount == 0)
{
return;
}
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
Log.Info($" Recent import calls for managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ({_recentImportTraceCount}):");
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + trace.Length) % trace.Length;
for (int i = 0; i < _recentImportTraceCount; i++)
{
int num2 = (num + i) % _recentImportTrace.Length;
var entry = _recentImportTrace[num2];
int num2 = (num + i) % trace.Length;
var entry = trace[num2];
if (!string.IsNullOrEmpty(entry.Nid))
{
Log.Info(
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
$" #{entry.DispatchIndex} managed={entry.ManagedThreadId} guest=0x{entry.GuestThreadHandle:X16} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
}
}
@@ -135,9 +123,8 @@ public sealed partial class DirectExecutionBackend
int num2 = 0;
List<ulong> list = new List<ulong>(16);
ulong num3 = scanStart;
var hostMemory = ResolveDiagnosticsHostMemory();
HostRegionInfo lpBuffer;
while (num3 < scanEnd && hostMemory.Query(num3, out lpBuffer))
MEMORY_BASIC_INFORMATION64 lpBuffer;
while (num3 < scanEnd && VirtualQuery((void*)num3, out lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
{
ulong baseAddress = lpBuffer.BaseAddress;
ulong num4 = baseAddress + lpBuffer.RegionSize;
@@ -147,7 +134,7 @@ public sealed partial class DirectExecutionBackend
}
ulong value = Math.Max(num3, baseAddress);
ulong num5 = Math.Min(num4, scanEnd);
if (lpBuffer.State == HostRegionState.Committed && IsReadableProtection(lpBuffer.RawProtection) && !IsExecutableProtection(lpBuffer.RawProtection))
if (lpBuffer.State == 4096 && IsReadableProtection(lpBuffer.Protect) && !IsExecutableProtection(lpBuffer.Protect))
{
ulong num6 = AlignUp(value, 8uL);
for (ulong num7 = num6; num7 + 8 <= num5; num7 += 8)
@@ -186,6 +173,50 @@ public sealed partial class DirectExecutionBackend
{
return;
}
const int preludeSize = 192;
Span<byte> prelude = stackalloc byte[preludeSize];
if (returnRip >= preludeSize && cpuContext.Memory.TryRead(returnRip - preludeSize, prelude))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex} pre-return bytes @0x{returnRip - preludeSize:X16}: " +
BitConverter.ToString(prelude.ToArray()).Replace("-", " "));
List<DecodedInst>? bestCallChain = null;
var preludeAddress = returnRip - preludeSize;
for (var startOffset = 0; startOffset < preludeSize; startOffset++)
{
var cursor = preludeAddress + (ulong)startOffset;
var candidate = new List<DecodedInst>();
while (cursor < returnRip && candidate.Count < 96 &&
IcedDecoder.TryReadGuestBytes(cpuContext.Memory, cursor, 15, out var instructionBytes) &&
IcedDecoder.TryDecode(cursor, instructionBytes, out var instruction) &&
instruction.Length > 0 &&
cursor + (ulong)instruction.Length <= returnRip)
{
candidate.Add(instruction);
cursor += (ulong)instruction.Length;
}
if (cursor == returnRip &&
candidate.Count > 0 &&
string.Equals(candidate[^1].Mnemonic, "Call", StringComparison.OrdinalIgnoreCase) &&
(bestCallChain is null || candidate.Count > bestCallChain.Count))
{
bestCallChain = candidate;
}
}
if (bestCallChain is not null)
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} pre-return disassembly:");
foreach (var instruction in bestCallChain.TakeLast(32))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] 0x{instruction.Rip:X16}: {instruction.Text} " +
$"bytes={IcedDecoder.FormatBytes(instruction.Bytes)}");
}
}
}
Span<byte> destination = stackalloc byte[128];
if (!cpuContext.Memory.TryRead(returnRip, destination))
{
@@ -237,15 +268,14 @@ public sealed partial class DirectExecutionBackend
ulong callRip = returnRip + (ulong)i;
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
var importEntries = _importEntries;
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
{
if (importEntries[importIndex].Address != target)
if (_importEntries[importIndex].Address != target)
{
continue;
}
string nid = importEntries[importIndex].Nid;
string nid = _importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Log.Debug(
@@ -272,14 +302,14 @@ public sealed partial class DirectExecutionBackend
{
Log.Debug(
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
{
if (importEntries[importIndex].Address != slotTarget)
if (_importEntries[importIndex].Address != slotTarget)
{
continue;
}
string nid = importEntries[importIndex].Nid;
string nid = _importEntries[importIndex].Nid;
if (_moduleManager.TryGetExport(nid, out var export))
{
Log.Debug(
@@ -303,6 +333,28 @@ public sealed partial class DirectExecutionBackend
return value == 65534 || value == 4294967294u || value == 18446744073709551614uL;
}
private static ulong ParseOptionalHexAddress(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return 0;
}
var text = value.Trim();
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
text = text[2..];
}
return ulong.TryParse(
text,
System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture,
out var address)
? address
: 0;
}
private static bool IsPlausibleReturnAddress(ulong address)
{
return address >= 12884901888L && address < 17592186044416L && !IsUnresolvedSentinel(address);
@@ -352,7 +404,7 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
@@ -361,7 +413,7 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (lpBuffer.State != HostRegionState.Committed || !IsReadableProtection(lpBuffer.RawProtection))
if (lpBuffer.State != 4096 || !IsReadableProtection(lpBuffer.Protect))
{
return false;
}
@@ -393,12 +445,12 @@ public sealed partial class DirectExecutionBackend
return true;
}
if (!ResolveDiagnosticsHostMemory().Query(address, out var lpBuffer))
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
var executable = lpBuffer.State == HostRegionState.Committed && IsExecutableProtection(lpBuffer.RawProtection);
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
if (executable)
{
_knownExecutablePages.TryAdd(pageAddress, 0);
@@ -417,14 +469,6 @@ public sealed partial class DirectExecutionBackend
return (value + num) & ~num;
}
// Diagnostics helpers are static (reachable from static handler paths), so
// they use the platform injected into the backend active on this thread and
// fall back to the process-wide singleton only when no run is bound.
private static IHostMemory ResolveDiagnosticsHostMemory()
{
return _activeExecutionBackend?._hostMemory ?? HostPlatform.Current.Memory;
}
private static bool IsReadableProtection(uint protect)
{
if ((protect & 0x100) != 0 || (protect & 1) != 0)
@@ -9,9 +9,7 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Cpu.Native.Windows;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -19,6 +17,8 @@ public sealed partial class DirectExecutionBackend
{
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
private static int _lazyCommitTraceCount;
private static int _guestAllocatorHoleRecoveries;
private static int _auxiliaryThreadExecuteFaultRecoveries;
private unsafe void SetupExceptionHandler()
{
@@ -30,12 +30,12 @@ public sealed partial class DirectExecutionBackend
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
{
_rawExceptionHandlerStub = _faultHandling.CreateHandlerThunk(RawVectoredHandlerPtrManaged, _hostRspSlotTlsIndex, _tlsGetValueAddress);
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
if (_rawExceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
}
_rawExceptionHandler = _faultHandling.AddFirstChanceHandler(_rawExceptionHandlerStub);
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
}
else
@@ -45,22 +45,22 @@ public sealed partial class DirectExecutionBackend
_handlerDelegate = VectoredHandler;
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
_exceptionHandlerStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_handlerDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
if (_exceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create exception handler trampoline");
}
_exceptionHandler = _faultHandling.AddFirstChanceHandler(_exceptionHandlerStub);
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
_unhandledFilterStub = _faultHandling.CreateHandlerThunk(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate), _hostRspSlotTlsIndex, _tlsGetValueAddress);
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
if (_unhandledFilterStub == 0)
{
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
}
_faultHandling.SetUnhandledFilter(_unhandledFilterStub);
SetUnhandledExceptionFilter(_unhandledFilterStub);
}
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
@@ -68,8 +68,8 @@ public sealed partial class DirectExecutionBackend
try
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RIP);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, CTX_RSP);
ulong rip = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 248);
ulong rsp = ReadCtxU64(((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord, 152);
Console.Error.WriteLine("[LOADER][FATAL] Unhandled exception filter fired.");
Console.Error.WriteLine($"[LOADER][FATAL] Code: 0x{exceptionRecord->ExceptionCode:X8}");
Console.Error.WriteLine($"[LOADER][FATAL] Exception Address: 0x{(ulong)(nint)exceptionRecord->ExceptionAddress:X16}");
@@ -108,19 +108,28 @@ public sealed partial class DirectExecutionBackend
return 0;
}
ulong rip = ReadCtxU64(contextRecord, CTX_RIP);
ulong rsp = ReadCtxU64(contextRecord, CTX_RSP);
// Thread-mode probe: a hardware exception raised while this thread is inside
// the managed import gateway means the VEH->managed reentry happened from
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
if (LogThreadMode && _threadModeGatewayDepth > 0)
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
if (TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
{
TraceThreadMode(
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
return -1;
}
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
{
return -1;
}
if (exceptionCode == WindowsFaultCodes.AccessViolation && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
}
if (exceptionCode == 3221225477u &&
TryRecoverGuestAllocatorHole(exceptionRecord, contextRecord, rip))
{
return -1;
}
if (exceptionCode == StatusIllegalInstruction &&
TryRecoverIllegalInstruction(contextRecord, rip))
{
return -1;
}
@@ -135,10 +144,10 @@ public sealed partial class DirectExecutionBackend
switch (exceptionCode)
{
case WindowsFaultCodes.AccessViolation:
case 3221225477u:
LogAccessViolationTrace(exceptionAddress, exceptionRecord);
break;
case WindowsFaultCodes.FastFail:
case 3221226505u:
{
ulong p0 = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
ulong p1 = exceptionRecord->NumberParameters >= 2 ? exceptionRecord->ExceptionInformation[1] : 0;
@@ -148,27 +157,56 @@ public sealed partial class DirectExecutionBackend
}
}
ulong rax = ReadCtxU64(contextRecord, CTX_RAX);
ulong rbx = ReadCtxU64(contextRecord, CTX_RBX);
ulong rcx = ReadCtxU64(contextRecord, CTX_RCX);
ulong rdx = ReadCtxU64(contextRecord, CTX_RDX);
ulong rsi = ReadCtxU64(contextRecord, CTX_RSI);
ulong rdi = ReadCtxU64(contextRecord, CTX_RDI);
ulong rbp = ReadCtxU64(contextRecord, CTX_RBP);
ulong r8 = ReadCtxU64(contextRecord, CTX_R8);
ulong r9 = ReadCtxU64(contextRecord, CTX_R9);
ulong r10 = ReadCtxU64(contextRecord, CTX_R10);
ulong r11 = ReadCtxU64(contextRecord, CTX_R11);
ulong r12 = ReadCtxU64(contextRecord, CTX_R12);
ulong r13 = ReadCtxU64(contextRecord, CTX_R13);
ulong r14 = ReadCtxU64(contextRecord, CTX_R14);
ulong r15 = ReadCtxU64(contextRecord, CTX_R15);
ulong rax = ReadCtxU64(contextRecord, 120);
ulong rbx = ReadCtxU64(contextRecord, 144);
ulong rcx = ReadCtxU64(contextRecord, 128);
ulong rdx = ReadCtxU64(contextRecord, 136);
ulong rsi = ReadCtxU64(contextRecord, 168);
ulong rdi = ReadCtxU64(contextRecord, 176);
ulong rbp = ReadCtxU64(contextRecord, 160);
ulong r8 = ReadCtxU64(contextRecord, 184);
ulong r9 = ReadCtxU64(contextRecord, 192);
ulong r10 = ReadCtxU64(contextRecord, 200);
ulong r11 = ReadCtxU64(contextRecord, 208);
ulong r12 = ReadCtxU64(contextRecord, 216);
ulong r13 = ReadCtxU64(contextRecord, 224);
ulong r14 = ReadCtxU64(contextRecord, 232);
ulong r15 = ReadCtxU64(contextRecord, 240);
Console.Error.WriteLine("[LOADER][INFO] =========================================");
Console.Error.WriteLine("[LOADER][INFO] NATIVE EXCEPTION CAUGHT!");
Console.Error.WriteLine($"[LOADER][INFO] Code: 0x{exceptionCode:X8}");
Console.Error.WriteLine($"[LOADER][INFO] Exception Address: 0x{exceptionAddress:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RIP: 0x{rip:X16}");
Console.Error.WriteLine(
$"[LOADER][INFO] Host thread: managed={Environment.CurrentManagedThreadId} " +
$"name='{Thread.CurrentThread.Name ?? "<unnamed>"}'");
if (_activeGuestThreadState is { } activeGuestThread)
{
Console.Error.WriteLine(
$"[LOADER][INFO] Guest thread: handle=0x{activeGuestThread.ThreadHandle:X16} " +
$"name='{activeGuestThread.Name}' state={activeGuestThread.State} " +
$"last_import={activeGuestThread.LastImportNid ?? "<none>"} " +
$"last_ret=0x{activeGuestThread.LastReturnRip:X16}");
Console.Error.WriteLine(
$"[LOADER][INFO] Last import registers: " +
$"rax=0x{Volatile.Read(ref activeGuestThread.LastImportRax):X16} " +
$"result_valid={Volatile.Read(ref activeGuestThread.LastImportResultValid) != 0} " +
$"rdi=0x{activeGuestThread.LastImportRdi:X16} " +
$"rsi=0x{activeGuestThread.LastImportRsi:X16} " +
$"rdx=0x{activeGuestThread.LastImportRdx:X16} " +
$"rcx=0x{activeGuestThread.LastImportRcx:X16} " +
$"r8=0x{activeGuestThread.LastImportR8:X16} " +
$"r9=0x{activeGuestThread.LastImportR9:X16}");
Console.Error.WriteLine(
$"[LOADER][INFO] Last import stack args: " +
$"0=0x{activeGuestThread.LastImportStack0:X16} " +
$"1=0x{activeGuestThread.LastImportStack1:X16} " +
$"2=0x{activeGuestThread.LastImportStack2:X16} " +
$"3=0x{activeGuestThread.LastImportStack3:X16} " +
$"4=0x{activeGuestThread.LastImportStack4:X16} " +
$"5=0x{activeGuestThread.LastImportStack5:X16}");
}
if (TryFormatNearestRuntimeSymbol(rip, out string symbol))
{
Console.Error.WriteLine("[LOADER][INFO] RIP symbol: " + symbol);
@@ -193,7 +231,7 @@ public sealed partial class DirectExecutionBackend
ulong accessType = 0;
ulong target = 0;
if (exceptionCode == WindowsFaultCodes.AccessViolation && exceptionRecord->NumberParameters >= 2)
if (exceptionCode == 3221225477u && exceptionRecord->NumberParameters >= 2)
{
accessType = *exceptionRecord->ExceptionInformation;
target = exceptionRecord->ExceptionInformation[1];
@@ -206,9 +244,9 @@ public sealed partial class DirectExecutionBackend
};
Console.Error.WriteLine("[LOADER][INFO] AV access: " + accessText);
Console.Error.WriteLine($"[LOADER][INFO] AV target: 0x{target:X16}");
if (_hostMemory.Query(target, out var mbi))
if (VirtualQuery((void*)target, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) != 0)
{
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}");
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}");
}
}
@@ -225,13 +263,46 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][INFO] [rsp+0x{i * 8:X2}] @0x{stackAddr:X16} = 0x{value:X16}");
}
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_FAULT_STACK_WINDOW"),
"1",
StringComparison.Ordinal))
{
Console.Error.WriteLine("[LOADER][INFO] Full fault stack window (RSP-0x300..RSP+0x100):");
var windowStart = rsp >= 0x300 ? rsp - 0x300 : 0;
for (var stackAddr = windowStart; stackAddr < rsp + 0x100; stackAddr += 8)
{
if (!TryReadHostQword(stackAddr, out var value))
{
continue;
}
var relative = unchecked((long)(stackAddr - rsp));
var relativeText = relative >= 0
? $"+0x{relative:X}"
: $"-0x{-relative:X}";
var symbolText = TryFormatNearestRuntimeSymbol(value, out var stackSymbol)
? $" [{stackSymbol}]"
: string.Empty;
Console.Error.WriteLine(
$"[LOADER][INFO] [rsp{relativeText}] " +
$"@0x{stackAddr:X16} = 0x{value:X16}{symbolText}");
}
}
DumpPointerWindow("fault-register-rbx", rbx, 0x60);
DumpPointerWindow("fault-register-rsi", rsi, 0x60);
DumpPointerWindow("fault-register-rdi", rdi, 0x60);
DumpPointerWindow("fault-register-r13", r13, 0x60);
DumpPointerWindow("fault-register-r14", r14, 0x60);
try
{
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
ulong frame = rbp;
for (int i = 0; i < 12; i++)
{
if (frame < 140733193388032L || frame > 140737488355327L)
if (frame < 0x10000)
{
break;
}
@@ -256,7 +327,7 @@ public sealed partial class DirectExecutionBackend
switch (exceptionCode)
{
case WindowsFaultCodes.AccessViolation:
case 3221225477u:
Console.Error.WriteLine("[LOADER][ERROR] Type: Access Violation");
Console.Error.WriteLine("[LOADER][ERROR] This usually means:");
Console.Error.WriteLine("[LOADER][ERROR] - Guest code called an unmapped import");
@@ -290,22 +361,57 @@ public sealed partial class DirectExecutionBackend
{
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
}
for (var stackIndex = 0; stackIndex < 16; stackIndex++)
{
byte[] stackSlot = new byte[8];
if (!TryReadHostBytes(rsp + (ulong)(stackIndex * 8), stackSlot))
{
continue;
}
var candidate = BitConverter.ToUInt64(stackSlot);
if (candidate < _entryPoint || candidate >= _entryPoint + 0x10000000 || candidate < 24)
{
continue;
}
byte[] callSiteWindow = new byte[48];
if (TryReadHostBytes(candidate - 24, callSiteWindow))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
BitConverter.ToString(callSiteWindow).Replace("-", " "));
}
}
}
else
{
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
}
DumpRecentImportTrace();
DumpGuestDisasmDiagnostics(rip, rbp);
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
DumpGuestRegisterWindowDiagnostics(
rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp,
r8, r9, r10, r11, r12, r13, r14, r15);
DumpGuestReferenceDiagnostics();
DumpGuestPointerWindowDiagnostics();
break;
case WindowsFaultCodes.Breakpoint:
case 2147483651u:
Console.Error.WriteLine("[LOADER][WARNING] Type: Breakpoint (int3)");
Console.Error.WriteLine("[LOADER][WARNING] Unexpected breakpoint in direct-bridge mode");
break;
case WindowsFaultCodes.IllegalInstruction:
case 1073741845u:
Console.Error.WriteLine("[LOADER][ERROR] Type: Abort (SIGABRT)");
DumpRecentImportTrace();
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
break;
case 3221225501u:
Console.Error.WriteLine("[LOADER][INFO] Type: Illegal Instruction");
byte[] illegalCode = new byte[16];
if (TryReadHostBytes(rip, illegalCode))
{
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(illegalCode).Replace("-", " "));
}
DumpRecentImportTrace();
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
break;
}
@@ -319,6 +425,108 @@ public sealed partial class DirectExecutionBackend
}
}
private unsafe bool TryRecoverAuxiliaryThreadExecuteFault(
EXCEPTION_RECORD* exceptionRecord,
void* contextRecord,
ulong rip)
{
if (exceptionRecord->ExceptionCode != 3221225477u ||
rip >= 0x0000000800000000UL ||
_activeGuestThreadState is not { Name: "tbb_thead" } activeThread)
{
return false;
}
var hostExit = ActiveEntryReturnSentinelRip;
if (hostExit < 0x10000)
{
hostExit = unchecked((ulong)_guestReturnStub);
}
if (hostExit < 0x10000)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Could not recover auxiliary TBB execute fault: target=0x{rip:X16} " +
$"active_exit=0x{ActiveEntryReturnSentinelRip:X16} guest_return_stub=0x{unchecked((ulong)_guestReturnStub):X16}");
return false;
}
_ = TryPatchActiveGuestReturnSlot(hostExit);
WriteCtxU64(contextRecord, 120, 0);
WriteCtxU64(contextRecord, 248, hostExit);
var recovery = Interlocked.Increment(ref _auxiliaryThreadExecuteFaultRecoveries);
Console.Error.WriteLine(
$"[LOADER][WARN] Recovered auxiliary TBB execute fault #{recovery}: " +
$"thread=0x{activeThread.ThreadHandle:X16} target=0x{rip:X16} -> host_exit=0x{hostExit:X16}");
return true;
}
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
{
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
{
return false;
}
byte[] opcode = new byte[2];
if (!TryReadHostBytes(rip, opcode) || opcode[0] != 0xCD || opcode[1] != 0x41)
{
return false;
}
var count = Interlocked.Increment(ref _ignoredGuestInt41Count);
WriteCtxU64(contextRecord, 248, rip + 2);
if (count <= 16 || count % 65536 == 0)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Ignored guest int 0x41 trap #{count} at 0x{rip:X16} (SHARPEMU_IGNORE_INT41=1)");
Console.Error.Flush();
}
return true;
}
private unsafe static bool TryRecoverGuestAllocatorHole(
EXCEPTION_RECORD* exceptionRecord,
void* contextRecord,
ulong rip)
{
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_GUEST_ALLOCATOR_HOLE_RECOVERY"),
"1",
StringComparison.Ordinal) ||
exceptionRecord->NumberParameters < 2 ||
exceptionRecord->ExceptionInformation[0] != 0 ||
exceptionRecord->ExceptionInformation[1] != 8 ||
ReadCtxU64(contextRecord, CTX_RDI) != 0 ||
rip < 0x10000)
{
return false;
}
// Demon's Souls occasionally leaves an empty payload in a locked pool
// tree node. The allocator dereferences payload+8 before reaching its
// existing empty-pool fallback. Match the instruction stream instead of
// a title-specific absolute address, then resume at that fallback so the
// lock is released and the allocator can try its next backing pool.
const ulong allocatorHoleSignature = 0x634CFF568D08778BUL;
if (*(ulong*)rip != allocatorHoleSignature || *((byte*)rip + 8) != 0xF2)
{
return false;
}
const ulong emptyPoolFallbackDelta = 0x8E;
WriteCtxU64(contextRecord, CTX_RIP, rip + emptyPoolFallbackDelta);
var recovery = Interlocked.Increment(ref _guestAllocatorHoleRecoveries);
if (recovery <= 16 || (recovery & (recovery - 1)) == 0)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Guest allocator empty-node adapter recovery #{recovery}: " +
$"rip=0x{rip:X16} -> 0x{rip + emptyPoolFallbackDelta:X16}");
Console.Error.Flush();
}
return true;
}
private static bool IsBenignHostDebugException(uint exceptionCode)
{
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
@@ -337,8 +545,8 @@ public sealed partial class DirectExecutionBackend
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
void* contextRecord = pointers->ContextRecord;
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RIP) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, CTX_RSP) : 0;
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
Console.Error.WriteLine(
@@ -398,7 +606,7 @@ public sealed partial class DirectExecutionBackend
}
}
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp)
private void DumpGuestDisasmDiagnostics(ulong rip, ulong rbp, ulong rsp)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISASM"), "1", StringComparison.Ordinal))
{
@@ -410,12 +618,20 @@ public sealed partial class DirectExecutionBackend
DumpGuestInstructionStream("fault-prelude", rip - 0x20, 24);
}
// Optimized guest code frequently omits frame pointers. The return
// address at RSP is then more useful than an RBP walk and identifies the
// exact call site that supplied the faulting arguments.
if (TryReadHostQword(rsp, out var stackReturn) && stackReturn >= 0x60)
{
DumpGuestInstructionStream("stack-return-prelude", stackReturn - 0x60, 40);
}
try
{
ulong frame = rbp;
for (int i = 0; i < 3; i++)
{
if (frame < 140733193388032L || frame > 140737488355327L)
if (frame < 0x10000)
{
break;
}
@@ -484,7 +700,7 @@ public sealed partial class DirectExecutionBackend
ulong address = scanBase;
while (address < scanEnd)
{
if (!_hostMemory.Query(address, out var mbi))
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
break;
}
@@ -496,9 +712,9 @@ public sealed partial class DirectExecutionBackend
break;
}
if (mbi.State == HostRegionState.Committed &&
IsReadableProtection(mbi.RawProtection) &&
IsExecutableProtection(mbi.RawProtection))
if (mbi.State == MEM_COMMIT &&
IsReadableProtection(mbi.Protect) &&
IsExecutableProtection(mbi.Protect))
{
ScanExecutableRegionForTargetReferences(regionBase, regionEnd, targetList, hitCounts, maxHitsPerTarget);
}
@@ -559,6 +775,55 @@ public sealed partial class DirectExecutionBackend
}
}
private void DumpGuestRegisterWindowDiagnostics(
ulong rax,
ulong rbx,
ulong rcx,
ulong rdx,
ulong rsi,
ulong rdi,
ulong rbp,
ulong rsp,
ulong r8,
ulong r9,
ulong r10,
ulong r11,
ulong r12,
ulong r13,
ulong r14,
ulong r15)
{
if (!string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_REGISTER_WINDOWS"),
"1",
StringComparison.Ordinal))
{
return;
}
// A register can be the only surviving reference to the object or
// argument array that caused a native guest fault. Capture a compact
// window while the process is alive so the post-mortem log can
// distinguish an absent object from a partially initialized one.
var registers = new (string Name, ulong Value)[]
{
("rax", rax), ("rbx", rbx), ("rcx", rcx), ("rdx", rdx),
("rsi", rsi), ("rdi", rdi), ("rbp", rbp), ("rsp", rsp),
("r8", r8), ("r9", r9), ("r10", r10), ("r11", r11),
("r12", r12), ("r13", r13), ("r14", r14), ("r15", r15),
};
var seen = new HashSet<ulong>();
foreach (var (name, value) in registers)
{
if (value < 0x10000 || !seen.Add(value))
{
continue;
}
DumpPointerWindow($"register-{name}", value, 0x80);
}
}
private void ScanExecutableRegionForTargetReferences(
ulong regionBase,
ulong regionEnd,
@@ -803,13 +1068,13 @@ public sealed partial class DirectExecutionBackend
return false;
}
if (!_hostMemory.Query(address, out var mbi))
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong regionEnd = mbi.BaseAddress + mbi.RegionSize;
if (mbi.State != HostRegionState.Committed || !IsReadableProtection(mbi.RawProtection) || regionEnd <= address || address > regionEnd - 8)
if (mbi.State != MEM_COMMIT || !IsReadableProtection(mbi.Protect) || regionEnd <= address || address > regionEnd - 8)
{
return false;
}
@@ -848,7 +1113,7 @@ public sealed partial class DirectExecutionBackend
}
}
private unsafe bool TryReadHostBytes(ulong address, byte[] buffer)
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
{
if (address < 65536)
{
@@ -861,9 +1126,9 @@ public sealed partial class DirectExecutionBackend
ulong end = address + (ulong)buffer.Length;
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
{
if (!_hostMemory.Query(page, out var mbi) ||
mbi.State != HostRegionState.Committed ||
!IsReadableProtection(mbi.RawProtection))
if (VirtualQuery((void*)page, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
mbi.State != MEM_COMMIT ||
!IsReadableProtection(mbi.Protect))
{
return false;
}
@@ -976,25 +1241,25 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
if (!_hostMemory.Query(faultAddress, out var mbi))
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.RawAllocationProtection);
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.RawState:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.RawAllocationProtection:X08} prot=0x{mbi.RawProtection: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.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
}
if (mbi.State == HostRegionState.Committed && IsAccessCompatible(accessType, mbi.RawProtection))
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
{
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.RawProtection:X08}");
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
}
return true;
}
@@ -1003,10 +1268,10 @@ public sealed partial class DirectExecutionBackend
ulong committedBase = 0;
ulong committedSize = 0;
if (mbi.State == HostRegionState.Free)
if (mbi.State == 65536)
{
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
TryReserveThenCommit(_hostMemory, windowBase, windowSize, windowBase, windowSize, commitProtect))
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
{
committed = true;
committedBase = windowBase;
@@ -1015,7 +1280,7 @@ public sealed partial class DirectExecutionBackend
else
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(_hostMemory, largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
@@ -1026,13 +1291,13 @@ public sealed partial class DirectExecutionBackend
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(_hostMemory, region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryReserveThenCommit(_hostMemory, pageBase, 4096uL, pageBase, 4096uL, commitProtect))
else if (TryReserveThenCommit(pageBase, 4096uL, pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
@@ -1045,7 +1310,7 @@ public sealed partial class DirectExecutionBackend
return false;
}
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
@@ -1053,13 +1318,13 @@ public sealed partial class DirectExecutionBackend
return true;
}
if (mbi.State != HostRegionState.Reserved)
if (mbi.State != 8192)
{
return false;
}
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
TryCommitRange(_hostMemory, commitWindowBase, commitWindowSize, commitProtect))
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
{
committed = true;
committedBase = commitWindowBase;
@@ -1068,7 +1333,7 @@ public sealed partial class DirectExecutionBackend
else
{
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(_hostMemory, largeCommitBase, 2097152uL, commitProtect))
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
@@ -1079,19 +1344,19 @@ public sealed partial class DirectExecutionBackend
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(_hostMemory, region64kBase, 65536uL, commitProtect))
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
{
committed = true;
committedBase = region64kBase;
committedSize = 65536uL;
}
else if (TryCommitRange(_hostMemory, pageBase, 8192uL, commitProtect))
else if (TryCommitRange(pageBase, 8192uL, commitProtect))
{
committed = true;
committedBase = pageBase;
committedSize = 8192uL;
}
else if (TryCommitRange(_hostMemory, pageBase, 4096uL, commitProtect))
else if (TryCommitRange(pageBase, 4096uL, commitProtect))
{
committed = true;
committedBase = pageBase;
@@ -1104,7 +1369,7 @@ public sealed partial class DirectExecutionBackend
return false;
}
TryCommitRange(_hostMemory, pageBase + 4096, 4096uL, commitProtect);
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
@@ -1145,33 +1410,31 @@ public sealed partial class DirectExecutionBackend
return true;
}
// 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)
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
{
if (length == 0)
{
return false;
}
return hostMemory.Commit(baseAddress, length, protection == 64u ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite);
return VirtualAlloc((void*)baseAddress, (nuint)length, 4096u, protection) != null;
}
static bool TryReserveRange(IHostMemory hostMemory, ulong baseAddress, ulong length)
static unsafe bool TryReserveRange(ulong baseAddress, ulong length)
{
if (length == 0)
{
return false;
}
return hostMemory.Reserve(baseAddress, length, HostPageProtection.ReadWrite) != 0;
return VirtualAlloc((void*)baseAddress, (nuint)length, 8192u, 4u) != null;
}
static bool TryReserveThenCommit(IHostMemory hostMemory, ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
static bool TryReserveThenCommit(ulong reserveAddress, ulong reserveSize, ulong commitAddress, ulong commitSize, uint protection)
{
if (!TryReserveRange(hostMemory, reserveAddress, reserveSize))
if (!TryReserveRange(reserveAddress, reserveSize))
{
return false;
}
return TryCommitRange(hostMemory, commitAddress, commitSize, protection);
return TryCommitRange(commitAddress, commitSize, protection);
}
static bool IsAccessCompatible(ulong accessType, uint protection)
@@ -0,0 +1,362 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Threading;
using Iced.Intel;
using SharpEmu.Core.Cpu.Emulation;
namespace SharpEmu.Core.Cpu.Native;
// Software fallback for the BMI1/BMI2/ABM general-purpose-register instructions.
//
// Guest code runs natively, so the guest and host share the same virtual address space and the
// same registers (the OS delivers them in the CONTEXT record on a fault). When the host CPU lacks
// one of these extensions it raises #UD instead of executing the opcode; without this the title
// simply aborts. Here we decode the faulting instruction, evaluate it against the trapped register
// and memory state, write the result back into the CONTEXT, step RIP past the instruction and ask
// the OS to continue. Only the register-only BMI/ABM forms are handled; anything else returns false
// and falls through to the existing diagnostics unchanged, so this can never mis-handle an opcode it
// does not fully model.
public sealed partial class DirectExecutionBackend
{
// Windows x64 CONTEXT.EFlags lives just past the segment selectors. The GPR offsets it shares
// with the rest of the backend are the CTX_* constants declared in DirectExecutionBackend.cs.
private const int CTX_EFLAGS = 68;
// STATUS_ILLEGAL_INSTRUCTION (#UD surfaced by the Windows vectored handler).
private const uint StatusIllegalInstruction = 0xC000001Du;
private const int MaxInstructionBytes = 15;
// Instruction-window sizes tried in turn so a fault near a page boundary still decodes.
private static readonly int[] DecodeWindowSizes = { MaxInstructionBytes, 11, 8, 4, 2 };
private static int _bmiSoftwareFallbackAnnounced;
private static long _bmiInstructionsEmulated;
private unsafe bool TryRecoverIllegalInstruction(void* contextRecord, ulong rip)
{
if (!TryReadFaultingInstruction(rip, out var instruction))
{
return false;
}
if (instruction.Op0Kind != OpKind.Register ||
!TryGetGprSlot(instruction.Op0Register, out var destOffset, out var size))
{
return false;
}
if (!TryEvaluate(contextRecord, in instruction, size, out var result, out var flagsChanged, out var eflags))
{
return false;
}
WriteCtxU64(contextRecord, destOffset, result);
if (flagsChanged)
{
WriteCtxU32(contextRecord, CTX_EFLAGS, eflags);
}
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
Interlocked.Increment(ref _bmiInstructionsEmulated);
if (Interlocked.Exchange(ref _bmiSoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks a BMI/ABM extension used by the guest; " +
"emulating those instructions in software.");
}
return true;
}
private unsafe bool TryEvaluate(
void* contextRecord,
in Instruction instruction,
GprOperandSize size,
out ulong result,
out bool flagsChanged,
out uint eflags)
{
result = 0;
flagsChanged = false;
eflags = ReadCtxU32(contextRecord, CTX_EFLAGS);
switch (instruction.Mnemonic)
{
case Mnemonic.Andn:
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var andnSrc1) ||
!TryReadOperand(contextRecord, in instruction, 2, size, out var andnSrc2))
{
return false;
}
result = BmiInstructionEmulator.Andn(andnSrc1, andnSrc2, size, ref eflags);
flagsChanged = true;
return true;
case Mnemonic.Blsi:
case Mnemonic.Blsmsk:
case Mnemonic.Blsr:
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var blsSrc))
{
return false;
}
result = instruction.Mnemonic switch
{
Mnemonic.Blsi => BmiInstructionEmulator.Blsi(blsSrc, size, ref eflags),
Mnemonic.Blsmsk => BmiInstructionEmulator.Blsmsk(blsSrc, size, ref eflags),
_ => BmiInstructionEmulator.Blsr(blsSrc, size, ref eflags),
};
flagsChanged = true;
return true;
case Mnemonic.Bextr:
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bextrSrc) ||
!TryReadOperand(contextRecord, in instruction, 2, size, out var bextrControl))
{
return false;
}
result = BmiInstructionEmulator.Bextr(bextrSrc, bextrControl, size, ref eflags);
flagsChanged = true;
return true;
case Mnemonic.Bzhi:
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var bzhiSrc) ||
!TryReadOperand(contextRecord, in instruction, 2, size, out var bzhiIndex))
{
return false;
}
result = BmiInstructionEmulator.Bzhi(bzhiSrc, bzhiIndex, size, ref eflags);
flagsChanged = true;
return true;
case Mnemonic.Tzcnt:
case Mnemonic.Lzcnt:
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var cntSrc))
{
return false;
}
result = instruction.Mnemonic == Mnemonic.Tzcnt
? BmiInstructionEmulator.Tzcnt(cntSrc, size, ref eflags)
: BmiInstructionEmulator.Lzcnt(cntSrc, size, ref eflags);
flagsChanged = true;
return true;
case Mnemonic.Rorx:
if (instruction.Op2Kind != OpKind.Immediate8 ||
!TryReadOperand(contextRecord, in instruction, 1, size, out var rorxSrc))
{
return false;
}
result = BmiInstructionEmulator.Rorx(rorxSrc, instruction.Immediate8, size);
return true;
case Mnemonic.Sarx:
case Mnemonic.Shlx:
case Mnemonic.Shrx:
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var shiftSrc) ||
!TryReadOperand(contextRecord, in instruction, 2, size, out var shiftCount))
{
return false;
}
result = instruction.Mnemonic switch
{
Mnemonic.Sarx => BmiInstructionEmulator.Sarx(shiftSrc, (int)shiftCount, size),
Mnemonic.Shlx => BmiInstructionEmulator.Shlx(shiftSrc, (int)shiftCount, size),
_ => BmiInstructionEmulator.Shrx(shiftSrc, (int)shiftCount, size),
};
return true;
case Mnemonic.Pdep:
case Mnemonic.Pext:
if (!TryReadOperand(contextRecord, in instruction, 1, size, out var packSrc) ||
!TryReadOperand(contextRecord, in instruction, 2, size, out var packMask))
{
return false;
}
result = instruction.Mnemonic == Mnemonic.Pdep
? BmiInstructionEmulator.Pdep(packSrc, packMask, size)
: BmiInstructionEmulator.Pext(packSrc, packMask, size);
return true;
default:
return false;
}
}
private unsafe bool TryReadFaultingInstruction(ulong rip, out Instruction instruction)
{
// Try the full instruction window first, then shrink so a fault near the end of a mapped
// page (where fewer than 15 bytes are readable) still decodes.
foreach (var attempt in DecodeWindowSizes)
{
var buffer = new byte[attempt];
if (!TryReadHostBytes(rip, buffer))
{
continue;
}
var decoder = Decoder.Create(64, new ByteArrayCodeReader(buffer));
decoder.IP = rip;
decoder.Decode(out instruction);
if (instruction.Code != Code.INVALID && instruction.Length > 0 && instruction.Length <= attempt)
{
return true;
}
}
instruction = default;
return false;
}
private unsafe bool TryReadOperand(
void* contextRecord,
in Instruction instruction,
int operandIndex,
GprOperandSize size,
out ulong value)
{
value = 0;
switch (instruction.GetOpKind(operandIndex))
{
case OpKind.Register:
if (!TryGetGprSlot(instruction.GetOpRegister(operandIndex), out var offset, out _))
{
return false;
}
var raw = ReadCtxU64(contextRecord, offset);
value = size == GprOperandSize.Bits64 ? raw : raw & 0xFFFF_FFFFUL;
return true;
case OpKind.Memory:
if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address))
{
return false;
}
var byteCount = size == GprOperandSize.Bits64 ? 8 : 4;
var buffer = new byte[byteCount];
if (!TryReadHostBytes(address, buffer))
{
return false;
}
value = byteCount == 8
? BinaryPrimitives.ReadUInt64LittleEndian(buffer)
: BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
default:
return false;
}
}
private unsafe bool TryComputeMemoryAddress(void* contextRecord, in Instruction instruction, out ulong address)
{
address = 0;
// FS/GS-relative operands need the guest segment base, which is not modelled here.
if (instruction.SegmentPrefix != Register.None)
{
return false;
}
if (instruction.IsIPRelativeMemoryOperand)
{
address = instruction.IPRelativeMemoryAddress;
return true;
}
var effective = instruction.MemoryDisplacement64;
if (instruction.MemoryBase != Register.None)
{
if (!TryGetGpr64Offset(instruction.MemoryBase, out var baseOffset))
{
return false;
}
effective += ReadCtxU64(contextRecord, baseOffset);
}
if (instruction.MemoryIndex != Register.None)
{
if (!TryGetGpr64Offset(instruction.MemoryIndex, out var indexOffset))
{
return false;
}
effective += ReadCtxU64(contextRecord, indexOffset) * (ulong)instruction.MemoryIndexScale;
}
address = effective;
return true;
}
// Maps a 32- or 64-bit GPR to its CONTEXT offset and reports the operand width it implies.
private static bool TryGetGprSlot(Register register, out int offset, out GprOperandSize size)
{
switch (register)
{
case Register.EAX: offset = CTX_RAX; size = GprOperandSize.Bits32; return true;
case Register.ECX: offset = CTX_RCX; size = GprOperandSize.Bits32; return true;
case Register.EDX: offset = CTX_RDX; size = GprOperandSize.Bits32; return true;
case Register.EBX: offset = CTX_RBX; size = GprOperandSize.Bits32; return true;
case Register.ESP: offset = CTX_RSP; size = GprOperandSize.Bits32; return true;
case Register.EBP: offset = CTX_RBP; size = GprOperandSize.Bits32; return true;
case Register.ESI: offset = CTX_RSI; size = GprOperandSize.Bits32; return true;
case Register.EDI: offset = CTX_RDI; size = GprOperandSize.Bits32; return true;
case Register.R8D: offset = CTX_R8; size = GprOperandSize.Bits32; return true;
case Register.R9D: offset = CTX_R9; size = GprOperandSize.Bits32; return true;
case Register.R10D: offset = CTX_R10; size = GprOperandSize.Bits32; return true;
case Register.R11D: offset = CTX_R11; size = GprOperandSize.Bits32; return true;
case Register.R12D: offset = CTX_R12; size = GprOperandSize.Bits32; return true;
case Register.R13D: offset = CTX_R13; size = GprOperandSize.Bits32; return true;
case Register.R14D: offset = CTX_R14; size = GprOperandSize.Bits32; return true;
case Register.R15D: offset = CTX_R15; size = GprOperandSize.Bits32; return true;
default:
if (TryGetGpr64Offset(register, out offset))
{
size = GprOperandSize.Bits64;
return true;
}
size = GprOperandSize.Bits32;
return false;
}
}
private static bool TryGetGpr64Offset(Register register, out int offset)
{
switch (register)
{
case Register.RAX: offset = CTX_RAX; return true;
case Register.RCX: offset = CTX_RCX; return true;
case Register.RDX: offset = CTX_RDX; return true;
case Register.RBX: offset = CTX_RBX; return true;
case Register.RSP: offset = CTX_RSP; return true;
case Register.RBP: offset = CTX_RBP; return true;
case Register.RSI: offset = CTX_RSI; return true;
case Register.RDI: offset = CTX_RDI; return true;
case Register.R8: offset = CTX_R8; return true;
case Register.R9: offset = CTX_R9; return true;
case Register.R10: offset = CTX_R10; return true;
case Register.R11: offset = CTX_R11; return true;
case Register.R12: offset = CTX_R12; return true;
case Register.R13: offset = CTX_R13; return true;
case Register.R14: offset = CTX_R14; return true;
case Register.R15: offset = CTX_R15; return true;
default: offset = 0; return false;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -6,8 +6,6 @@ using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.HLE.Host.Posix;
namespace SharpEmu.Core.Cpu.Native;
@@ -37,6 +35,20 @@ public sealed partial class DirectExecutionBackend
private bool _nativeWorkersDisposed;
private int _nativeWorkerCreationFailedLogged;
private const uint StackSizeParamIsAReservation = 0x00010000u;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint CreateThread(
nint lpThreadAttributes,
nuint dwStackSize,
nint lpStartAddress,
nint lpParameter,
uint dwCreationFlags,
out uint lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
// thread; falls back to the historical inline calli (guest frames above this
// thread's managed frames) when workers are disabled or unavailable.
@@ -49,7 +61,7 @@ public sealed partial class DirectExecutionBackend
var worker = RentNativeGuestExecutor();
if (worker is null)
{
_hostThreading.SetTlsValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
return CallNativeEntry(entryStub);
}
try
@@ -80,7 +92,10 @@ public sealed partial class DirectExecutionBackend
private NativeGuestExecutor? RentNativeGuestExecutor()
{
if (NativeGuestWorkersDisabled)
// NativeGuestExecutor emits a Win32 wait loop and creates it with
// kernel32!CreateThread. POSIX hosts use the established inline entry
// path until the worker loop has a pthread/eventfd implementation.
if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled)
{
return null;
}
@@ -234,7 +249,7 @@ public sealed partial class DirectExecutionBackend
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
{
if (!EnsureHostRuntimeExports(backend._hostSymbols))
if (!EnsureKernel32Exports())
{
return null;
}
@@ -247,27 +262,32 @@ public sealed partial class DirectExecutionBackend
return executor;
}
private static bool EnsureHostRuntimeExports(IHostSymbolResolver symbols)
private static bool EnsureKernel32Exports()
{
if (_exitThreadAddress != 0)
{
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
}
_waitForSingleObjectAddress = symbols.GetAddress(HostRuntimeFunction.WaitForSingleObject);
_setEventAddress = symbols.GetAddress(HostRuntimeFunction.SetEvent);
_exitThreadAddress = symbols.GetAddress(HostRuntimeFunction.ExitThread);
nint kernel32 = GetModuleHandle("kernel32.dll");
if (kernel32 == 0)
{
return false;
}
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
}
private bool Initialize()
{
_selfHandle = GCHandle.Alloc(this);
_controlBlock = (void*)_backend._hostMemory.Allocate(0, 4096u, HostPageProtection.ReadWrite);
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
if (_controlBlock == null)
{
return false;
}
_loopStub = (void*)_backend._hostMemory.Allocate(0, LoopStubSize, HostPageProtection.ReadWriteExecute);
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
if (_loopStub == null)
{
return false;
@@ -375,15 +395,17 @@ public sealed partial class DirectExecutionBackend
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
uint oldProtect = 0;
if (!_backend._hostMemory.Protect((ulong)_loopStub, LoopStubSize, HostPageProtection.ReadExecute, out oldProtect))
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
{
return false;
}
_backend._hostMemory.FlushInstructionCache((ulong)_loopStub, LoopStubSize);
_threadHandle = _backend._hostThreading.CreateNativeThread(
(nint)_loopStub,
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
_threadHandle = CreateThread(
0,
WorkerStackReservation,
(nint)_loopStub,
0,
StackSizeParamIsAReservation,
out _nativeThreadId);
if (_threadHandle == 0)
{
@@ -511,7 +533,7 @@ public sealed partial class DirectExecutionBackend
_prevYieldRequested = _activeGuestThreadYieldRequested;
_prevYieldReason = _activeGuestThreadYieldReason;
_prevState = _activeGuestThreadState;
_prevHostRspSlot = backend._hostThreading.GetTlsValue(backend._hostRspSlotTlsIndex);
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
_entered = true;
_activeExecutionBackend = backend;
@@ -523,11 +545,11 @@ public sealed partial class DirectExecutionBackend
_activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!);
backend._hostThreading.SetTlsValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (_runState is { } state)
{
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
Volatile.Write(ref state.HostThreadId, unchecked((int)backend._hostThreading.CurrentThreadId));
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
}
if (_runAffinityMask != 0)
{
@@ -557,7 +579,7 @@ public sealed partial class DirectExecutionBackend
{
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
}
_backend._hostThreading.SetTlsValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
_activeExecutionBackend = _prevBackend;
_activeCpuContext = _prevContext;
@@ -594,8 +616,8 @@ public sealed partial class DirectExecutionBackend
var exited = _threadHandle == 0;
if (_threadHandle != 0)
{
exited = _backend._hostThreading.WaitForThreadExit(_threadHandle, 1000u);
_backend._hostThreading.CloseThreadHandle(_threadHandle);
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
CloseHandle(_threadHandle);
_threadHandle = 0;
}
if (!exited)
@@ -609,12 +631,12 @@ public sealed partial class DirectExecutionBackend
}
if (_loopStub != null)
{
_backend._hostMemory.Free((ulong)_loopStub);
VirtualFree(_loopStub, 0u, 32768u);
_loopStub = null;
}
if (_controlBlock != null)
{
_backend._hostMemory.Free((ulong)_controlBlock);
VirtualFree(_controlBlock, 0u, 32768u);
_controlBlock = null;
}
if (_selfHandle.IsAllocated)
@@ -4,7 +4,7 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Native.Windows;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
@@ -21,6 +21,8 @@ public sealed unsafe partial class DirectExecutionBackend
// runtime keeps turning its own faults into managed exceptions.
private const int PosixSigIll = 4;
private const int PosixSigTrap = 5;
private const int PosixSigAbort = 6;
private const int PosixSigSegv = 11;
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
@@ -62,6 +64,9 @@ public sealed unsafe partial class DirectExecutionBackend
private static bool _posixSignalWarmup;
private static readonly nint[] _posixPreviousActions = new nint[32];
private static int _posixSignalTraceCount;
private static long _perfSignalCount;
private static readonly bool _perfSignalCounter =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_PERF_MEM"), "1", StringComparison.Ordinal);
[ThreadStatic]
private static int _posixSignalHandlerDepth;
@@ -88,10 +93,13 @@ public sealed unsafe partial class DirectExecutionBackend
}
WarmUpPosixSignalPath();
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
if (!InstallPosixSignalHandler(PosixSigSegv) ||
!InstallPosixSignalHandler(PosixSigBus) ||
!InstallPosixSignalHandler(PosixSigIll))
!InstallPosixSignalHandler(PosixSigIll) ||
!InstallPosixSignalHandler(PosixSigTrap) ||
!InstallPosixSignalHandler(PosixSigAbort))
{
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
}
@@ -132,8 +140,8 @@ public sealed unsafe partial class DirectExecutionBackend
// action and sigaction(0, ...) fails with EINVAL).
EXCEPTION_RECORD record = default;
record.ExceptionCode = DBG_PRINTEXCEPTION_C;
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
byte* contextRecord = stackalloc byte[Win64ContextSize];
new Span<byte>(contextRecord, Win64ContextSize).Clear();
EXCEPTION_POINTERS pointers;
pointers.ExceptionRecord = &record;
pointers.ContextRecord = contextRecord;
@@ -190,8 +198,27 @@ public sealed unsafe partial class DirectExecutionBackend
}
_posixSignalHandlerDepth++;
if (_perfSignalCounter)
{
var n = Interlocked.Increment(ref _perfSignalCount);
if (n % 100000 == 0)
{
Console.Error.WriteLine($"[PERF][MEM] posix_faults={n}");
}
}
try
{
// Guest-image write tracking runs first: it only needs the fault
// address (safe for host and guest threads alike) and must resume
// the faulting write immediately after restoring write access.
if (signal != PosixSigIll &&
siginfo != 0 &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
*(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset)))
{
return;
}
if (TryHandlePosixFault(signal, siginfo, ucontext))
{
return;
@@ -217,8 +244,8 @@ public sealed unsafe partial class DirectExecutionBackend
return false;
}
byte* contextRecord = stackalloc byte[Win64ContextOffsets.Size];
new Span<byte>(contextRecord, Win64ContextOffsets.Size).Clear();
byte* contextRecord = stackalloc byte[Win64ContextSize];
new Span<byte>(contextRecord, Win64ContextSize).Clear();
int[] offsets = PosixRegisterOffsets;
for (int i = 0; i < offsets.Length; i++)
{
@@ -231,6 +258,14 @@ public sealed unsafe partial class DirectExecutionBackend
{
record.ExceptionCode = 3221225501u;
}
else if (signal == PosixSigTrap)
{
record.ExceptionCode = 2147483651u;
}
else if (signal == PosixSigAbort)
{
record.ExceptionCode = 1073741845u;
}
else
{
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,425 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
/// <summary>
/// POSIX replacements for the kernel32 helpers the native backend embeds in
/// emitted x86-64 code. Every stub exposed here follows the Win64 calling
/// convention the emitted call sites were written for (first argument in
/// ECX, result in RAX, Win64 non-volatile registers preserved), so the
/// emission code stays identical across platforms.
/// </summary>
internal static unsafe class PosixHostStubs
{
private static readonly object Gate = new();
private static bool _initialized;
private static nint _tlsGetValueStub;
private static nint _queryPerformanceCounterStub;
private static nint _switchToThreadStub;
private static nint _sleepStub;
public static nint TlsGetValueStubAddress
{
get { EnsureInitialized(); return _tlsGetValueStub; }
}
public static nint QueryPerformanceCounterStubAddress
{
get { EnsureInitialized(); return _queryPerformanceCounterStub; }
}
public static nint SwitchToThreadStubAddress
{
get { EnsureInitialized(); return _switchToThreadStub; }
}
public static nint SleepStubAddress
{
get { EnsureInitialized(); return _sleepStub; }
}
public static nint CreateWorkerEvent()
{
if (OperatingSystem.IsMacOS())
{
return dispatch_semaphore_create(0);
}
var semaphore = Marshal.AllocHGlobal(64);
if (sem_init(semaphore, 0, 0) != 0)
{
Marshal.FreeHGlobal(semaphore);
return 0;
}
return semaphore;
}
public static bool SignalWorkerEvent(nint handle)
{
if (OperatingSystem.IsMacOS())
{
_ = dispatch_semaphore_signal(handle);
return true;
}
return sem_post(handle) == 0;
}
public static bool WaitWorkerEvent(nint handle, int timeoutMilliseconds)
{
if (OperatingSystem.IsMacOS())
{
if (timeoutMilliseconds < 0)
{
return dispatch_semaphore_wait(handle, ulong.MaxValue) == 0;
}
var deadline = dispatch_time(0, timeoutMilliseconds * 1_000_000L);
return dispatch_semaphore_wait(handle, deadline) == 0;
}
if (timeoutMilliseconds < 0)
{
while (sem_wait(handle) != 0)
{
}
return true;
}
var deadlineTicks = Environment.TickCount64 + timeoutMilliseconds;
while (sem_trywait(handle) != 0)
{
if (Environment.TickCount64 >= deadlineTicks)
{
return false;
}
Thread.Sleep(1);
}
return true;
}
public static void DestroyWorkerEvent(nint handle)
{
if (handle == 0)
{
return;
}
if (OperatingSystem.IsMacOS())
{
dispatch_release(handle);
return;
}
_ = sem_destroy(handle);
Marshal.FreeHGlobal(handle);
}
/// <summary>Allocates a pthread TLS key, mirroring kernel32!TlsAlloc.</summary>
public static uint TlsAlloc()
{
if (OperatingSystem.IsMacOS())
{
nuint key;
return pthread_key_create_mac(&key, 0) == 0 ? (uint)key : uint.MaxValue;
}
uint key32;
return pthread_key_create_linux(&key32, 0) == 0 ? key32 : uint.MaxValue;
}
public static bool TlsFree(uint key)
{
return OperatingSystem.IsMacOS()
? pthread_key_delete_mac((nuint)key) == 0
: pthread_key_delete_linux(key) == 0;
}
public static bool TlsSetValue(uint key, nint value)
{
return OperatingSystem.IsMacOS()
? pthread_setspecific_mac((nuint)key, value) == 0
: pthread_setspecific_linux(key, value) == 0;
}
public static nint TlsGetValue(uint key)
{
return OperatingSystem.IsMacOS()
? pthread_getspecific_mac((nuint)key)
: pthread_getspecific_linux(key);
}
/// <summary>Stable numeric id of the calling thread (kernel32!GetCurrentThreadId).</summary>
public static uint GetCurrentThreadId()
{
if (OperatingSystem.IsMacOS())
{
ulong tid;
return pthread_threadid_np(0, &tid) == 0 ? unchecked((uint)tid) : 0u;
}
return unchecked((uint)gettid());
}
/// <summary>
/// Wraps a managed callback (compiled for the SysV ABI on POSIX .NET) in a
/// thunk that accepts up to four integer arguments in the Win64 ABI the
/// emitted x86-64 call sites use. Win64 passes args in rcx/rdx/r8/r9 and
/// treats rdi/rsi as non-volatile; SysV expects rdi/rsi/rdx/rcx and
/// clobbers them, so the thunk saves rdi/rsi, shuffles the registers, keeps
/// the stack 16-byte aligned for the call, and forwards the rax result.
/// </summary>
public static nint CreateWin64ToSysVThunk(nint sysvTarget)
{
var page = (byte*)HostMemory.Alloc(
null,
4096,
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
HostMemory.PAGE_EXECUTE_READWRITE);
if (page == null)
{
throw new OutOfMemoryException("Failed to allocate Win64->SysV thunk page");
}
var offset = 0;
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x48, 0x89, 0xCF); // mov rdi, rcx
Emit(page, ref offset, 0x48, 0x89, 0xD6); // mov rsi, rdx
Emit(page, ref offset, 0x4C, 0x89, 0xC2); // mov rdx, r8
Emit(page, ref offset, 0x4C, 0x89, 0xC9); // mov rcx, r9
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8 (realign to 16)
EmitMovRaxImm64(page, ref offset, sysvTarget); // mov rax, target
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0xC3); // ret
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
{
throw new InvalidOperationException("Failed to protect Win64->SysV thunk page");
}
HostMemory.FlushInstructionCache(page, (nuint)offset);
return (nint)page;
}
private static void EnsureInitialized()
{
if (_initialized)
{
return;
}
lock (Gate)
{
if (_initialized)
{
return;
}
BuildStubs();
_initialized = true;
}
}
private static void BuildStubs()
{
var page = (byte*)HostMemory.Alloc(
null,
4096,
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
HostMemory.PAGE_EXECUTE_READWRITE);
if (page == null)
{
throw new OutOfMemoryException("Failed to allocate POSIX host helper stub page");
}
var offset = 0;
_tlsGetValueStub = EmitTlsGetValue(page, ref offset);
_queryPerformanceCounterStub = EmitQueryPerformanceCounter(page, ref offset);
_switchToThreadStub = EmitSwitchToThread(page, ref offset);
_sleepStub = EmitSleep(page, ref offset);
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
{
throw new InvalidOperationException("Failed to protect POSIX host helper stub page");
}
HostMemory.FlushInstructionCache(page, (nuint)offset);
}
private static nint EmitTlsGetValue(byte* page, ref int offset)
{
var start = (nint)(page + offset);
if (OperatingSystem.IsMacOS())
{
// On macOS x86-64 pthread keys index the gs-based thread specific
// data array directly, so TlsGetValue(index in ecx) collapses to a
// single load that clobbers nothing but RAX.
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
Emit(page, ref offset, 0x65, 0x48, 0x8B, 0x04, 0xC5, 0, 0, 0, 0); // mov rax, gs:[rax*8]
Emit(page, ref offset, 0xC3); // ret
return start;
}
// Linux: call pthread_getspecific, preserving the registers that are
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
var pthreadGetSpecific = ResolveLibcExport("pthread_getspecific");
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
EmitMovRaxImm64(page, ref offset, pthreadGetSpecific); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitQueryPerformanceCounter(byte* page, ref int offset)
{
// BOOL QueryPerformanceCounter(LARGE_INTEGER* out in rcx): the emitted
// consumers only need a monotonically increasing counter, which rdtsc
// provides without leaving Win64-safe registers.
var start = (nint)(page + offset);
Emit(page, ref offset, 0x0F, 0x31); // rdtsc
Emit(page, ref offset, 0x48, 0xC1, 0xE2, 0x20); // shl rdx, 32
Emit(page, ref offset, 0x48, 0x09, 0xD0); // or rax, rdx
Emit(page, ref offset, 0x48, 0x89, 0x01); // mov [rcx], rax
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitSwitchToThread(byte* page, ref int offset)
{
var schedYield = ResolveLibcExport("sched_yield");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
EmitMovRaxImm64(page, ref offset, schedYield); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xB8, 0x01, 0x00, 0x00, 0x00); // mov eax, 1
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint EmitSleep(byte* page, ref int offset)
{
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
var usleep = ResolveLibcExport("usleep");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
Emit(page, ref offset, 0x76, 0x05); // jbe +5
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xC3); // ret
return start;
}
private static nint ResolveLibcExport(string name)
{
var libc = NativeLibrary.Load(OperatingSystem.IsMacOS() ? "libSystem.dylib" : "libc.so.6");
return NativeLibrary.GetExport(libc, name);
}
private static void Emit(byte* page, ref int offset, params byte[] bytes)
{
foreach (var value in bytes)
{
page[offset++] = value;
}
}
private static void EmitMovRaxImm64(byte* page, ref int offset, nint value)
{
Emit(page, ref offset, 0x48, 0xB8);
*(long*)(page + offset) = value;
offset += sizeof(long);
}
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
private static extern int pthread_key_create_linux(uint* key, nint destructor);
[DllImport("libc", EntryPoint = "pthread_key_delete")]
private static extern int pthread_key_delete_mac(nuint key);
[DllImport("libc", EntryPoint = "pthread_key_delete")]
private static extern int pthread_key_delete_linux(uint key);
[DllImport("libc", EntryPoint = "pthread_setspecific")]
private static extern int pthread_setspecific_mac(nuint key, nint value);
[DllImport("libc", EntryPoint = "pthread_setspecific")]
private static extern int pthread_setspecific_linux(uint key, nint value);
[DllImport("libc", EntryPoint = "pthread_getspecific")]
private static extern nint pthread_getspecific_mac(nuint key);
[DllImport("libc", EntryPoint = "pthread_getspecific")]
private static extern nint pthread_getspecific_linux(uint key);
[DllImport("libc")]
private static extern int pthread_threadid_np(nint thread, ulong* threadId);
[DllImport("libc")]
private static extern int gettid();
[DllImport("libc")]
private static extern nint dispatch_semaphore_create(long value);
[DllImport("libc")]
private static extern nint dispatch_semaphore_signal(nint semaphore);
[DllImport("libc")]
private static extern nint dispatch_semaphore_wait(nint semaphore, ulong timeout);
[DllImport("libc")]
private static extern ulong dispatch_time(ulong when, long deltaNanoseconds);
[DllImport("libc")]
private static extern void dispatch_release(nint handle);
[DllImport("libc")]
private static extern int sem_init(nint semaphore, int shared, uint value);
[DllImport("libc")]
private static extern int sem_post(nint semaphore);
[DllImport("libc")]
private static extern int sem_wait(nint semaphore);
[DllImport("libc")]
private static extern int sem_trywait(nint semaphore);
[DllImport("libc")]
private static extern int sem_destroy(nint semaphore);
}
+31 -6
View File
@@ -3,7 +3,6 @@
using System.Runtime.InteropServices;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
@@ -12,15 +11,17 @@ public sealed unsafe class StubManager : IDisposable
private readonly List<nint> _allocatedStubs = new();
private readonly Dictionary<string, nint> _importHandlers = new();
private readonly Dictionary<ulong, nint> _stubAddresses = new();
private readonly IHostMemory _hostMemory;
private byte* _pltMemory;
private int _pltOffset;
private const int PltMemorySize = 1024 * 1024; // 1MB for stubs
public StubManager(IHostMemory? hostMemory = null)
public StubManager()
{
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
_pltMemory = (byte*)_hostMemory.Allocate(0, PltMemorySize, HostPageProtection.ReadWriteExecute);
_pltMemory = (byte*)VirtualAlloc(
null,
(nuint)PltMemorySize,
AllocationType.Reserve | AllocationType.Commit,
MemoryProtection.ExecuteReadWrite);
if (_pltMemory == null)
{
@@ -184,7 +185,7 @@ public sealed unsafe class StubManager : IDisposable
{
if (_pltMemory != null)
{
_hostMemory.Free((ulong)_pltMemory);
VirtualFree(_pltMemory, 0, FreeType.Release);
_pltMemory = null;
}
@@ -193,5 +194,29 @@ public sealed unsafe class StubManager : IDisposable
_stubAddresses.Clear();
}
private static void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect) =>
HostMemory.Alloc(lpAddress, dwSize, (uint)flAllocationType, (uint)flProtect);
private static bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType) =>
HostMemory.Free(lpAddress, dwSize, (uint)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);
}
@@ -48,6 +48,8 @@ public enum ProgramHeaderType : uint
Tls = 7,
GnuEhFrame = 0x6474E550,
SceRela = 0x60000000,
SceProcParam = 0x61000001,
+14 -1
View File
@@ -24,7 +24,10 @@ public sealed class SelfImage
ulong procParamAddress = 0,
string? title = null,
string? titleId = null,
string? version = null)
string? version = null,
uint tlsModuleId = 0,
ulong tlsMemorySize = 0,
ulong tlsStaticOffset = 0)
{
ArgumentNullException.ThrowIfNull(programHeaders);
ArgumentNullException.ThrowIfNull(mappedRegions);
@@ -44,6 +47,9 @@ public sealed class SelfImage
Title = title;
TitleId = titleId;
Version = version;
TlsModuleId = tlsModuleId;
TlsMemorySize = tlsMemorySize;
TlsStaticOffset = tlsStaticOffset;
}
public bool IsSelf { get; }
@@ -75,4 +81,11 @@ public sealed class SelfImage
public string? TitleId { get; }
public string? Version { get; }
public uint TlsModuleId { get; }
public ulong TlsMemorySize { get; }
/// <summary>Variant II distance from the thread pointer to this module's static TLS base.</summary>
public ulong TlsStaticOffset { get; }
}
+515 -109
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@@ -10,16 +11,12 @@ using SharpEmu.Core;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Loader;
public sealed class SelfLoader : ISelfLoader
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
private const uint ElfMagic = 0x7F454C46;
private const uint Ps4SelfMagic = 0x4F153D1D;
private const uint Ps5SelfMagic = 0x5414F5EE;
private const uint SelfMagic = 0x4F153D1D;
private const ulong SelfSegmentFlag = 0x800;
private const int PageSize = 0x1000;
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
@@ -58,11 +55,22 @@ public sealed class SelfLoader : ISelfLoader
private const long DtSceSymTab = 0x61000039;
private const long DtSceSymTabSize = 0x6100003F;
private const uint RelocationTypeNone = 0;
private const uint RelocationTypeAbsolute64 = 1;
private const uint RelocationTypePc32 = 2;
private const uint RelocationTypePlt32 = 4;
private const uint RelocationTypeGlobalData = 6;
private const uint RelocationTypeJumpSlot = 7;
private const uint RelocationTypeRelative = 8;
private const uint RelocationTypeTlsModuleId = 16;
private const uint RelocationTypeUnsigned32 = 10;
private const uint RelocationTypeSigned32 = 11;
private const uint RelocationTypeTlsModuleId = 16; // R_X86_64_DTPMOD64
private const uint RelocationTypeTlsDtpOff64 = 17; // R_X86_64_DTPOFF64
private const uint RelocationTypeTlsTpOff64 = 18; // R_X86_64_TPOFF64
private const uint RelocationTypePc64 = 24;
private const uint RelocationTypeSize32 = 32;
private const uint RelocationTypeSize64 = 33;
private const uint RelocationTypeRelative64 = 38;
private const ulong Ps5MainImageBase = 0x0000000800000000UL;
private const ulong Ps4MainImageBase = 0x0000000000400000UL;
private const ulong Ps5ModuleSearchStart = 0x0000000804000000UL;
@@ -88,6 +96,11 @@ public sealed class SelfLoader : ISelfLoader
private static readonly int SelfSegmentSize = Unsafe.SizeOf<SelfSegment>();
private static readonly int ProgramHeaderSize = Unsafe.SizeOf<ProgramHeader>();
static SelfLoader()
{
RunRelocationSelfChecks();
}
public SelfImage Load(ReadOnlySpan<byte> imageData, IVirtualMemory virtualMemory)
{
return Load(imageData, virtualMemory, fs: null, mountRoot: null);
@@ -156,16 +169,22 @@ public sealed class SelfLoader : ISelfLoader
{
virtualMemory.Clear();
_nextTlsModuleId = 1;
GuestTlsTemplate.Reset();
}
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
var loadContext = ParseLayout(imageData);
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
ValidateElfHeader(elfHeader);
var programHeaders = ParseProgramHeaders(imageData, loadContext, elfHeader);
var hasTlsSegment = TryGetProgramHeader(programHeaders, ProgramHeaderType.Tls, out var processTlsHeader, out _) &&
processTlsHeader.MemorySize != 0;
var tlsModuleId = hasTlsSegment
? (_nextTlsModuleId == 0 ? 1u : _nextTlsModuleId)
: 0u;
Console.Error.WriteLine(
$"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} " +
$"assigned={tlsModuleId} has_pt_tls={hasTlsSegment}");
var totalImageSize = CalculateTotalImageSize(programHeaders);
Console.WriteLine($"Total image size needed: 0x{totalImageSize:X} ({totalImageSize} bytes)");
@@ -178,8 +197,9 @@ public sealed class SelfLoader : ISelfLoader
{
if (!physicalVm.TryAllocateAtExact(imageBase, totalImageSize, executable: true, out var allocatedBase))
{
var reason = physicalVm.DescribeAddressForDiagnostics(imageBase);
throw new InvalidOperationException(
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}).");
$"Could not allocate main image at required base 0x{imageBase:X16} (size=0x{totalImageSize:X}): {reason}.");
}
imageBase = allocatedBase;
@@ -198,6 +218,13 @@ public sealed class SelfLoader : ISelfLoader
}
MapLoadSegments(imageData, loadContext, programHeaders, virtualMemory, imageBase);
// Register every module before relocations so DTPMOD/DTPOFF/TPOFF use
// the module's real PT_TLS identity and Variant II static offset.
var tlsInfo = RegisterModuleTlsTemplate(
programHeaders,
virtualMemory,
imageBase,
tlsModuleId);
var importStubs = ResolveAndPatchImportStubs(
imageData,
loadContext,
@@ -255,11 +282,11 @@ public sealed class SelfLoader : ISelfLoader
Console.WriteLine($"[LOADER] PH[{i}]: type={ph.HeaderType}, vaddr=0x{ph.VirtualAddress:X16} -> 0x{ph.VirtualAddress + imageBase:X16}, memsz=0x{ph.MemorySize:X}");
}
if (_nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
if (tlsModuleId != 0 && _nextTlsModuleId == tlsModuleId && _nextTlsModuleId < uint.MaxValue)
{
_nextTlsModuleId++;
}
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
return new SelfImage(
loadContext.IsSelf,
@@ -276,7 +303,10 @@ public sealed class SelfLoader : ISelfLoader
procParamAddress,
applicationInfo.Title,
applicationInfo.TitleId,
applicationInfo.Version);
applicationInfo.Version,
tlsModuleId,
tlsInfo.MemorySize,
tlsInfo.StaticOffset);
}
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
@@ -325,8 +355,7 @@ public sealed class SelfLoader : ISelfLoader
throw new InvalidDataException("Input image is too small to contain an ELF header.");
}
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
if (magic is Ps4SelfMagic or Ps5SelfMagic)
if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
{
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
@@ -348,10 +377,19 @@ public sealed class SelfLoader : ISelfLoader
return new LoadContext(IsSelf: true, elfOffset, selfHeader.FileSize, segments);
}
if (magic != ElfMagic)
// Not a recognized (fake-signed) SELF. Only a bare, decrypted ELF is
// acceptable here; anything else — most commonly a still-encrypted
// retail eboot — must be reported clearly rather than failing later
// with an opaque "not a valid ELF header" message.
const uint ElfMagicBigEndian = 0x7F454C46; // "\x7fELF"
var leadingWord = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
if (leadingWord != ElfMagicBigEndian)
{
throw new InvalidDataException(
$"Unsupported executable signature 0x{magic:X8}");
$"Image is neither a decrypted ELF nor a recognized fake-signed SELF " +
$"(leading bytes 0x{leadingWord:X8}). This is almost certainly a still-encrypted " +
$"retail eboot — SharpEmu has no decryption keys and requires a decrypted / " +
$"fake-signed (fSELF) image.");
}
return new LoadContext(IsSelf: false, ElfOffset: 0, SelfFileSize: 0, Array.Empty<SelfSegment>());
@@ -414,15 +452,15 @@ public sealed class SelfLoader : ISelfLoader
var virtualAddress = header.VirtualAddress + imageBase;
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
if (header.Alignment > 1)
{
var vaddrMod = virtualAddress % header.Alignment;
var offsetMod = header.Offset % header.Alignment;
if (vaddrMod != offsetMod)
{
Log.Warning(
$"Segment {index} ELF alignment mismatch! " +
Console.Error.WriteLine(
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
}
@@ -465,6 +503,43 @@ public sealed class SelfLoader : ISelfLoader
return 0;
}
private static ModuleTlsInfo RegisterModuleTlsTemplate(
IReadOnlyList<ProgramHeader> programHeaders,
IVirtualMemory virtualMemory,
ulong imageBase,
uint tlsModuleId)
{
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Tls, out var tlsHeader, out _) ||
tlsHeader.MemorySize == 0)
{
return default;
}
// tdata (initialized) bytes come from the mapped segment; tbss is the
// implicitly-zero remainder up to MemorySize.
var fileSize = (int)Math.Min(tlsHeader.FileSize, tlsHeader.MemorySize);
var initImage = fileSize > 0 ? new byte[fileSize] : [];
if (fileSize > 0 &&
!virtualMemory.TryRead(imageBase + tlsHeader.VirtualAddress, initImage))
{
Console.Error.WriteLine(
$"[LOADER][TLS] Failed to read TLS init image at 0x{imageBase + tlsHeader.VirtualAddress:X}; seeding zeros.");
initImage = [];
}
var staticOffset = GuestTlsTemplate.RegisterModule(
tlsModuleId,
initImage,
tlsHeader.MemorySize,
tlsHeader.Alignment,
tlsHeader.VirtualAddress);
Console.Error.WriteLine(
$"[LOADER][TLS] Module {tlsModuleId} TLS template: memsz=0x{tlsHeader.MemorySize:X} " +
$"filesz=0x{tlsHeader.FileSize:X} align=0x{tlsHeader.Alignment:X} " +
$"static_offset=0x{staticOffset:X} total_static=0x{GuestTlsTemplate.StaticTlsSize:X}");
return new ModuleTlsInfo(tlsHeader.MemorySize, staticOffset);
}
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
@@ -544,12 +619,17 @@ public sealed class SelfLoader : ISelfLoader
uint maxSymbolIndex = 0;
foreach (var relocation in relocations)
{
if (relocation.Type == RelocationTypeNone)
{
continue;
}
if (!IsSupportedRelocationType(relocation.Type))
{
continue;
}
if (relocation.Type is RelocationTypeRelative or RelocationTypeTlsModuleId)
if (relocation.Type is RelocationTypeNone or RelocationTypeRelative or RelocationTypeRelative64 or RelocationTypeTlsModuleId)
{
continue;
}
@@ -633,13 +713,16 @@ public sealed class SelfLoader : ISelfLoader
importedRelocations = BuildImportedRelocations(descriptors);
var stubsByAddress = CreateImportStubMapping(virtualMemory, orderedImportNids);
var stubImportNids = orderedImportNids
.Where(nid => ShouldCreateImportStub(nid, descriptors, moduleManager))
.ToArray();
var stubsByAddress = CreateImportStubMapping(virtualMemory, stubImportNids);
Console.WriteLine($"[LOADER] Created {stubsByAddress.Count} import stubs");
int printCount = Math.Min(10, orderedImportNids.Count);
int printCount = Math.Min(10, stubImportNids.Length);
for (int i = 0; i < printCount; i++)
{
var nid = orderedImportNids[i];
var nid = stubImportNids[i];
var addr = stubsByAddress.First(x => x.Value == nid).Key;
}
@@ -663,45 +746,65 @@ public sealed class SelfLoader : ISelfLoader
foreach (var descriptor in descriptors)
{
ulong targetValue;
ulong symbolValue;
if (descriptor.ImportNid is null)
{
targetValue = AddSigned(descriptor.SymbolValue, descriptor.Addend);
symbolValue = descriptor.SymbolValue;
}
else
{
if (!addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
if (addressesByNid.TryGetValue(descriptor.ImportNid, out var stubAddress))
{
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
symbolValue = stubAddress;
}
targetValue = AddSigned(stubAddress, descriptor.Addend);
}
if (targetValue < 0x1000)
{
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
else if (descriptor.IsWeak)
{
Log.Debug(
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
// ELF unresolved weak definitions use S=0. They must not
// receive a trap import stub, which would turn a permitted
// null test into a call to an unresolved-import handler.
symbolValue = 0;
}
else
{
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
throw new InvalidOperationException($"Import stub not found for NID '{descriptor.ImportNid}'.");
}
}
if (!TryWriteUInt64(virtualMemory, descriptor.TargetAddress, targetValue))
var targetValue = ComputeRelocationValue(descriptor, symbolValue);
if (targetValue < 0x1000 && descriptor.ValueKind is
RelocationValueKind.TlsOffset or
RelocationValueKind.PcRelative or
RelocationValueKind.SymbolSize)
{
throw new InvalidDataException($"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}.");
// A TLS offset (TPOFF64/DTPOFF64) is a signed displacement, not a
// mapped address, so a small or negative value here is expected.
}
else if (targetValue < 0x1000 && !descriptor.IsWeak)
{
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
{
Console.Error.WriteLine(
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
}
else
{
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
}
}
if (!TryWriteRelocationValue(virtualMemory, descriptor, targetValue, out var writeError))
{
throw new InvalidDataException(
$"Failed to patch relocation at 0x{descriptor.TargetAddress:X16}: {writeError}");
}
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
{
Log.Debug(
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
Console.Error.WriteLine(
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
}
}
@@ -794,29 +897,36 @@ public sealed class SelfLoader : ISelfLoader
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Log.Debug(
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
Console.Error.WriteLine(
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
}
if (!IsSupportedRelocationType(relocation.Type))
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
// Surface unsupported relocation types loudly instead of
// skipping silently: TLS-relative (17/18), COPY (5), and
// IRELATIVE/ifunc (37) leave their targets unrelocated, which
// manifests later as reads of zero or calls into address 0.
ReportUnsupportedRelocation(relocation.Type, relocation.Offset, imageBase);
if (relocation.Type is 5 or 37)
{
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
throw new NotSupportedException(
$"Relocation type {relocation.Type} requires deferred runtime-linker processing.");
}
continue;
}
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, sizeof(ulong), out var targetAddress))
var relocationWriteSize = GetRelocationWriteSize(relocation.Type);
if (!TryResolveMappedAddress(virtualMemory, relocation.Offset, imageBase, relocationWriteSize, out var targetAddress))
{
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
{
Log.Debug("[FOCUS][SKIP] target address not mapped");
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
}
continue;
}
if (relocation.Type == RelocationTypeRelative)
if (relocation.Type is RelocationTypeRelative or RelocationTypeRelative64)
{
descriptors.Add(new RelocationDescriptor(
targetAddress,
@@ -830,7 +940,12 @@ public sealed class SelfLoader : ISelfLoader
if (relocation.Type == RelocationTypeTlsModuleId)
{
var dtpmodValue = tlsModuleId == 0 ? 1u : tlsModuleId;
if (tlsModuleId == 0)
{
throw new InvalidDataException(
$"R_X86_64_DTPMOD64 at 0x{targetAddress:X16} references a module without PT_TLS.");
}
var dtpmodValue = tlsModuleId;
descriptors.Add(new RelocationDescriptor(
targetAddress,
0,
@@ -851,50 +966,100 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
}
continue;
}
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot ? 0 : relocation.Addend;
var symbolBind = GetSymbolBind(symbol.Info);
if (symbolBind == SymbolBindLocal)
if (relocation.Type is RelocationTypeTlsDtpOff64 or RelocationTypeTlsTpOff64)
{
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
// Variant II static TLS: the module block sits at
// [tp - blockSize, tp). DTPOFF64 is the module-relative offset
// (st_value + addend); TPOFF64 is that offset expressed relative
// to the thread pointer, i.e. minus the aligned block size.
var tlsSymbolOffset = AddSigned(symbol.Value, relocation.Addend);
if (!GuestTlsTemplate.TryGetStaticOffset(tlsModuleId, out var moduleStaticOffset))
{
Log.Warning(
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
Console.Error.WriteLine(
$"[LOADER][TLS] Missing PT_TLS registration for module {tlsModuleId}; " +
$"cannot apply relocation {relocation.Type} at 0x{targetAddress:X16}.");
continue;
}
var tlsValue = relocation.Type == RelocationTypeTlsTpOff64
? unchecked(tlsSymbolOffset - moduleStaticOffset)
: tlsSymbolOffset;
descriptors.Add(new RelocationDescriptor(
targetAddress,
addend,
0,
null,
symbolAddress,
RelocationValueKind.Pointer,
tlsValue,
RelocationValueKind.TlsOffset,
IsDataImport: false));
continue;
}
var symbolBind = GetSymbolBind(symbol.Info);
if (symbolIndex == 0)
{
descriptors.Add(CreateSymbolRelocationDescriptor(
relocation,
targetAddress,
symbol,
symbolAddress: 0,
importNid: null,
isWeak: false));
continue;
}
if (symbolBind == SymbolBindLocal)
{
var symbolAddress = relocation.Type is RelocationTypeSize32 or RelocationTypeSize64
? 0
: ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
if (relocation.Type is not (RelocationTypeSize32 or RelocationTypeSize64))
{
Console.Error.WriteLine(
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
continue;
}
}
descriptors.Add(CreateSymbolRelocationDescriptor(
relocation,
targetAddress,
symbol,
symbolAddress,
importNid: null,
isWeak: false));
continue;
}
if (symbol.Value != 0)
{
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
var symbolAddress = relocation.Type is RelocationTypeSize32 or RelocationTypeSize64
? 0
: ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
if (symbolAddress == 0)
{
Log.Warning(
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
if (relocation.Type is not (RelocationTypeSize32 or RelocationTypeSize64))
{
Console.Error.WriteLine(
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
continue;
}
}
descriptors.Add(new RelocationDescriptor(
descriptors.Add(CreateSymbolRelocationDescriptor(
relocation,
targetAddress,
addend,
null,
symbol,
symbolAddress,
RelocationValueKind.Pointer,
IsDataImport: false));
importNid: null,
isWeak: false));
continue;
}
@@ -902,16 +1067,26 @@ public sealed class SelfLoader : ISelfLoader
{
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
}
continue;
}
if (!TryReadNullTerminatedAscii(stringTable, symbol.NameOffset, out var symbolName))
{
if (symbolBind == SymbolBindWeak)
{
descriptors.Add(CreateSymbolRelocationDescriptor(
relocation,
targetAddress,
symbol,
symbolAddress: 0,
importNid: null,
isWeak: true));
}
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
{
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
}
continue;
}
@@ -919,6 +1094,16 @@ public sealed class SelfLoader : ISelfLoader
var nid = ExtractNid(symbolName);
if (string.IsNullOrWhiteSpace(nid))
{
if (symbolBind == SymbolBindWeak)
{
descriptors.Add(CreateSymbolRelocationDescriptor(
relocation,
targetAddress,
symbol,
symbolAddress: 0,
importNid: null,
isWeak: true));
}
continue;
}
@@ -927,16 +1112,74 @@ public sealed class SelfLoader : ISelfLoader
orderedImportNids.Add(nid);
}
descriptors.Add(new RelocationDescriptor(
descriptors.Add(CreateSymbolRelocationDescriptor(
relocation,
targetAddress,
addend,
nid,
0,
RelocationValueKind.Pointer,
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject));
symbol,
symbolAddress: 0,
importNid: nid,
isWeak: symbolBind == SymbolBindWeak));
}
}
private static RelocationDescriptor CreateSymbolRelocationDescriptor(
ElfRelocation relocation,
ulong targetAddress,
ElfSymbol symbol,
ulong symbolAddress,
string? importNid,
bool isWeak)
{
var valueKind = relocation.Type switch
{
RelocationTypePc32 or RelocationTypePlt32 or RelocationTypePc64 => RelocationValueKind.PcRelative,
RelocationTypeSize32 or RelocationTypeSize64 => RelocationValueKind.SymbolSize,
_ => RelocationValueKind.Pointer,
};
var writeKind = relocation.Type switch
{
RelocationTypePc32 or RelocationTypePlt32 or RelocationTypeSigned32 => RelocationWriteKind.Int32,
RelocationTypeUnsigned32 or RelocationTypeSize32 => RelocationWriteKind.UInt32,
_ => RelocationWriteKind.UInt64,
};
var symbolValue = valueKind == RelocationValueKind.SymbolSize
? symbol.Size
: symbolAddress;
var addend = relocation.Type is RelocationTypeGlobalData or RelocationTypeJumpSlot
? 0
: relocation.Addend;
return new RelocationDescriptor(
targetAddress,
addend,
importNid,
symbolValue,
valueKind,
IsDataImport: GetSymbolType(symbol.Info) == SymbolTypeObject,
writeKind,
isWeak);
}
private static bool ShouldCreateImportStub(
string nid,
IReadOnlyList<RelocationDescriptor> descriptors,
IModuleManager? moduleManager)
{
for (var i = 0; i < descriptors.Count; i++)
{
var descriptor = descriptors[i];
if (!string.Equals(descriptor.ImportNid, nid, StringComparison.Ordinal))
{
continue;
}
if (!descriptor.IsWeak || moduleManager?.TryGetExport(nid, out _) == true)
{
return true;
}
}
return false;
}
private static void RegisterRuntimeSymbolsAndHooks(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
@@ -959,8 +1202,8 @@ public sealed class SelfLoader : ISelfLoader
if (sectionSymbols > 0 || dynamicSymbols > 0)
{
Log.Info(
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
Console.Error.WriteLine(
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
}
}
@@ -1034,8 +1277,8 @@ public sealed class SelfLoader : ISelfLoader
if (preInitializers.Count != 0 || initializers.Count != 0)
{
Log.Info(
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
Console.Error.WriteLine(
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
}
}
@@ -1418,8 +1661,8 @@ public sealed class SelfLoader : ISelfLoader
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
Log.Debug(
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
Console.Error.WriteLine(
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
if (mapSize > int.MaxValue)
{
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
@@ -1691,11 +1934,51 @@ public sealed class SelfLoader : ISelfLoader
private static bool IsSupportedRelocationType(uint relocationType)
{
return relocationType is
RelocationTypeNone or
RelocationTypeAbsolute64 or
RelocationTypePc32 or
RelocationTypePlt32 or
RelocationTypeGlobalData or
RelocationTypeJumpSlot or
RelocationTypeRelative or
RelocationTypeTlsModuleId;
RelocationTypeUnsigned32 or
RelocationTypeSigned32 or
RelocationTypeTlsModuleId or
RelocationTypeTlsDtpOff64 or
RelocationTypeTlsTpOff64 or
RelocationTypePc64 or
RelocationTypeSize32 or
RelocationTypeSize64 or
RelocationTypeRelative64;
}
private static readonly HashSet<uint> _reportedUnsupportedRelocationTypes = new();
private static void ReportUnsupportedRelocation(uint relocationType, ulong offset, ulong imageBase)
{
// Report each distinct unsupported type once to keep the log useful.
lock (_reportedUnsupportedRelocationTypes)
{
if (!_reportedUnsupportedRelocationTypes.Add(relocationType))
{
if (IsFocusRelocationOffset(offset, imageBase))
{
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocationType}");
}
return;
}
}
var name = relocationType switch
{
5 => "R_X86_64_COPY",
37 => "R_X86_64_IRELATIVE (ifunc)",
_ => "unknown",
};
Console.Error.WriteLine(
$"[LOADER][ERROR] Unsupported relocation type {relocationType} ({name}) rejected " +
$"(first at off=0x{offset:X16}); COPY requires dependency symbol storage and IRELATIVE requires resolver execution.");
}
private static ulong DetermineRequestedImageBase(
@@ -1851,16 +2134,16 @@ public sealed class SelfLoader : ISelfLoader
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
var guestAddr = location + imageBase;
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
if (virtualMemory.TryRead(guestAddr, tableBytes))
{
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
return true;
}
if (virtualMemory.TryRead(location, tableBytes))
{
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
return true;
}
@@ -1868,11 +2151,11 @@ public sealed class SelfLoader : ISelfLoader
{
var slice = elfData.Slice((int)location, (int)size);
tableBytes = slice.ToArray();
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
return true;
}
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
tableBytes = Array.Empty<byte>();
return false;
}
@@ -1929,17 +2212,17 @@ public sealed class SelfLoader : ISelfLoader
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
{
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
if (address == 0)
{
Log.Debug("-> return 0 (null)");
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
return 0;
}
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
{
Log.Debug($"-> resolved raw 0x{resolved:X16}");
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
return resolved;
}
@@ -1948,18 +2231,18 @@ public sealed class SelfLoader : ISelfLoader
var rebased = address + imageBase;
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
{
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
return resolvedRebased;
}
}
if (address < 0x10000)
{
Log.Debug($"-> reject small 0x{address:X}");
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
return 0;
}
Log.Debug($"-> fallback raw 0x{address:X}");
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
return address;
}
@@ -2049,6 +2332,106 @@ public sealed class SelfLoader : ISelfLoader
return virtualMemory.TryWrite(address, buffer);
}
private static ulong ComputeRelocationValue(RelocationDescriptor descriptor, ulong resolvedSymbolValue)
{
var baseValue = descriptor.ValueKind == RelocationValueKind.SymbolSize
? descriptor.SymbolValue
: resolvedSymbolValue;
var value = AddSigned(baseValue, descriptor.Addend);
return descriptor.ValueKind == RelocationValueKind.PcRelative
? unchecked(value - descriptor.TargetAddress)
: value;
}
private static bool TryWriteRelocationValue(
IVirtualMemory virtualMemory,
RelocationDescriptor descriptor,
ulong value,
out string? error)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
var length = sizeof(ulong);
switch (descriptor.WriteKind)
{
case RelocationWriteKind.UInt64:
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
break;
case RelocationWriteKind.UInt32:
if (value > uint.MaxValue)
{
error = $"value 0x{value:X16} overflows an unsigned 32-bit relocation";
return false;
}
BinaryPrimitives.WriteUInt32LittleEndian(buffer, (uint)value);
length = sizeof(uint);
break;
case RelocationWriteKind.Int32:
var signedValue = unchecked((long)value);
if (signedValue is < int.MinValue or > int.MaxValue)
{
error = $"value {signedValue} overflows a signed 32-bit relocation";
return false;
}
BinaryPrimitives.WriteInt32LittleEndian(buffer, (int)signedValue);
length = sizeof(int);
break;
default:
error = $"unknown relocation write kind {descriptor.WriteKind}";
return false;
}
error = virtualMemory.TryWrite(descriptor.TargetAddress, buffer[..length])
? null
: "target memory is not writable";
return error is null;
}
private static int GetRelocationWriteSize(uint relocationType)
{
return relocationType is
RelocationTypePc32 or
RelocationTypePlt32 or
RelocationTypeUnsigned32 or
RelocationTypeSigned32 or
RelocationTypeSize32
? sizeof(uint)
: sizeof(ulong);
}
[Conditional("DEBUG")]
private static void RunRelocationSelfChecks()
{
var pc32 = new RelocationDescriptor(
TargetAddress: 0x1000,
Addend: -4,
ImportNid: null,
SymbolValue: 0x1800,
RelocationValueKind.PcRelative,
IsDataImport: false,
RelocationWriteKind.Int32);
Debug.Assert(
unchecked((long)ComputeRelocationValue(pc32, pc32.SymbolValue)) == 0x7FC,
"R_X86_64_PC32 did not apply S + A - P.");
var weak = new RelocationDescriptor(
TargetAddress: 0x2000,
Addend: 7,
ImportNid: "weak",
SymbolValue: 0,
RelocationValueKind.Pointer,
IsDataImport: false,
IsWeak: true);
Debug.Assert(
ComputeRelocationValue(weak, resolvedSymbolValue: 0) == 7,
"An unresolved weak relocation did not use S=0.");
Debug.Assert(
!ShouldCreateImportStub("weak", [weak], moduleManager: null),
"An unresolved weak symbol incorrectly received a trap import stub.");
}
private static ulong AlignUp(ulong value, ulong alignment)
{
var mask = alignment - 1;
@@ -2257,6 +2640,16 @@ public sealed class SelfLoader : ISelfLoader
{
throw new InvalidDataException($"Unsupported ELF program header entry size: {header.ProgramHeaderEntrySize}.");
}
// The CPU backend executes guest instructions natively, so a non
// x86-64 image can only fail deep in execution with an opaque SIGILL.
// Catch it here with an actionable message. (EM_X86_64 == 62.)
const ushort ElfMachineX86_64 = 62;
if (header.Machine != ElfMachineX86_64)
{
throw new InvalidDataException(
$"Unsupported ELF machine type 0x{header.Machine:X4}; SharpEmu only runs x86-64 (EM_X86_64) images.");
}
}
private static unsafe T ReadUnmanaged<T>(ReadOnlySpan<byte> source, int offset)
@@ -2337,10 +2730,25 @@ public sealed class SelfLoader : ISelfLoader
public uint Type => (uint)(Info & uint.MaxValue);
}
private readonly record struct ModuleTlsInfo(ulong MemorySize, ulong StaticOffset);
private enum RelocationValueKind : byte
{
Pointer = 0,
TlsModuleId = 1
TlsModuleId = 1,
// A pre-computed TLS offset written verbatim (TPOFF64/DTPOFF64). Unlike
// Pointer it is a signed displacement, not a mapped address, so it is
// patched as-is without the low-address validity warning.
TlsOffset = 2,
PcRelative = 3,
SymbolSize = 4,
}
private enum RelocationWriteKind : byte
{
UInt64 = 0,
UInt32 = 1,
Int32 = 2,
}
private readonly record struct RelocationDescriptor(
@@ -2349,7 +2757,9 @@ public sealed class SelfLoader : ISelfLoader
string? ImportNid,
ulong SymbolValue,
RelocationValueKind ValueKind,
bool IsDataImport);
bool IsDataImport,
RelocationWriteKind WriteKind = RelocationWriteKind.UInt64,
bool IsWeak = false);
private enum SelfSegmentResolveStatus
{
@@ -2389,14 +2799,10 @@ public sealed class SelfLoader : ISelfLoader
public ulong FileSize => _fileSize;
public bool HasKnownLayout =>
((_ident0 == 0x4F &&
_ident1 == 0x15 &&
_ident2 == 0x3D &&
_ident3 == 0x1D) ||
(_ident0 == 0x54 &&
_ident1 == 0x14 &&
_ident2 == 0xF5 &&
_ident3 == 0xEE)) &&
_ident0 == 0x4F &&
_ident1 == 0x15 &&
_ident2 == 0x3D &&
_ident3 == 0x1D &&
_ident4 == 0x00 &&
_ident5 == 0x01 &&
_ident6 == 0x01 &&
+124 -51
View File
@@ -48,7 +48,107 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public PhysicalVirtualMemory(IHostMemory? hostMemory = null)
{
_hostMemory = hostMemory ?? HostPlatform.Current.Memory;
_hostMemory = hostMemory ?? CrossPlatformHostMemory.Instance;
}
private sealed class CrossPlatformHostMemory : IHostMemory
{
public static readonly CrossPlatformHostMemory Instance = new();
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
unchecked((ulong)HostMemory.Alloc(
(void*)desiredAddress,
(nuint)size,
HostMemory.MEM_RESERVE | HostMemory.MEM_COMMIT,
ToRawProtection(protection)));
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
unchecked((ulong)HostMemory.Alloc(
(void*)desiredAddress,
(nuint)size,
HostMemory.MEM_RESERVE,
ToRawProtection(protection)));
public bool Commit(ulong address, ulong size, HostPageProtection protection) =>
HostMemory.Alloc(
(void*)address,
(nuint)size,
HostMemory.MEM_COMMIT,
ToRawProtection(protection)) != null;
public bool Free(ulong address) =>
HostMemory.Free((void*)address, 0, HostMemory.MEM_RELEASE);
public bool Protect(
ulong address,
ulong size,
HostPageProtection protection,
out uint rawOldProtection) =>
HostMemory.Protect(
(void*)address,
(nuint)size,
ToRawProtection(protection),
out rawOldProtection);
public bool ProtectRaw(
ulong address,
ulong size,
uint rawProtection,
out uint rawOldProtection) =>
HostMemory.Protect((void*)address, (nuint)size, rawProtection, out rawOldProtection);
public bool Query(ulong address, out HostRegionInfo info)
{
if (HostMemory.Query((void*)address, out var raw) == 0)
{
info = default;
return false;
}
var state = raw.State switch
{
HostMemory.MEM_FREE_STATE => HostRegionState.Free,
HostMemory.MEM_RESERVE => HostRegionState.Reserved,
_ => HostRegionState.Committed,
};
info = new HostRegionInfo(
raw.BaseAddress,
raw.AllocationBase,
raw.RegionSize,
state,
raw.State,
FromRawProtection(raw.Protect),
raw.Protect,
raw.AllocationProtect);
return true;
}
public void FlushInstructionCache(ulong address, ulong size) =>
HostMemory.FlushInstructionCache((void*)address, (nuint)size);
private static uint ToRawProtection(HostPageProtection protection) => protection switch
{
HostPageProtection.NoAccess => HostMemory.PAGE_NOACCESS,
HostPageProtection.ReadOnly => HostMemory.PAGE_READONLY,
HostPageProtection.ReadWrite => HostMemory.PAGE_READWRITE,
HostPageProtection.Execute => HostMemory.PAGE_EXECUTE,
HostPageProtection.ReadExecute => HostMemory.PAGE_EXECUTE_READ,
HostPageProtection.ReadWriteExecute => HostMemory.PAGE_EXECUTE_READWRITE,
HostPageProtection.ExecuteWriteCopy => 0x80,
_ => HostMemory.PAGE_NOACCESS,
};
private static HostPageProtection FromRawProtection(uint protection) => protection switch
{
HostMemory.PAGE_READONLY => HostPageProtection.ReadOnly,
HostMemory.PAGE_READWRITE => HostPageProtection.ReadWrite,
HostMemory.PAGE_EXECUTE => HostPageProtection.Execute,
HostMemory.PAGE_EXECUTE_READ => HostPageProtection.ReadExecute,
HostMemory.PAGE_EXECUTE_READWRITE => HostPageProtection.ReadWriteExecute,
0x80 => HostPageProtection.ExecuteWriteCopy,
_ => HostPageProtection.NoAccess,
};
}
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
@@ -98,6 +198,24 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true;
}
public string DescribeAddressForDiagnostics(ulong address)
{
if (!_hostMemory.Query(address, out var info))
{
return "unable to query host memory at this address";
}
return info.State switch
{
HostRegionState.Free => "address reports free, but the exact-address reservation still failed",
HostRegionState.Reserved =>
$"already reserved by another host allocation (base=0x{info.AllocationBase:X16}, size=0x{info.RegionSize:X})",
HostRegionState.Committed =>
$"already committed by another host allocation (base=0x{info.AllocationBase:X16}, size=0x{info.RegionSize:X}, protect=0x{info.RawProtection:X})",
_ => $"in an unexpected host state (raw=0x{info.RawState:X})",
};
}
public ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true)
{
if (size == 0)
@@ -249,42 +367,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
// Under Rosetta 2 the kernel can ignore placement hints for whole
// windows, so page-stepped exact probes are pathological on macOS.
// Linux must keep using the exact-address search below: PS5 resource
// descriptors cannot represent ordinary 0x7F... host mappings. Linux
// HostMemory uses MAP_FIXED_NOREPLACE, making those low-address probes
// safe without clobbering existing host mappings.
// macOS needs alignment over-allocation; Linux uses exact-address search.
if (OperatingSystem.IsMacOS())
{
// Prefer the requested low address. Besides matching the guest
// address model, this keeps the allocation representable by every
// PS5 GPU descriptor (the strictest ones carry 40 address bits).
try
{
var exactAddress = AllocateAt(
cursor,
alignedSize,
executable,
allowAlternative: false);
if (exactAddress == cursor)
{
actualAddress = exactAddress;
UpdateAllocationSearchCursor(
desiredAddress,
effectiveAlignment,
executable,
exactAddress + alignedSize);
return true;
}
}
catch
{
}
// Over-allocate by the alignment so a kernel-chosen placement
// always contains an aligned start; the unused head/tail stays
// part of the tracked region and is simply never handed out.
var reserveSize = effectiveAlignment > PageSize
? alignedSize + effectiveAlignment
: alignedSize;
@@ -294,10 +379,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
if (posixAddress != 0)
{
var alignedBase = AlignUp(posixAddress, effectiveAlignment);
const ulong gpuAddressLimit = 1UL << 40;
if (alignedBase < gpuAddressLimit &&
alignedSize <= gpuAddressLimit - alignedBase &&
alignedBase + alignedSize <= posixAddress + reserveSize)
if (alignedBase + alignedSize <= posixAddress + reserveSize)
{
actualAddress = alignedBase;
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, alignedBase + alignedSize);
@@ -327,19 +409,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
continue;
}
try
{
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
if (actualAddress == cursor)
{
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
return true;
}
actualAddress = 0;
}
catch
if (TryAllocateAtExact(cursor, alignedSize, executable, out actualAddress))
{
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
return true;
}
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
+188 -129
View File
@@ -7,25 +7,22 @@ using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData;
using SharpEmu.Logging;
using SharpEmu.Libs.Fiber;
using SharpEmu.Libs.SystemService;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace SharpEmu.Core.Runtime;
public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
private readonly record struct LoadedModuleImage(string Path, SelfImage Image, int Handle, bool StartAtBoot);
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
{
@@ -84,22 +81,19 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
};
var moduleManager = new ModuleManager();
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
// The compile-time generated registry (SharpEmu.SourceGenerators) is the sole
// registration source; content tests in SharpEmu.Libs.Tests pin its invariants.
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
moduleManager.Freeze();
// 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 virtualMemory = new PhysicalVirtualMemory();
var fileSystem = new PhysicalFileSystem();
return new SharpEmuRuntime(
new SelfLoader(),
virtualMemory,
new CpuDispatcher(virtualMemory, moduleManager, hostPlatform: hostPlatform),
new CpuDispatcher(virtualMemory, moduleManager),
moduleManager,
Aerolib.Instance,
cpuExecutionOptions,
@@ -137,20 +131,21 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
var normalizedEbootPath = Path.GetFullPath(ebootPath);
using var app0Binding = BindApp0Root(normalizedEbootPath);
Log.Info($"Loading: {ebootPath}");
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
LastExecutionDiagnostics = null;
LastExecutionTrace = null;
LastSessionSummary = null;
LastBasicBlockTrace = null;
LastMilestoneLog = null;
FiberExports.ResetRuntimeState();
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
LogAppBundleInfo(normalizedEbootPath, image);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
SystemServiceExports.ConfigureApplicationInfo(image.TitleId);
_ = RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
@@ -173,7 +168,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName);
if (initializerResult is { } failedInitializerResult)
{
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -181,8 +176,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return failedInitializerResult;
}
Log.Info($"Dispatching, gen: {generation}");
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
var result = _cpuDispatcher.DispatchEntry(
image.EntryPoint,
@@ -192,8 +187,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName,
_cpuExecutionOptions);
Log.Info($"DispatchEntry returned: {result}");
Log.Info($"Dispatch result: {result}");
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -364,66 +359,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return result;
}
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
{
var executableName = Path.GetFileName(ebootPath);
if (string.IsNullOrWhiteSpace(executableName))
{
executableName = "eboot.bin";
}
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
var builder = new StringBuilder();
builder.AppendLine("App bundle info:");
builder.AppendLine($"- Display name: {displayName}");
builder.AppendLine($"- Version: {version}");
builder.AppendLine($"- Title ID: {titleId}");
builder.AppendLine($"- Content ID: {contentId}");
builder.AppendLine($"- Executable: {executableName}");
builder.Append("- Platform: PlayStation 5");
Log.Info(builder.ToString());
}
private static string? ResolveContentId(string? bundleRoot)
{
if (string.IsNullOrEmpty(bundleRoot))
{
return null;
}
foreach (var candidate in new[]
{
Path.Combine(bundleRoot, "sce_sys", "param.json"),
Path.Combine(bundleRoot, "param.json"),
})
{
if (!File.Exists(candidate))
{
continue;
}
try
{
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
{
return contentId.GetString();
}
}
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
{
}
break;
}
return null;
}
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
{
const string app0VariableName = "SHARPEMU_APP0_DIR";
@@ -438,6 +373,20 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return null;
}
// Dump tools commonly place decrypted executables in an app0/decrypted
// sidecar while leaving Unity data in the parent app0 directory. Keep
// loading code/modules beside the decrypted eboot, but mount /app0 at
// the content-bearing parent so boot.config, metadata and assets resolve.
if (string.Equals(Path.GetFileName(app0Root), "decrypted", StringComparison.OrdinalIgnoreCase))
{
var parentRoot = Path.GetDirectoryName(app0Root);
if (!string.IsNullOrWhiteSpace(parentRoot) &&
Directory.Exists(Path.Combine(parentRoot, "Media")))
{
app0Root = parentRoot;
}
}
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
return new App0BindingScope(app0VariableName);
}
@@ -483,6 +432,56 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return null;
}
private bool TryGetEhFrameInfo(
SelfImage image,
ulong imageSize,
out ulong ehFrameHeaderAddress,
out ulong ehFrameAddress,
out ulong ehFrameSize)
{
ehFrameHeaderAddress = 0;
ehFrameAddress = 0;
ehFrameSize = 0;
var imageBase = image.EntryPoint >= image.ElfHeader.EntryPoint
? image.EntryPoint - image.ElfHeader.EntryPoint
: 0UL;
for (var i = 0; i < image.ProgramHeaders.Count; i++)
{
var header = image.ProgramHeaders[i];
if (header.HeaderType != ProgramHeaderType.GnuEhFrame || header.MemorySize < 8)
{
continue;
}
var headerAddress = imageBase + header.VirtualAddress;
Span<byte> ehHeader = stackalloc byte[8];
if (!_virtualMemory.TryRead(headerAddress, ehHeader) ||
ehHeader[0] != 1 ||
ehHeader[1] != 0x1B)
{
continue;
}
var relativeOffset = BinaryPrimitives.ReadInt32LittleEndian(ehHeader[4..]);
ehFrameHeaderAddress = headerAddress;
ehFrameAddress = unchecked((ulong)((long)headerAddress + 4 + relativeOffset));
if (ehFrameAddress < 0x10000)
{
continue;
}
var relativeEhFrameAddress = ehFrameAddress - imageBase;
ehFrameSize = header.VirtualAddress > relativeEhFrameAddress
? header.VirtualAddress - relativeEhFrameAddress
: imageSize > header.VirtualAddress
? imageSize - header.VirtualAddress
: 0;
return true;
}
return false;
}
private OrbisGen2Result? RunPreloadedModuleInitializers(
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
Generation generation,
@@ -492,20 +491,30 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
for (var i = 0; i < loadedModuleImages.Count; i++)
{
var loadedModule = loadedModuleImages[i];
if (!loadedModule.StartAtBoot)
{
continue;
}
var initEntryPoint = loadedModule.Image.InitFunctionEntryPoint;
if (initEntryPoint < 0x10000)
{
continue;
}
if (!KernelModuleRegistry.TryBeginModuleStart(loadedModule.Handle, out _))
{
continue;
}
var moduleName = Path.GetFileName(loadedModule.Path);
if (string.IsNullOrWhiteSpace(moduleName))
{
moduleName = $"module#{i}";
}
Log.Info(
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
var result = _cpuDispatcher.DispatchModuleInitializer(
initEntryPoint,
@@ -514,10 +523,13 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
activeRuntimeSymbols,
moduleName,
_cpuExecutionOptions);
KernelModuleRegistry.CompleteModuleStart(
loadedModule.Handle,
result == OrbisGen2Result.ORBIS_GEN2_OK);
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
Log.Error(
$"Module start failed: {moduleName} -> {result}");
Console.Error.WriteLine(
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
return result;
}
}
@@ -538,8 +550,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return null;
}
Log.Info(
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
Console.Error.WriteLine(
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
var result = RunInitializerList(
$"{label}:preinit",
@@ -578,8 +590,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
continue;
}
Log.Debug(
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
var result = _cpuDispatcher.DispatchEntry(
initializerAddress,
@@ -611,12 +623,17 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
var moduleDirectories = new[]
{
Path.Combine(ebootDirectory, "sce_module"),
Path.Combine(ebootDirectory, "sce_modules"),
Path.Combine(ebootDirectory, "Media", "Modules"),
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true),
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true),
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true),
// Unity native plugins are loaded later through sceKernelLoadStartModule. Map
// them up front so the HLE loader can return a real module handle and dlsym
// can resolve their exports, but defer DT_INIT until the guest requests them.
(Path: Path.Combine(ebootDirectory, "Media", "Plugins"), StartAtBoot: false),
}
.Distinct(StringComparer.OrdinalIgnoreCase)
.Where(Directory.Exists)
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.Where(entry => Directory.Exists(entry.Path))
.ToArray();
if (moduleDirectories.Length == 0)
@@ -626,28 +643,30 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
var allModulePaths = moduleDirectories
.SelectMany(directory => Directory
.EnumerateFiles(directory)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
.Where(path =>
.EnumerateFiles(directory.Path)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.Select(path => (Path: path, directory.StartAtBoot)))
.Where(entry =>
{
var extension = Path.GetExtension(path);
var extension = Path.GetExtension(entry.Path);
return string.Equals(extension, ".prx", StringComparison.OrdinalIgnoreCase) ||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
})
.Distinct(StringComparer.OrdinalIgnoreCase)
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.ToArray();
var modulePaths = allModulePaths
.Where(ShouldPreloadModule)
.Where(entry => ShouldPreloadModule(entry.Path))
.ToArray();
var skippedModules = allModulePaths
.Where(path => !ShouldPreloadModule(path))
.Select(Path.GetFileName)
.Where(entry => !ShouldPreloadModule(entry.Path))
.Select(entry => Path.GetFileName(entry.Path))
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToArray();
if (skippedModules.Length > 0)
{
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
}
if (modulePaths.Length == 0)
@@ -655,14 +674,15 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return loadedImages;
}
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
Log.Info($"Loading {modulePaths.Length} module(s)...");
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories.Select(entry => entry.Path))}");
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
var loadedModules = 0;
var failedModules = 0;
var mergedImportCount = 0;
var mergedSymbolCount = 0;
foreach (var modulePath in modulePaths)
foreach (var moduleEntry in modulePaths)
{
var modulePath = moduleEntry.Path;
try
{
var fileInfo = new FileInfo(modulePath);
@@ -687,25 +707,52 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
mergedImportCount += MergeImportStubs(importStubs, moduleImage.ImportStubs, modulePath);
mergedSymbolCount += MergeRuntimeSymbols(runtimeSymbols, moduleImage.RuntimeSymbols);
RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
InstallNativePluginCompatibilityHooks(importStubs, moduleImage, modulePath);
var moduleHandle = RegisterLoadedModule(modulePath, moduleImage, isMain: false, isSystemModule: false);
var moduleName = Path.GetFileName(modulePath);
var startAtBoot = moduleEntry.StartAtBoot ||
string.Equals(moduleName, "libfmod.prx", StringComparison.OrdinalIgnoreCase) ||
string.Equals(moduleName, "libfmodstudio.prx", StringComparison.OrdinalIgnoreCase);
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage, moduleHandle, startAtBoot));
loadedModules++;
Log.Info(
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
Console.Error.WriteLine(
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
}
catch (Exception ex)
{
failedModules++;
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
}
}
Log.Info(
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
Console.Error.WriteLine(
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
return loadedImages;
}
private static void InstallNativePluginCompatibilityHooks(
IDictionary<ulong, string> importStubs,
SelfImage moduleImage,
string modulePath)
{
if (!string.Equals(Path.GetFileName(modulePath), "libfmod.prx", StringComparison.OrdinalIgnoreCase))
{
return;
}
ReadOnlySpan<string> compatibilityNids = ["uPLTdl3psGk"];
foreach (var nid in compatibilityNids)
{
if (moduleImage.RuntimeSymbols.TryGetValue(nid, out var address) && address >= 0x10000)
{
importStubs[address] = nid;
Console.Error.WriteLine(
$"[RUNTIME] Installed FMOD compatibility hook: {nid} -> 0x{address:X16}");
}
}
}
private void RebindImportedDataSymbols(
SelfImage mainImage,
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
@@ -722,8 +769,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (rebound != 0 || unresolved != 0)
{
Log.Info(
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
}
}
@@ -755,8 +802,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
Log.Warning(
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
}
unresolved++;
@@ -768,8 +815,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
Log.Error(
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
unresolved++;
@@ -778,8 +825,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (logRebind)
{
Log.Debug(
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
rebound++;
@@ -815,8 +862,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
{
Log.Warning(
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
Console.Error.WriteLine(
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
}
continue;
@@ -910,7 +957,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return !PreloadSkipModules.Contains(fileName);
}
private static void RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
private int RegisterLoadedModule(string modulePath, SelfImage image, bool isMain, bool isSystemModule)
{
if (!TryComputeImageRange(image, out var baseAddress, out var size))
{
@@ -918,15 +965,27 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
size = 0;
}
_ = TryGetEhFrameInfo(
image,
size,
out var ehFrameHeaderAddress,
out var ehFrameAddress,
out var ehFrameSize);
var handle = KernelModuleRegistry.RegisterModule(
modulePath,
baseAddress,
size,
image.EntryPoint,
image.InitFunctionEntryPoint,
ehFrameHeaderAddress,
ehFrameAddress,
ehFrameSize,
isMain,
isSystemModule);
Log.Info(
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
KernelModuleRegistry.RegisterModuleSymbols(handle, image.RuntimeSymbols);
Console.Error.WriteLine(
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
return handle;
}
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
+4
View File
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageReference Include="Iced" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
-155
View File
@@ -1,155 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Iced": {
"type": "Direct",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
"resolved": "3.1.6",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Input.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Silk.NET.Input.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[1.0.0, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[1.0.0, )",
"Silk.NET.Input": "[2.23.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
"Silk.NET.Windowing": "[2.23.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Silk.NET.Input": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Input.Glfw": "2.23.0"
}
},
"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"
}
}
}
}
}
+2
View File
@@ -48,6 +48,8 @@ public sealed class GuiSettings
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
public bool DiscordRichPresence { get; set; } = true;
public bool CheckForUpdatesOnStartup { 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();
+13 -5
View File
@@ -28,9 +28,9 @@
"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.Desc": "Variáveis de ambiente passadas ao emulador durante a inicialização.",
"Options.Env.Bthid.Desc": "Informa o Bluetooth HID como indisponível para títulos cujo middleware de volante/FFB fica verificando indefinidamente.\nNormalmente, deixe desativado. Alguns títulos travam quando a inicialização falha.",
"Options.Env.LoopGuard.Desc": "Não força o encerramento de títulos que repetem a mesma chamada por tempo excessivo.\nExperimente esta opção se um jogo fechar 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).",
@@ -83,7 +83,7 @@
"Console.Title": "CONSOLE",
"Console.SearchWatermark": "Pesquisar...",
"Console.AutoScroll": "Rolagem automática",
"Console.Split": "Recortar",
"Console.Split": "Dividir",
"Console.Copy": "Copiar",
"Console.Clear": "Limpar",
"Console.WindowTitle": "Console do SharpEmu",
@@ -134,5 +134,13 @@
"Dialog.PsExecutables": "Executáveis de PS",
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
"Dialog.PlainTextFiles": "Arquivos de texto simples",
"Dialog.LogFiles": "Arquivos de log"
"Dialog.LogFiles": "Arquivos de log",
"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": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Entre no nosso Discord!"
}
+20 -1
View File
@@ -31,9 +31,11 @@
"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.WritableApp0.Desc": "Allow titles to create and write files inside their install folder.\nNeeded by unpackaged dumps that write their save or config data under /app0.",
"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.LogIo.Desc": "Log file open, read, and path-resolve activity to the console.\nUse when a game cannot find its data files during boot.",
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
"Options.Section.Emulation": "EMULATION",
"Options.Section.Logging": "LOGGING",
@@ -139,8 +141,25 @@
"Options.About" : "About",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Source code, issues and project development.",
"About.Github.LatestCommitLabel": "Latest Commit",
"About.Github.LatestCommitDescription": "Latest commit on the main branch",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Join the community, get support and follow development.",
"About.GithubButton": "Contribute in GitHub!",
"About.DiscordButton": "Join our Discord!"
"About.DiscordButton": "Join our Discord!",
"Updater.Auto.Label": "Check for updates on startup",
"Updater.Auto.Desc": "Checks GitHub without delaying startup.",
"Updater.Label": "Updates",
"Updater.Check": "Check for updates",
"Updater.DownloadRestart": "Download and restart",
"Updater.Status.Ready": "Current build: {0}",
"Updater.Status.Checking": "Checking for updates…",
"Updater.Status.Current": "You are up to date ({0}).",
"Updater.Status.Available": "A new build is available: {0}",
"Updater.Status.Downloading": "Downloading update… {0}%",
"Updater.Status.Installing": "Installing update…",
"Updater.Status.Timeout": "Update check timed out after 10 seconds.",
"Updater.Status.Failed": "Could not check for updates.",
"Updater.Status.Unsupported": "Automatic updating requires a Windows, Linux or macOS x64 build."
}
+2
View File
@@ -131,6 +131,8 @@
"About.Github.Label": "GitHub",
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
"About.Discord.Label": "Discord",
"About.Github.LatestCommitLabel": "Último Commit",
"About.Github.LatestCommitDescription": "Último commit en la rama main",
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
"About.GithubButton": "Contribuye en GitHub!",
"About.DiscordButton": "Únete a nuestro Discord!"
+18 -1
View File
@@ -26,6 +26,15 @@
"Library.Loading": "Chargement de la bibliothèque…",
"Options.General": "Général",
"Options.Env.Tab": "Environnement",
"Options.Section.Environment": "VARIABLES DENVIRONNEMENT",
"Options.Env.Desc": "Paramètres passés à l’émulateur comme variables denvironnement au lancement.",
"Options.Env.Bthid.Desc": "Signaler le HID Bluetooth comme indisponible pour les titres dont le middleware volant/FFB interroge indéfiniment.\nLaissez désactivé normalement. Certains titres se bloquent si linitialisation échoue.",
"Options.Env.LoopGuard.Desc": "Ne pas forcer la fermeture des titres qui répètent le même appel trop longtemps.\nEssayez ceci quand un jeu se ferme tout seul pendant le chargement.",
"Options.Env.VkValidation.Desc": "Activer les couches de validation Vulkan pour le débogage GPU.\nRalentit. Nécessite linstallation du Vulkan SDK.",
"Options.Env.DumpSpirv.Desc": "Exporter les shaders AGC et leurs traductions SPIR-V dans le dossier shader-dumps.\nÀ utiliser pour signaler des bugs de shader ou de rendu.",
"Options.Env.LogDirectMemory.Desc": "Journaliser les allocations de mémoire directe et les échecs dans la console.\nÀ utiliser quand un jeu plante ou se ferme pendant le démarrage.",
"Options.Env.LogNp.Desc": "Journaliser les appels de la bibliothèque NP (PlayStation Network) dans la console.",
"Options.Section.Emulation": "ÉMULATION",
"Options.Section.Logging": "JOURNALISATION",
"Options.Section.Launcher": "LANCEUR",
@@ -125,5 +134,13 @@
"Dialog.PsExecutables": "Exécutables PlayStation",
"Dialog.SaveLogFile": "Choisir lemplacement du fichier journal",
"Dialog.PlainTextFiles": "Fichiers texte brut",
"Dialog.LogFiles": "Fichiers journaux"
"Dialog.LogFiles": "Fichiers journaux",
"Options.About": "À propos",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Code source, issues et développement du projet.",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Rejoignez la communauté, obtenez de laide et suivez le développement.",
"About.GithubButton": "Contribuer sur GitHub !",
"About.DiscordButton": "Rejoindre notre Discord !"
}
+16 -1
View File
@@ -125,5 +125,20 @@
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
"Dialog.LogFiles": "Günlük Dosyaları"
"Dialog.LogFiles": "Günlük Dosyaları",
"Updater.Auto.Label": "Açılışta güncellemeleri denetle",
"Updater.Auto.Desc": "Açılışı geciktirmeden GitHub'ı denetler.",
"Updater.Label": "Güncellemeler",
"Updater.Check": "Güncellemeleri denetle",
"Updater.DownloadRestart": "İndir ve yeniden başlat",
"Updater.Status.Ready": "Mevcut build: {0}",
"Updater.Status.Checking": "Güncellemeler denetleniyor…",
"Updater.Status.Current": "Güncelsiniz ({0}).",
"Updater.Status.Available": "Yeni build mevcut: {0}",
"Updater.Status.Downloading": "Güncelleme indiriliyor… %{0}",
"Updater.Status.Installing": "Güncelleme kuruluyor…",
"Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.",
"Updater.Status.Failed": "Güncellemeler denetlenemedi.",
"Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir."
}
+66
View File
@@ -315,6 +315,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="AutoUpdateLabel" Text="Check for updates on startup" FontSize="13" />
<TextBlock x:Name="AutoUpdateDesc" Text="Checks GitHub without delaying startup."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="AutoUpdateToggle" OnContent="On" OffContent="Off"
IsChecked="True" VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
<Border Classes="card">
@@ -322,6 +332,40 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TextBlock x:Name="AboutSectionTitle"
Classes="sectionTitle"
Text="ABOUT" />
<!--Latest commit info-->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
Width="24" Height="24" VerticalAlignment="Center" />
<StackPanel Spacing="2" VerticalAlignment="Center">
<TextBlock x:Name="LatestCommitLabel" Text="Latest commit" FontSize="13"/>
<TextBlock x:Name="LatestCommitDescription" Text="Latest commit on the main branch" FontSize="11" Foreground="{StaticResource MutedBrush}"/>
</StackPanel>
</StackPanel>
<Button Grid.Column="1" x:Name="LatestCommitHashText" Classes="ghost"
Content="Loading…" FontSize="12" FontFamily="Consolas,monospace"
VerticalAlignment="Center" IsEnabled="False" />
</Grid>
<!--Update-->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/update-icon.png"
Width="24"
Height="24"
VerticalAlignment="Center" />
<StackPanel Spacing="2" VerticalAlignment="Center">
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
Content="Check for updates" VerticalAlignment="Center" />
</Grid>
<!--Github-->
<Grid ColumnDefinitions="*,Auto">
@@ -420,6 +464,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_WRITABLE_APP0" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvWritableApp0Desc"
Text="Allow titles to create and write files inside their install folder.&#10;Needed by unpackaged dumps that write their save or config data under /app0."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvWritableApp0Toggle" 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" />
@@ -453,6 +508,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
VerticalAlignment="Center" />
</Grid>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="SHARPEMU_LOG_IO" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogIoDesc"
Text="Log file open, read, and path-resolve activity to the console.&#10;Use when a game cannot find its data files during boot."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogIoToggle" 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" />
+217 -10
View File
@@ -20,6 +20,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Net.Http.Headers;
namespace SharpEmu.GUI;
@@ -34,6 +35,12 @@ public partial class MainWindow : Window
private static readonly IBrush WarningLineBrush = new SolidColorBrush(Color.Parse("#E8B341"));
private static readonly IBrush ErrorLineBrush = new SolidColorBrush(Color.Parse("#F2777C"));
private static readonly IBrush SuccessLineBrush = new SolidColorBrush(Color.Parse("#63D489"));
private static readonly StringComparer FilePathComparer = OperatingSystem.IsWindows()
? StringComparer.OrdinalIgnoreCase
: StringComparer.Ordinal;
private static readonly StringComparison FilePathComparison = OperatingSystem.IsWindows()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
private readonly List<GameEntry> _allGames = new();
private readonly ObservableCollection<GameEntry> _visibleGames = new();
@@ -51,6 +58,9 @@ public partial class MainWindow : Window
private bool _isRunning;
private int _autoScrollTicks;
private int _activePageIndex;
private Updater.UpdateInfo? _availableUpdate;
private string _updateStatusKey = "Updater.Status.Ready";
private object?[] _updateStatusArgs = [BuildInfo.CommitSha ?? "dev"];
// Discord Rich Presence state.
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
@@ -69,6 +79,10 @@ public partial class MainWindow : Window
private long _navUpNextAt;
private long _navDownNextAt;
//Github http client for latest commit
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
private string? _latestCommitSha;
public MainWindow()
{
InitializeComponent();
@@ -125,17 +139,24 @@ public partial class MainWindow : Window
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
UpdateDiscordPresence();
};
AutoUpdateToggle.IsCheckedChanged += (_, _) =>
_settings.CheckForUpdatesOnStartup = AutoUpdateToggle.IsChecked == true;
UpdateButton.Click += async (_, _) => await OnUpdateButtonAsync();
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);
EnvWritableApp0Toggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_WRITABLE_APP0", EnvWritableApp0Toggle.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);
EnvLogIoToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_IO", EnvLogIoToggle.IsChecked == true);
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
@@ -166,7 +187,7 @@ public partial class MainWindow : Window
{
Process.Start(new ProcessStartInfo
{
FileName = "https://github.com/par274/sharpemu",
FileName = "https://github.com/sharpemu/sharpemu",
UseShellExecute = true
});
};
@@ -179,6 +200,21 @@ public partial class MainWindow : Window
UseShellExecute = true
});
};
LatestCommitHashText.Click += (_, _) =>
{
if (string.IsNullOrWhiteSpace(_latestCommitSha))
{
return;
}
Process.Start(new ProcessStartInfo
{
FileName =
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
UseShellExecute = true
});
};
}
/// <summary>
@@ -220,6 +256,91 @@ public partial class MainWindow : Window
}
}
// ---- Github http client config ----
// This is for getting lash commit id
private static HttpClient CreateGithubHttpClient()
{
var client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(15)
};
client.DefaultRequestHeaders.UserAgent.ParseAdd("SharpEmu/1.0");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.github.sha"));
client.DefaultRequestHeaders.Add(
"X-GitHub-Api-Version",
"2026-03-10");
return client;
}
private async Task LoadLatestCommitAsync()
{
const string apiUrl =
"https://api.github.com/repos/sharpemu/sharpemu/commits/main";
_latestCommitSha = null;
LatestCommitHashText.Content = "Loading…";
LatestCommitHashText.IsEnabled = false;
try
{
using var response = await GithubHttpClient.GetAsync(apiUrl);
var responseBody =
(await response.Content.ReadAsStringAsync()).Trim();
if (!response.IsSuccessStatusCode)
{
LatestCommitHashText.Content =
$"HTTP {(int)response.StatusCode}";
ToolTip.SetTip(
LatestCommitHashText,
string.IsNullOrWhiteSpace(responseBody)
? response.ReasonPhrase
: responseBody);
return;
}
if (responseBody.Length < 7)
{
LatestCommitHashText.Content = "Invalid response";
ToolTip.SetTip(LatestCommitHashText, responseBody);
return;
}
// Keep the complete SHA for the URL.
_latestCommitSha = responseBody;
// Display only the short SHA.
LatestCommitHashText.Content =
responseBody[..Math.Min(7, responseBody.Length)];
LatestCommitHashText.IsEnabled = true;
ToolTip.SetTip(
LatestCommitHashText,
$"Open commit {_latestCommitSha}");
}
catch (TaskCanceledException ex)
{
LatestCommitHashText.Content = "Timeout";
ToolTip.SetTip(LatestCommitHashText, ex.Message);
}
catch (HttpRequestException ex)
{
LatestCommitHashText.Content = "Connection error";
ToolTip.SetTip(LatestCommitHashText, ex.Message);
}
catch (Exception ex)
{
LatestCommitHashText.Content = "Error";
ToolTip.SetTip(LatestCommitHashText, ex.Message);
}
}
// ---- Controller navigation ----
private void PollGamepad()
@@ -365,6 +486,12 @@ public partial class MainWindow : Window
ApplySettingsToControls();
LocateEmulator();
UpdateDiscordPresence();
_ = LoadLatestCommitAsync();
if (_settings.CheckForUpdatesOnStartup)
{
_ = CheckForUpdatesAsync();
}
await RescanLibraryAsync();
}
@@ -421,9 +548,11 @@ public partial class MainWindow : Window
EnvDesc.Text = loc.Get("Options.Env.Desc");
EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
EnvWritableApp0Desc.Text = loc.Get("Options.Env.WritableApp0.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");
EnvLogIoDesc.Text = loc.Get("Options.Env.LogIo.Desc");
EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
@@ -466,8 +595,10 @@ public partial class MainWindow : Window
DiscordLabel.Text = loc.Get("Options.Discord.Label");
DiscordDesc.Text = loc.Get("Options.Discord.Desc");
AutoUpdateLabel.Text = loc.Get("Updater.Auto.Label");
AutoUpdateDesc.Text = loc.Get("Updater.Auto.Desc");
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle })
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle })
{
toggle.OnContent = loc.Get("Common.On");
toggle.OffContent = loc.Get("Common.Off");
@@ -491,6 +622,10 @@ public partial class MainWindow : Window
DiscordServerDesc.Text = loc.Get("About.Discord.Desc");
GithubButton.Content = loc.Get("About.GithubButton");
DiscordButton.Content = loc.Get("About.DiscordButton");
UpdateLabel.Text = loc.Get("Updater.Label");
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
RefreshUpdateText();
UpdateEmptyStateTexts();
UpdateSelectedGameTexts();
@@ -606,15 +741,87 @@ public partial class MainWindow : Window
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
AutoUpdateToggle.IsChecked = _settings.CheckForUpdatesOnStartup;
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
EnvWritableApp0Toggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_WRITABLE_APP0");
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");
EnvLogIoToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_IO");
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
UpdateLogFilePathText();
}
private async Task OnUpdateButtonAsync()
{
if (_availableUpdate is null)
{
await CheckForUpdatesAsync();
return;
}
UpdateButton.IsEnabled = false;
try
{
var progress = new Progress<int>(value =>
SetUpdateStatus("Updater.Status.Downloading", value));
await Updater.DownloadAndRestartAsync(_availableUpdate, progress);
SetUpdateStatus("Updater.Status.Installing");
Close();
}
catch
{
SetUpdateStatus("Updater.Status.Failed");
UpdateButton.IsEnabled = true;
}
}
private async Task CheckForUpdatesAsync()
{
_availableUpdate = null;
UpdateButton.IsEnabled = false;
SetUpdateStatus("Updater.Status.Checking");
try
{
_availableUpdate = await Updater.CheckAsync(BuildInfo.CommitSha);
SetUpdateStatus(
_availableUpdate is null ? "Updater.Status.Current" : "Updater.Status.Available",
_availableUpdate?.Sha ?? BuildInfo.CommitSha ?? "dev");
}
catch (OperationCanceledException)
{
SetUpdateStatus("Updater.Status.Timeout");
}
catch (PlatformNotSupportedException)
{
SetUpdateStatus("Updater.Status.Unsupported");
}
catch
{
SetUpdateStatus("Updater.Status.Failed");
}
finally
{
UpdateButton.IsEnabled = true;
RefreshUpdateText();
}
}
private void SetUpdateStatus(string key, params object?[] args)
{
_updateStatusKey = key;
_updateStatusArgs = args;
RefreshUpdateText();
}
private void RefreshUpdateText()
{
UpdateStatusText.Text = Localization.Instance.Format(_updateStatusKey, _updateStatusArgs);
UpdateButton.Content = Localization.Instance.Get(
_availableUpdate is null ? "Updater.Check" : "Updater.DownloadRestart");
}
// 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);
@@ -727,7 +934,7 @@ public partial class MainWindow : Window
}
var changed = false;
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
if (!_settings.GameFolders.Contains(path, FilePathComparer))
{
_settings.GameFolders.Add(path);
changed = true;
@@ -737,7 +944,7 @@ public partial class MainWindow : Window
// games beneath it that were removed from the library earlier.
var prefix = Path.TrimEndingDirectorySeparator(path) + Path.DirectorySeparatorChar;
changed |= _settings.ExcludedGames.RemoveAll(excluded =>
excluded.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) > 0;
excluded.StartsWith(prefix, FilePathComparison)) > 0;
if (changed)
{
@@ -750,7 +957,7 @@ public partial class MainWindow : Window
private async Task RescanLibraryAsync()
{
var folders = _settings.GameFolders.ToArray();
var excluded = new HashSet<string>(_settings.ExcludedGames, StringComparer.OrdinalIgnoreCase);
var excluded = new HashSet<string>(_settings.ExcludedGames, FilePathComparer);
StatusBarRight.Text = Localization.Instance.Get("Status.ScanningLibrary");
EmptyState.IsVisible = false;
LoadingState.IsVisible = true;
@@ -868,7 +1075,7 @@ public partial class MainWindow : Window
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders, IReadOnlySet<string> excludedPaths)
{
var games = new List<GameEntry>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var seen = new HashSet<string>(FilePathComparer);
var enumeration = new EnumerationOptions
{
IgnoreInaccessible = true,
@@ -1132,13 +1339,13 @@ public partial class MainWindow : Window
return;
}
if (!_settings.ExcludedGames.Contains(game.Path, StringComparer.OrdinalIgnoreCase))
if (!_settings.ExcludedGames.Contains(game.Path, FilePathComparer))
{
_settings.ExcludedGames.Add(game.Path);
_settings.Save();
}
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, StringComparison.OrdinalIgnoreCase));
_allGames.RemoveAll(g => string.Equals(g.Path, game.Path, FilePathComparison));
GameList.SelectedItem = null;
RefreshVisibleGames();
StatusBarRight.Text = Localization.Instance.Format("Status.RemovedFromLibrary", game.Name);
@@ -1162,7 +1369,7 @@ public partial class MainWindow : Window
}
if (selectedPath is not null &&
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, StringComparison.OrdinalIgnoreCase))
_visibleGames.FirstOrDefault(g => g.Path.Equals(selectedPath, FilePathComparison))
is { } reselected)
{
GameList.SelectedItem = reselected;
@@ -1487,7 +1694,7 @@ public partial class MainWindow : Window
_isRunning = true;
_runningGameName = displayName;
_runningGameTitleId = _allGames
.FirstOrDefault(game => game.Path.Equals(ebootPath, StringComparison.OrdinalIgnoreCase))?
.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?
.TitleId;
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
StatusDot.Fill = SuccessLineBrush;
+2 -1
View File
@@ -9,7 +9,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
the executable is started without arguments. -->
<PropertyGroup>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<Version>0.0.1</Version>
<!-- Required by the source-generated LibraryImport stubs in the linked
controller readers below. -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@@ -33,6 +32,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
</ItemGroup>
<ItemGroup>
+253
View File
@@ -0,0 +1,253 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
using System.Formats.Tar;
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using System.Text.Json;
namespace SharpEmu.GUI;
/// <summary>Self-contained Windows updater; the emulator layers do not depend on it.</summary>
public static class Updater
{
private const string ApplyArgument = "--sharpemu-apply-update";
private const string LatestReleaseUrl = "https://api.github.com/repos/sharpemu/sharpemu/releases/latest";
private static readonly TimeSpan CheckTimeout = TimeSpan.FromSeconds(10);
private static readonly HttpClient Http = CreateHttpClient();
public sealed record UpdateInfo(string Sha, string Name, string DownloadUrl, long Size);
public static async Task<UpdateInfo?> CheckAsync(string? currentSha, CancellationToken cancellationToken = default)
{
var platform = CurrentPlatform();
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(CheckTimeout);
using var response = await Http.GetAsync(LatestReleaseUrl, timeout.Token);
response.EnsureSuccessStatusCode();
return ParseRelease(
await response.Content.ReadAsStringAsync(timeout.Token),
currentSha,
platform.Rid,
platform.Extension);
}
public static async Task DownloadAndRestartAsync(
UpdateInfo update,
IProgress<int>? progress = null,
CancellationToken cancellationToken = default)
{
var root = Path.Combine(Path.GetTempPath(), "SharpEmu.Update");
var payload = Path.Combine(root, "payload");
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
Directory.CreateDirectory(root);
var archive = Path.Combine(root, update.Name);
using (var response = await Http.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{
response.EnsureSuccessStatusCode();
await using var input = await response.Content.ReadAsStreamAsync(cancellationToken);
await using var output = File.Create(archive);
var buffer = new byte[81920];
long written = 0;
int read;
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
written += read;
progress?.Report(update.Size == 0 ? 0 : (int)(written * 100 / update.Size));
}
if (written != update.Size)
{
throw new InvalidDataException($"Downloaded {written} bytes; expected {update.Size}.");
}
}
var platform = CurrentPlatform();
var stagedExe = ExtractArchive(archive, payload, platform.Extension, platform.ExecutableName);
var start = new ProcessStartInfo(stagedExe)
{
UseShellExecute = false,
WorkingDirectory = payload,
};
start.ArgumentList.Add(ApplyArgument);
start.ArgumentList.Add(Environment.ProcessId.ToString());
start.ArgumentList.Add(AppContext.BaseDirectory);
using var helper = Process.Start(start)
?? throw new InvalidOperationException("The update installer could not be started.");
}
/// <summary>Runs from the downloaded executable after the old GUI exits.</summary>
public static bool TryApply(string[] args, out int exitCode)
{
exitCode = 0;
if (args.Length != 3 || args[0] != ApplyArgument)
{
return false;
}
try
{
if (int.TryParse(args[1], out var oldPid))
{
try
{
if (!Process.GetProcessById(oldPid).WaitForExit(30_000))
{
throw new TimeoutException("SharpEmu did not close within 30 seconds.");
}
}
catch (ArgumentException)
{
// The old process has already exited.
}
}
var source = AppContext.BaseDirectory;
var target = Path.GetFullPath(args[2]);
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
{
var relative = Path.GetRelativePath(source, file);
if (relative.Equals("gui-settings.json", StringComparison.OrdinalIgnoreCase) ||
relative.StartsWith("user" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
relative.StartsWith("logs" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
relative.StartsWith("Languages" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var destination = Path.Combine(target, relative);
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
File.Copy(file, destination, overwrite: true);
if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(destination, File.GetUnixFileMode(file));
}
}
using var restarted = Process.Start(new ProcessStartInfo(
Path.Combine(target, CurrentPlatform().ExecutableName))
{
UseShellExecute = false,
WorkingDirectory = target,
}) ?? throw new InvalidOperationException("The updated SharpEmu could not be started.");
}
catch (Exception ex)
{
exitCode = 1;
try
{
File.WriteAllText(Path.Combine(args[2], "update-error.log"), ex.ToString());
}
catch
{
// Best-effort diagnostics only.
}
}
return true;
}
private static UpdateInfo? ParseRelease(
string json,
string? currentSha,
string rid,
string extension)
{
using var document = JsonDocument.Parse(json);
var candidates = new List<(DateTimeOffset Created, UpdateInfo Update)>();
foreach (var asset in document.RootElement.GetProperty("assets").EnumerateArray())
{
var name = asset.GetProperty("name").GetString() ?? "";
var marker = $"-{rid}-";
var markerIndex = name.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase);
if (!name.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ||
markerIndex < 0)
{
continue;
}
var sha = name[(markerIndex + marker.Length)..^extension.Length];
if (sha.Length < 7 || !sha.All(Uri.IsHexDigit))
{
continue;
}
candidates.Add((
asset.GetProperty("created_at").GetDateTimeOffset(),
new UpdateInfo(
sha,
name,
asset.GetProperty("browser_download_url").GetString()!,
asset.GetProperty("size").GetInt64())));
}
var latest = candidates.OrderByDescending(candidate => candidate.Created).FirstOrDefault().Update;
return latest is null || string.Equals(latest.Sha, currentSha, StringComparison.OrdinalIgnoreCase)
? null
: latest;
}
private static string ExtractArchive(
string archive,
string payload,
string extension,
string executableName)
{
if (extension == ".zip")
{
ZipFile.ExtractToDirectory(archive, payload);
}
else
{
Directory.CreateDirectory(payload);
using var compressed = File.OpenRead(archive);
using var gzip = new GZipStream(compressed, CompressionMode.Decompress);
TarFile.ExtractToDirectory(gzip, payload, overwriteFiles: false);
}
var executable = Path.Combine(payload, executableName);
if (!File.Exists(executable))
{
throw new InvalidDataException($"The update archive does not contain {executableName}.");
}
if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(executable, File.GetUnixFileMode(executable) | UnixFileMode.UserExecute);
}
return executable;
}
private static HttpClient CreateHttpClient()
{
var client = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("SharpEmu", "0.0.1"));
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
return client;
}
private static PlatformInfo CurrentPlatform()
{
if (RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
throw new PlatformNotSupportedException("SharpEmu releases require an x64 process.");
}
if (OperatingSystem.IsWindows()) return new("win-x64", ".zip", "SharpEmu.exe");
if (OperatingSystem.IsLinux()) return new("linux-x64", ".tar.gz", "SharpEmu");
if (OperatingSystem.IsMacOS()) return new("osx-x64", ".tar.gz", "SharpEmu");
throw new PlatformNotSupportedException();
}
private sealed record PlatformInfo(string Rid, string Extension, string ExecutableName);
}
-189
View File
@@ -1,189 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Avalonia": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "Direct",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Tmds.DBus.Protocol": {
"type": "Direct",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
},
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.BuildServices": {
"type": "Transitive",
"resolved": "11.3.2",
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
},
"Avalonia.FreeDesktop": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
"dependencies": {
"Avalonia": "11.3.18",
"Tmds.DBus.Protocol": "0.21.3"
}
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Remote.Protocol": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
},
"Avalonia.Skia": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
"dependencies": {
"Avalonia": "11.3.18",
"HarfBuzzSharp": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
"SkiaSharp": "2.88.9",
"SkiaSharp.NativeAssets.Linux": "2.88.9",
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
}
},
"Avalonia.Win32": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
}
},
"Avalonia.X11": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.FreeDesktop": "11.3.18",
"Avalonia.Skia": "11.3.18"
}
},
"HarfBuzzSharp": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
"dependencies": {
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"MicroCom.Runtime": {
"type": "Transitive",
"resolved": "0.11.0",
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"sharpemu.logging": {
"type": "Project"
}
}
}
}
Binary file not shown.
+6
View File
@@ -26,6 +26,12 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
public ulong GsBase { get; set; }
/// <summary>x87 control word observed at the current guest boundary.</summary>
public ushort FpuControlWord { get; set; } = 0x037F;
/// <summary>MXCSR observed at the current guest boundary.</summary>
public uint Mxcsr { get; set; } = 0x1F80;
public ulong this[CpuRegister register]
{
get => _registers[(int)register];
+20
View File
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
/// <summary>
/// Marks a string parameter of a [SysAbiExport] handler as a guest null-terminated
/// UTF-8 pointer. The generated thunk reads the string from the argument register's
/// address (bounded by <see cref="MaxLength"/>) before invoking the handler, and
/// returns ORBIS_GEN2_ERROR_MEMORY_FAULT to the guest when the read fails — including
/// a null pointer, matching TryReadNullTerminatedUtf8's contract.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)]
public sealed class GuestCStringAttribute : Attribute
{
public GuestCStringAttribute(int maxLength) => MaxLength = maxLength;
/// <summary>Upper bound in bytes for the guest read, terminator included.</summary>
public int MaxLength { get; }
}
+613
View File
@@ -0,0 +1,613 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Globalization;
using System.Runtime.InteropServices;
namespace SharpEmu.HLE;
/// <summary>
/// Detects guest CPU writes into memory that backs a host GPU image. On PS5
/// render targets alias unified memory, so games freely mix CPU writes and GPU
/// draws on the same surface (Chowdren titles memset their fog layers every
/// frame). Host GPU images are separate storage, so the video backend needs to
/// know when the guest CPU rewrote a surface to re-upload it. Ranges are
/// write-protected; the first write faults, the fault handler restores write
/// access and marks the range dirty, and the video backend consumes the dirty
/// flag once per flip and re-arms protection after re-uploading.
/// </summary>
public static unsafe class GuestImageWriteTracker
{
private const int ProtRead = 0x1;
private const int ProtWrite = 0x2;
private const int ClockMonotonicRaw = 4;
private sealed class TrackedRange
{
public ulong Address;
public ulong ByteCount;
public ulong Start;
public ulong End;
public int Dirty;
public int Armed;
public int FirstCpuWriteSeen;
public int PendingFirstCpuWrite;
public bool TraceLifetime;
public long SourceSequence;
public long FirstCpuWriteTraceSequence;
public long FirstCpuWriteTimestampNanoseconds;
public ulong FirstCpuWriteAddress;
public ulong FirstCpuWritePage;
public string Source = "unspecified";
}
[StructLayout(LayoutKind.Sequential)]
private struct Timespec
{
public long Seconds;
public long Nanoseconds;
}
private static readonly object _gate = new();
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
// Snapshot array read lock-free from the signal handler; rebuilt on every
// mutation under the gate. Signal handlers must not take managed locks.
private static TrackedRange[] _rangeSnapshot = [];
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
private static readonly (bool Wildcard, string[] Sources) _lifetimeSourceTraceFilter =
ParseSourceList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_MEMORY_LIFETIME"));
private static readonly bool _lifetimeTraceEnabled =
_lifetimeTraceFilter.Wildcard ||
_lifetimeTraceFilter.Addresses.Length != 0 ||
_lifetimeSourceTraceFilter.Wildcard ||
_lifetimeSourceTraceFilter.Sources.Length != 0;
private static readonly long _lifetimeTraceEpochNanoseconds =
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
private static long _lifetimeTraceSequence;
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
private static extern int Mprotect(nint address, nuint length, int protection);
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
private static extern int ClockGetTime(int clockId, Timespec* time);
public static bool Enabled => _enabled;
/// <summary>
/// Exercises the fault-handling path once outside signal context so every
/// branch is JIT-compiled (and, under Rosetta 2, translated) before a real
/// fault arrives — a cold signal path is silently never entered there.
/// </summary>
public static void WarmUp()
{
if (!_enabled)
{
return;
}
var scratch = NativeMemory.AllocZeroed(4096);
try
{
// Warm the timestamp P/Invoke used by the signal-safe scalar
// capture path before a real protected-page write reaches it.
_ = GetMonotonicNanoseconds();
var address = (ulong)scratch;
Track(address, 4096);
_ = TryHandleWriteFault(address);
_ = ConsumeDirty(address);
Untrack(address);
}
finally
{
NativeMemory.Free(scratch);
}
}
/// <summary>Registers a range and arms write protection on it.</summary>
public static void Track(
ulong address,
ulong byteCount,
long sourceSequence = 0,
string source = "unspecified")
{
if (!_enabled || address == 0 || byteCount == 0)
{
return;
}
var (start, length) = PageAlign(address, byteCount);
lock (_gate)
{
_rangesByAddress.TryGetValue(address, out var range);
if (range is not null &&
(range.Start != start ||
range.End != start + length ||
range.ByteCount != byteCount))
{
// Never resize an object that is still reachable from the
// signal handler's lock-free snapshot. Retire it and publish
// a fresh immutable range.
DisarmLocked(range, "replace-range");
_rangesByAddress.Remove(address);
range = null;
}
if (range is null)
{
range = new TrackedRange
{
Address = address,
ByteCount = byteCount,
Start = start,
End = start + length,
TraceLifetime =
ShouldTraceRange(start, start + length) || ShouldTraceSource(source),
SourceSequence = sourceSequence,
Source = source,
};
_rangesByAddress[address] = range;
RebuildSnapshotLocked();
}
else
{
FlushPendingFirstCpuWrite(range);
}
range.SourceSequence = sourceSequence;
range.Source = source;
range.TraceLifetime =
ShouldTraceRange(range.Start, range.End) || ShouldTraceSource(source);
ArmLocked(range, "arm");
}
}
public static void Untrack(ulong address)
{
if (!_enabled)
{
return;
}
lock (_gate)
{
if (_rangesByAddress.TryGetValue(address, out var range))
{
DisarmLocked(range, "untrack");
_rangesByAddress.Remove(address);
RebuildSnapshotLocked();
}
}
}
/// <summary>
/// Returns true when the guest CPU wrote the range since the last call,
/// clearing the flag. The caller re-arms via <see cref="Rearm"/> after it
/// finished reading the guest bytes.
/// </summary>
public static bool ConsumeDirty(ulong address)
{
if (!_enabled)
{
return false;
}
lock (_gate)
{
if (!_rangesByAddress.TryGetValue(address, out var range))
{
return false;
}
FlushPendingFirstCpuWrite(range);
return Interlocked.Exchange(ref range.Dirty, 0) != 0;
}
}
/// <summary>
/// Non-consuming variant of <see cref="ConsumeDirty"/>: reports whether
/// the range has been written since it was last re-armed, leaving the
/// flag for the owner that evicts and re-uploads.
/// </summary>
public static bool PeekDirty(ulong address)
{
if (!_enabled)
{
return false;
}
lock (_gate)
{
if (!_rangesByAddress.TryGetValue(address, out var range))
{
return false;
}
FlushPendingFirstCpuWrite(range);
return Volatile.Read(ref range.Dirty) != 0;
}
}
public static void Rearm(ulong address)
{
if (!_enabled)
{
return;
}
lock (_gate)
{
if (_rangesByAddress.TryGetValue(address, out var range))
{
ArmLocked(range, "rearm");
}
}
}
/// <summary>
/// Prepares pages touched by a managed HLE memory write. Native guest
/// stores fault and enter <see cref="TryHandleWriteFault"/> through the
/// POSIX signal bridge, but a managed Buffer.MemoryCopy into a protected
/// page is surfaced by the runtime as a fatal AccessViolation instead of
/// a resumable guest fault. Visit every page in the write span up front so
/// all overlapping texture owners are dirtied and made writable.
/// </summary>
public static void NotifyManagedWrite(ulong address, ulong byteCount)
{
if (!_enabled || address == 0 || byteCount == 0)
{
return;
}
var end = address > ulong.MaxValue - byteCount
? ulong.MaxValue
: address + byteCount;
var candidate = address;
while (candidate < end)
{
_ = TryHandleWriteFault(candidate);
var nextPage = (candidate & ~0xFFFUL) + 0x1000UL;
if (nextPage <= candidate)
{
break;
}
candidate = nextPage;
}
}
/// <summary>
/// Flushes scalar first-write records captured by the POSIX signal handler.
/// Call only from ordinary managed execution, never from signal context.
/// </summary>
public static void FlushPendingDiagnostics()
{
if (!_enabled || !_lifetimeTraceEnabled)
{
return;
}
lock (_gate)
{
foreach (var range in _rangesByAddress.Values)
{
FlushPendingFirstCpuWrite(range);
}
}
}
/// <summary>
/// Signal-handler entry: if the fault address lies in a tracked, armed
/// range, restore write access, mark the range dirty, and return true so
/// the faulting write can be retried. Must not allocate or lock.
/// </summary>
public static bool TryHandleWriteFault(ulong faultAddress)
{
if (!_enabled || faultAddress == 0)
{
return false;
}
var ranges = Volatile.Read(ref _rangeSnapshot);
var writableStart = ulong.MaxValue;
var writableEnd = 0UL;
for (var index = 0; index < ranges.Length; index++)
{
var range = ranges[index];
if (faultAddress < range.Start || faultAddress >= range.End)
{
continue;
}
writableStart = Math.Min(writableStart, range.Start);
writableEnd = Math.Max(writableEnd, range.End);
}
if (writableStart == ulong.MaxValue)
{
return false;
}
// Ranges are page-aligned and may overlap (font atlases and other
// suballocations commonly share pages). Unprotecting one range also
// makes every overlapping tracked page writable. Expand to the full
// transitive overlap and dirty/disarm every owner, otherwise only the
// first dictionary entry observes the write and the others retain a
// stale cached texture indefinitely.
var expanded = true;
while (expanded)
{
expanded = false;
for (var index = 0; index < ranges.Length; index++)
{
var range = ranges[index];
if (range.Start >= writableEnd || range.End <= writableStart)
{
continue;
}
var start = Math.Min(writableStart, range.Start);
var end = Math.Max(writableEnd, range.End);
if (start != writableStart || end != writableEnd)
{
writableStart = start;
writableEnd = end;
expanded = true;
}
}
}
var needsUnprotect = false;
for (var index = 0; index < ranges.Length; index++)
{
var range = ranges[index];
if (range.Start < writableEnd && range.End > writableStart &&
Volatile.Read(ref range.Armed) != 0)
{
needsUnprotect = true;
break;
}
}
if (needsUnprotect &&
Mprotect(
(nint)writableStart,
(nuint)(writableEnd - writableStart),
ProtRead | ProtWrite) != 0)
{
return false;
}
for (var index = 0; index < ranges.Length; index++)
{
var range = ranges[index];
if (range.Start >= writableEnd || range.End <= writableStart)
{
continue;
}
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) != 0;
if (wasArmed &&
range.TraceLifetime &&
Interlocked.CompareExchange(ref range.FirstCpuWriteSeen, 1, 0) == 0)
{
// Signal context: capture preallocated scalar fields only.
// Formatting and I/O are deferred to a locked safe path.
range.FirstCpuWriteTraceSequence =
Interlocked.Increment(ref _lifetimeTraceSequence);
range.FirstCpuWriteTimestampNanoseconds = GetMonotonicNanoseconds();
range.FirstCpuWriteAddress = faultAddress;
range.FirstCpuWritePage = faultAddress & ~0xFFFUL;
Volatile.Write(ref range.PendingFirstCpuWrite, 1);
Volatile.Write(ref range.FirstCpuWriteSeen, 2);
}
Volatile.Write(ref range.Dirty, 1);
}
return true;
}
private static void ArmLocked(TrackedRange range, string operation)
{
FlushPendingFirstCpuWrite(range);
if (Interlocked.Exchange(ref range.Armed, 1) == 1)
{
return;
}
// A new publication/rearm starts a new first-write lifetime.
Volatile.Write(ref range.FirstCpuWriteSeen, 0);
var failed = Mprotect(
(nint)range.Start,
(nuint)(range.End - range.Start),
ProtRead) != 0;
if (failed)
{
Volatile.Write(ref range.Armed, 0);
}
if (range.TraceLifetime)
{
TraceLifetime(
range,
failed ? $"{operation}-failed-errno-{Marshal.GetLastPInvokeError()}" : operation);
}
}
private static void DisarmLocked(TrackedRange range, string operation)
{
FlushPendingFirstCpuWrite(range);
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1;
if (wasArmed)
{
_ = Mprotect(
(nint)range.Start,
(nuint)(range.End - range.Start),
ProtRead | ProtWrite);
}
if (range.TraceLifetime)
{
TraceLifetime(range, wasArmed ? operation : $"{operation}-already-disarmed");
}
}
private static void RebuildSnapshotLocked()
{
_rangeSnapshot = _rangesByAddress.Values.ToArray();
}
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
{
const ulong pageMask = 0xFFFUL;
var start = address & ~pageMask;
var end = (address + byteCount + pageMask) & ~pageMask;
return (start, end - start);
}
private static bool ShouldTraceRange(ulong start, ulong end)
{
if (_lifetimeTraceFilter.Wildcard)
{
return true;
}
var addresses = _lifetimeTraceFilter.Addresses;
for (var index = 0; index < addresses.Length; index++)
{
if (addresses[index] >= start && addresses[index] < end)
{
return true;
}
}
return false;
}
private static (bool Wildcard, ulong[] Addresses) ParseAddressList(string? addresses)
{
if (string.IsNullOrWhiteSpace(addresses))
{
return (false, []);
}
var parsedAddresses = new List<ulong>();
foreach (var token in addresses.Split(
[',', ';', ' ', '\t'],
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (token == "*")
{
return (true, []);
}
var span = token.AsSpan();
if (span.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
span = span[2..];
}
if (ulong.TryParse(span, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
{
parsedAddresses.Add(parsed);
}
}
return (false, parsedAddresses.ToArray());
}
private static bool ShouldTraceSource(string source)
{
if (_lifetimeSourceTraceFilter.Wildcard)
{
return true;
}
return Array.IndexOf(_lifetimeSourceTraceFilter.Sources, source) >= 0;
}
private static (bool Wildcard, string[] Sources) ParseSourceList(string? sources)
{
if (string.IsNullOrWhiteSpace(sources))
{
return (false, []);
}
var parsedSources = new List<string>();
foreach (var token in sources.Split(
[',', ';'],
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
if (token == "*")
{
return (true, []);
}
parsedSources.Add(token);
}
return (false, parsedSources.ToArray());
}
private static void FlushPendingFirstCpuWrite(TrackedRange range)
{
var spin = new SpinWait();
while (Volatile.Read(ref range.FirstCpuWriteSeen) == 1)
{
spin.SpinOnce();
}
if (!range.TraceLifetime || Interlocked.Exchange(ref range.PendingFirstCpuWrite, 0) == 0)
{
return;
}
TraceLifetime(
range,
"first-cpu-write-disarm",
range.FirstCpuWriteAddress,
range.FirstCpuWritePage,
range.FirstCpuWriteTraceSequence,
range.FirstCpuWriteTimestampNanoseconds);
}
private static void TraceLifetime(
TrackedRange range,
string operation,
ulong faultAddress = 0,
ulong faultPage = 0,
long traceSequence = 0,
long timestampNanoseconds = 0)
{
if (traceSequence == 0)
{
traceSequence = Interlocked.Increment(ref _lifetimeTraceSequence);
}
if (timestampNanoseconds == 0)
{
timestampNanoseconds = GetMonotonicNanoseconds();
}
var elapsedMilliseconds =
(timestampNanoseconds - _lifetimeTraceEpochNanoseconds) / 1_000_000.0;
Console.Error.WriteLine(
$"[WT][LIFETIME] seq={traceSequence} t_ms={elapsedMilliseconds:F3} " +
$"event={operation} source_seq={range.SourceSequence} source='{range.Source}' " +
$"requested=0x{range.Address:X16}+0x{range.ByteCount:X} " +
$"range=0x{range.Start:X16}..0x{range.End:X16} " +
$"fault=0x{faultAddress:X16} page=0x{faultPage:X16}");
}
private static long GetMonotonicNanoseconds()
{
Timespec time;
return ClockGetTime(ClockMonotonicRaw, &time) == 0
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
: 0;
}
}
+112 -2
View File
@@ -41,6 +41,14 @@ public interface IGuestThreadScheduler
{
bool SupportsGuestContextTransfer { get; }
/// <summary>
/// Associates a pthread identity created on the primary guest executor
/// with its live CPU context. Primary execution does not pass through
/// TryStartThread, but kernel exception delivery must still be able to
/// target it.
/// </summary>
void RegisterGuestThreadContext(ulong threadHandle, CpuContext context);
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
bool TryJoinThread(
@@ -53,6 +61,19 @@ public interface IGuestThreadScheduler
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
/// <summary>
/// Applies a new guest scheduling priority to a live thread, mapping it
/// onto the host thread if one is running. Returns false when the thread
/// handle is unknown.
/// </summary>
bool TrySetGuestThreadPriority(ulong guestThreadHandle, int guestPriority);
/// <summary>
/// Records a new affinity mask for a guest thread and re-applies it to
/// the host thread where the platform supports it.
/// </summary>
bool TrySetGuestThreadAffinity(ulong guestThreadHandle, ulong affinityMask);
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
bool TryCallGuestFunction(
@@ -65,11 +86,36 @@ public interface IGuestThreadScheduler
string reason,
out string? error);
bool TryCallGuestFunction(
CpuContext callerContext,
ulong entryPoint,
ulong arg0,
ulong arg1,
ulong arg2,
ulong stackAddress,
ulong stackSize,
string reason,
out ulong returnValue,
out string? error);
bool TryCallGuestContinuation(
CpuContext callerContext,
GuestCpuContinuation continuation,
string reason,
out string? error);
/// <summary>
/// Asynchronously invokes an installed kernel exception handler as the
/// target guest thread. This is used by IL2CPP's stop-the-world collector:
/// the handler acknowledges suspension and may remain blocked until the
/// collecting thread resumes it.
/// </summary>
bool TryRaiseGuestException(
CpuContext callerContext,
ulong threadHandle,
ulong handler,
int exceptionType,
out string? error);
}
public readonly record struct GuestImportCallFrame(
@@ -94,13 +140,34 @@ public readonly record struct GuestCpuContinuation(
ulong Rdi,
ulong R8,
ulong R9,
ulong R10,
ulong R11,
ulong R12,
ulong R13,
ulong R14,
ulong R15);
ulong R15,
ushort FpuControlWord,
uint Mxcsr,
bool RestoreFullFpuState);
public static class GuestThreadExecution
{
private sealed class DelegateGuestThreadBlockWaiter : IGuestThreadBlockWaiter
{
private readonly Func<int> _resume;
private readonly Func<bool> _tryWake;
public DelegateGuestThreadBlockWaiter(Func<int> resume, Func<bool> tryWake)
{
_resume = resume;
_tryWake = tryWake;
}
public int Resume() => _resume();
public bool TryWake() => _tryWake();
}
[ThreadStatic]
private static ulong _currentGuestThreadHandle;
@@ -246,6 +313,23 @@ public static class GuestThreadExecution
return true;
}
// Compatibility bridge for exports that still describe blocked work as a
// resume/wake delegate pair. New hot paths should provide an
// IGuestThreadBlockWaiter directly to avoid allocating closures.
public static bool RequestCurrentThreadBlock(
CpuContext? context,
string reason,
string? wakeKey,
Func<int> resumeHandler,
Func<bool> wakeHandler,
long blockDeadlineTimestamp = 0) =>
RequestCurrentThreadBlock(
context,
reason,
wakeKey,
new DelegateGuestThreadBlockWaiter(resumeHandler, wakeHandler),
blockDeadlineTimestamp);
public static bool TryConsumeCurrentThreadBlock(out string reason)
{
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
@@ -314,6 +398,27 @@ public static class GuestThreadExecution
return true;
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler,
out long blockDeadlineTimestamp)
{
var consumed = TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out var waiter,
out blockDeadlineTimestamp);
resumeHandler = waiter is null ? null : waiter.Resume;
wakeHandler = waiter is null ? null : waiter.TryWake;
return consumed;
}
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
{
if (timeout <= TimeSpan.Zero)
@@ -360,10 +465,15 @@ public static class GuestThreadExecution
context[CpuRegister.Rdi],
context[CpuRegister.R8],
context[CpuRegister.R9],
context[CpuRegister.R10],
context[CpuRegister.R11],
context[CpuRegister.R12],
context[CpuRegister.R13],
context[CpuRegister.R14],
context[CpuRegister.R15]);
context[CpuRegister.R15],
context.FpuControlWord,
context.Mxcsr,
RestoreFullFpuState: false);
return true;
}
+442
View File
@@ -0,0 +1,442 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Diagnostics;
namespace SharpEmu.HLE;
/// <summary>
/// Process-wide registry of ELF PT_TLS templates and per-thread dynamic thread
/// vectors. AMD64 uses TLS Variant II: startup/static module blocks precede the
/// thread pointer, while modules introduced after a thread was initialized are
/// represented by separately allocated DTV entries.
/// </summary>
public static class GuestTlsTemplate
{
// Must match CpuDispatcher/DirectExecutionBackend's mapped prefix. PS5
// modules can require more than one host page of Variant II static TLS;
// Dreaming Sarah's startup image, for example, reaches 0x1870 bytes.
public const ulong StartupStaticTlsReservation = 0x10000UL;
private static readonly object _gate = new();
private static readonly SortedDictionary<ulong, ModuleTemplate> _modules = new();
private static readonly Dictionary<ulong, ThreadDtv> _threadDtvs = new();
private static ulong _staticTlsSize;
private static ulong _maximumAlignment = 1;
private static ulong _generation;
static GuestTlsTemplate()
{
RunTlsLayoutSelfChecks();
}
private sealed class ModuleTemplate
{
public required ulong ModuleId { get; init; }
public required byte[] InitImage { get; init; }
public required ulong MemorySize { get; init; }
public required ulong Alignment { get; init; }
public required ulong AlignmentBias { get; init; }
public required ulong StaticOffset { get; init; }
}
private sealed class ThreadDtv
{
public ulong Generation { get; set; }
public Dictionary<ulong, DtvEntry> Entries { get; } = new();
public nint DtvAllocationBase { get; set; }
public ulong DtvAddress { get; set; }
}
private sealed class DtvEntry
{
public required ulong Address { get; init; }
public nint AllocationBase { get; init; }
}
/// <summary>Main executable's initialized PT_TLS bytes.</summary>
public static byte[] InitImage
{
get
{
lock (_gate)
{
return _modules.TryGetValue(1, out var module)
? (byte[])module.InitImage.Clone()
: [];
}
}
}
/// <summary>Aligned size of the main executable's static TLS block.</summary>
public static ulong BlockSize
{
get
{
lock (_gate)
{
return _modules.TryGetValue(1, out var module)
? module.StaticOffset
: 0;
}
}
}
/// <summary>Main executable's PT_TLS alignment.</summary>
public static ulong Alignment
{
get
{
lock (_gate)
{
return _modules.TryGetValue(1, out var module)
? module.Alignment
: 1;
}
}
}
/// <summary>Total Variant II static TLS span required below the TCB.</summary>
public static ulong StaticTlsSize
{
get { lock (_gate) { return _staticTlsSize; } }
}
/// <summary>Largest PT_TLS alignment required by the registered modules.</summary>
public static ulong MaximumAlignment
{
get { lock (_gate) { return _maximumAlignment; } }
}
/// <summary>Current DTV generation, incremented for every registered module.</summary>
public static ulong Generation
{
get { lock (_gate) { return _generation; } }
}
/// <summary>
/// Backwards-compatible main-module registration entry point.
/// </summary>
public static void Set(ReadOnlySpan<byte> initImage, ulong memorySize, ulong alignment)
{
Reset();
RegisterModule(1, initImage, memorySize, alignment, alignmentBias: 0);
}
/// <summary>
/// Registers one module's PT_TLS template and allocates its Variant II
/// static offset. Modules must be registered in loader module-ID order.
/// </summary>
public static ulong RegisterModule(
ulong moduleId,
ReadOnlySpan<byte> initImage,
ulong memorySize,
ulong alignment,
ulong alignmentBias = 0)
{
if (moduleId == 0)
{
throw new ArgumentOutOfRangeException(nameof(moduleId));
}
var normalizedAlignment = alignment == 0 ? 1 : alignment;
if ((normalizedAlignment & (normalizedAlignment - 1)) != 0)
{
throw new InvalidDataException($"PT_TLS alignment 0x{alignment:X} is not a power of two.");
}
if (memorySize > int.MaxValue || (ulong)initImage.Length > memorySize)
{
throw new InvalidDataException("PT_TLS template size is invalid or exceeds the supported process limit.");
}
lock (_gate)
{
if (_modules.TryGetValue(moduleId, out var existing))
{
if (existing.MemorySize != memorySize ||
existing.Alignment != normalizedAlignment ||
existing.AlignmentBias != alignmentBias ||
!existing.InitImage.AsSpan().SequenceEqual(initImage))
{
throw new InvalidOperationException($"TLS module {moduleId} was registered with a different template.");
}
return existing.StaticOffset;
}
// FreeBSD/AMD64 Variant II: choose the smallest offset satisfying
// offset - previous >= size and (-offset) % align == p_vaddr % align.
var staticOffset = CalculateStaticOffset(
_staticTlsSize,
memorySize,
normalizedAlignment,
alignmentBias);
if (staticOffset > StartupStaticTlsReservation)
{
throw new InvalidOperationException(
$"Static TLS requires 0x{staticOffset:X} bytes, but startup maps only " +
$"0x{StartupStaticTlsReservation:X} bytes below the thread pointer.");
}
_modules.Add(moduleId, new ModuleTemplate
{
ModuleId = moduleId,
InitImage = initImage.ToArray(),
MemorySize = memorySize,
Alignment = normalizedAlignment,
AlignmentBias = alignmentBias,
StaticOffset = staticOffset,
});
_staticTlsSize = staticOffset;
_maximumAlignment = Math.Max(_maximumAlignment, normalizedAlignment);
_generation++;
return staticOffset;
}
}
/// <summary>Returns the static TP-relative block offset for a module.</summary>
public static bool TryGetStaticOffset(ulong moduleId, out ulong staticOffset)
{
lock (_gate)
{
if (_modules.TryGetValue(moduleId, out var module))
{
staticOffset = module.StaticOffset;
return true;
}
}
staticOffset = 0;
return false;
}
/// <summary>Clears all module templates and frees dynamic DTV blocks.</summary>
public static void Reset()
{
lock (_gate)
{
foreach (var dtv in _threadDtvs.Values)
{
FreeDynamicEntries(dtv);
}
_threadDtvs.Clear();
_modules.Clear();
_staticTlsSize = 0;
_maximumAlignment = 1;
_generation++;
}
}
/// <summary>
/// Initializes every currently registered startup TLS block for a thread
/// and records their addresses in that thread's DTV.
/// </summary>
public static void SeedThreadBlock(CpuContext context, ulong threadPointer)
{
ArgumentNullException.ThrowIfNull(context);
if (threadPointer == 0)
{
return;
}
lock (_gate)
{
if (_threadDtvs.Remove(threadPointer, out var oldDtv))
{
FreeDynamicEntries(oldDtv);
}
var dtv = new ThreadDtv();
_threadDtvs.Add(threadPointer, dtv);
foreach (var module in _modules.Values)
{
dtv.Entries[module.ModuleId] = CreateStaticOrDynamicEntry(context, threadPointer, module);
}
RebuildGuestDtv(context, threadPointer, dtv);
}
}
/// <summary>
/// Implements the DTV lookup used by <c>__tls_get_addr</c>. Unknown module
/// IDs and offsets outside the module's TLS image are rejected with zero.
/// </summary>
public static ulong ResolveAddress(CpuContext context, ulong moduleId, ulong offset)
{
ArgumentNullException.ThrowIfNull(context);
if (context.FsBase == 0 || moduleId == 0)
{
return 0;
}
lock (_gate)
{
if (!_modules.TryGetValue(moduleId, out var module) || offset >= module.MemorySize)
{
return 0;
}
if (!_threadDtvs.TryGetValue(context.FsBase, out var dtv))
{
dtv = new ThreadDtv();
_threadDtvs.Add(context.FsBase, dtv);
foreach (var startupModule in _modules.Values)
{
dtv.Entries[startupModule.ModuleId] = CreateStaticOrDynamicEntry(context, context.FsBase, startupModule);
}
RebuildGuestDtv(context, context.FsBase, dtv);
}
var entryAdded = false;
if (!dtv.Entries.TryGetValue(moduleId, out var entry))
{
// This module appeared after the thread's startup TLS layout was
// seeded. Model rtld's lazy DTV allocation instead of assuming
// unused space exists below the already-live thread pointer.
entry = CreateDynamicEntry(module);
dtv.Entries.Add(moduleId, entry);
entryAdded = true;
}
if (entryAdded || dtv.Generation != _generation)
{
RebuildGuestDtv(context, context.FsBase, dtv);
}
return checked(entry.Address + offset);
}
}
private static DtvEntry CreateStaticOrDynamicEntry(
CpuContext context,
ulong threadPointer,
ModuleTemplate module)
{
if (threadPointer >= module.StaticOffset)
{
var address = threadPointer - module.StaticOffset;
var zeroImage = module.MemorySize == 0 ? [] : new byte[(int)module.MemorySize];
if ((zeroImage.Length == 0 || context.Memory.TryWrite(address, zeroImage)) &&
(module.InitImage.Length == 0 || context.Memory.TryWrite(address, module.InitImage)))
{
return new DtvEntry { Address = address };
}
}
return CreateDynamicEntry(module);
}
private static DtvEntry CreateDynamicEntry(ModuleTemplate module)
{
var size = Math.Max(1UL, module.MemorySize);
var allocationSize = checked(size + module.Alignment - 1);
if (allocationSize > int.MaxValue)
{
throw new OutOfMemoryException("TLS module allocation exceeds the supported host allocation size.");
}
var allocationBase = Marshal.AllocHGlobal((int)allocationSize);
var alignedAddress = AlignUp(unchecked((ulong)allocationBase), module.Alignment);
Marshal.Copy(new byte[(int)size], 0, unchecked((nint)alignedAddress), (int)size);
if (module.InitImage.Length != 0)
{
Marshal.Copy(module.InitImage, 0, unchecked((nint)alignedAddress), module.InitImage.Length);
}
return new DtvEntry
{
Address = alignedAddress,
AllocationBase = allocationBase,
};
}
private static void FreeDynamicEntries(ThreadDtv dtv)
{
foreach (var entry in dtv.Entries.Values)
{
if (entry.AllocationBase != 0)
{
Marshal.FreeHGlobal(entry.AllocationBase);
}
}
if (dtv.DtvAllocationBase != 0)
{
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
dtv.DtvAllocationBase = 0;
dtv.DtvAddress = 0;
}
}
private static void RebuildGuestDtv(CpuContext context, ulong threadPointer, ThreadDtv dtv)
{
var maximumModuleId = _modules.Count == 0 ? 0UL : _modules.Keys.Max();
var byteSize = checked(2UL * sizeof(ulong) + maximumModuleId * sizeof(ulong));
if (byteSize > int.MaxValue)
{
throw new OutOfMemoryException("Guest DTV exceeds the supported host allocation size.");
}
var newAllocation = Marshal.AllocHGlobal((int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
var newAddress = unchecked((ulong)newAllocation);
Marshal.Copy(new byte[(int)Math.Max((ulong)sizeof(ulong) * 2, byteSize)], 0, newAllocation, (int)Math.Max((ulong)sizeof(ulong) * 2, byteSize));
Marshal.WriteInt64(newAllocation, 0, unchecked((long)_generation));
Marshal.WriteInt64(newAllocation, sizeof(ulong), unchecked((long)maximumModuleId));
foreach (var (moduleId, entry) in dtv.Entries)
{
var slotOffset = checked((int)(2UL * sizeof(ulong) + (moduleId - 1) * sizeof(ulong)));
Marshal.WriteInt64(newAllocation, slotOffset, unchecked((long)entry.Address));
}
if (!context.TryWriteUInt64(threadPointer + sizeof(ulong), newAddress))
{
Marshal.FreeHGlobal(newAllocation);
throw new InvalidOperationException("Failed to install the guest DTV pointer in the thread control block.");
}
if (dtv.DtvAllocationBase != 0)
{
Marshal.FreeHGlobal(dtv.DtvAllocationBase);
}
dtv.DtvAllocationBase = newAllocation;
dtv.DtvAddress = newAddress;
dtv.Generation = _generation;
}
private static ulong AlignUp(ulong value, ulong alignment)
{
var mask = alignment - 1;
return checked((value + mask) & ~mask);
}
private static ulong CalculateStaticOffset(
ulong previousOffset,
ulong size,
ulong alignment,
ulong alignmentBias)
{
var result = checked(previousOffset + size + alignment - 1);
return result - ((result + alignmentBias) & (alignment - 1));
}
[Conditional("DEBUG")]
private static void RunTlsLayoutSelfChecks()
{
var firstOffset = RegisterModule(1, [0x11, 0x22], 0x20, 0x10, 0);
var secondOffset = RegisterModule(2, [0x7A], 0x18, 0x20, 8);
Debug.Assert(firstOffset == 0x20, "First Variant II TLS offset is incorrect.");
Debug.Assert(secondOffset >= firstOffset + 0x18, "TLS modules overlap in the static layout.");
Debug.Assert((unchecked(0UL - secondOffset) & 0x1F) == 8, "PT_TLS virtual-address congruence was not preserved.");
var lateEntry = CreateDynamicEntry(_modules[2]);
try
{
Debug.Assert(lateEntry.AllocationBase != 0, "A late TLS module did not receive a dynamic DTV block.");
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address)) == 0x7A, "Late-module tdata was not initialized.");
Debug.Assert(Marshal.ReadByte(unchecked((nint)lateEntry.Address + 1)) == 0, "Late-module tbss was not zero initialized.");
}
finally
{
Marshal.FreeHGlobal(lateEntry.AllocationBase);
Reset();
}
}
}
+3 -1
View File
@@ -13,7 +13,9 @@ public static class HleDataSymbols
private const string LibcNeedFlagNid = "P330P3dFF68";
private const string LibcInternalNeedFlagNid = "ZT4ODD2Ts9o";
private const int ProgNameMaxBytes = 511;
private const ulong StackChkGuardValue = 0xC0DEC0DECAFEBABEUL;
// Terminator canaries reserve the low byte as NUL. Keep the process data
// symbol and every per-thread TLS copy byte-for-byte identical.
private const ulong StackChkGuardValue = 0xC0DEC0DECAFEBA00UL;
private static readonly object _gate = new();
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
+84 -4
View File
@@ -163,13 +163,18 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
private const int PROT_EXEC = 0x4;
private const int MAP_PRIVATE = 0x02;
private const int MAP_FIXED = 0x10;
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
// Linux-only: fail instead of clobbering an existing mapping.
private const int MAP_FIXED_NOREPLACE = 0x100000;
private const int KERN_SUCCESS = 0;
// On Darwin fixed placement is represented by the absence of
// VM_FLAGS_ANYWHERE. Do not add VM_FLAGS_OVERWRITE: overlap must fail.
private const int VM_FLAGS_FIXED = 0;
private static readonly nint MAP_FAILED = -1;
private static readonly object Gate = new();
@@ -181,6 +186,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
public ulong Size;
public uint DefaultProtect;
public Dictionary<ulong, uint>? PageProtects;
public bool UsesMachAllocation;
public ulong End => Base + Size;
@@ -251,6 +257,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
}
nint result;
var usesMachAllocation = false;
if (address != null)
{
// Win32 maps at exactly the requested address or fails
@@ -271,6 +278,28 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
var exactFlags = OperatingSystem.IsMacOS() ? flags : flags | MAP_FIXED_NOREPLACE;
result = mmap((nint)address, (nuint)alignedSize, posixProtect, exactFlags, -1, 0);
if (result != MAP_FAILED && (ulong)result != (ulong)address)
{
munmap(result, (nuint)alignedSize);
result = MAP_FAILED;
}
if (result == MAP_FAILED && OperatingSystem.IsMacOS())
{
// The hint-only attempt above didn't land at the requested
// address. mach_vm_allocate with fixed placement and no
// overwrite flag atomically maps the requested range or
// fails if any host mapping already owns it. Unlike
// MAP_FIXED, it cannot clobber CLR, dyld, JIT, or Rosetta
// memory that is absent from our shadow table.
Trace($"exact mmap hint failed, retrying with fixed Mach allocation: addr=0x{(ulong)address:X16}");
result = AllocateDarwinFixed(
(nint)address,
alignedSize,
posixProtect);
usesMachAllocation = result != MAP_FAILED;
}
if (result == MAP_FAILED || (ulong)result != (ulong)address)
{
Trace($"exact mmap failed: addr=0x{(ulong)address:X16} got=0x{(ulong)result:X16} size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
@@ -296,7 +325,8 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
{
Base = (ulong)result,
Size = alignedSize,
DefaultProtect = protect
DefaultProtect = protect,
UsesMachAllocation = usesMachAllocation,
};
return (void*)result;
@@ -315,8 +345,15 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
return false;
}
Regions.Remove((ulong)address);
return munmap((nint)address, (nuint)region.Size) == 0;
var released = region.UsesMachAllocation
? mach_vm_deallocate(mach_task_self(), (ulong)address, region.Size) == KERN_SUCCESS
: munmap((nint)address, (nuint)region.Size) == 0;
if (released)
{
Regions.Remove((ulong)address);
}
return released;
}
}
@@ -484,6 +521,40 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
};
}
private static nint AllocateDarwinFixed(nint requestedAddress, ulong size, int posixProtect)
{
var address = (ulong)requestedAddress;
var result = mach_vm_allocate(
mach_task_self(),
ref address,
size,
VM_FLAGS_FIXED);
if (result != KERN_SUCCESS || address != (ulong)requestedAddress)
{
if (result == KERN_SUCCESS)
{
_ = mach_vm_deallocate(mach_task_self(), address, size);
}
Trace(
$"fixed Mach allocation failed: addr=0x{(ulong)requestedAddress:X16} " +
$"size=0x{size:X} kern_return={result}");
return MAP_FAILED;
}
if (mprotect((nint)address, (nuint)size, posixProtect) == 0)
{
return (nint)address;
}
var error = Marshal.GetLastPInvokeError();
_ = mach_vm_deallocate(mach_task_self(), address, size);
Trace(
$"fixed Mach allocation protection failed: addr=0x{address:X16} " +
$"size=0x{size:X} errno={error}");
return MAP_FAILED;
}
private static void Trace(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
@@ -504,5 +575,14 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
[DllImport("libc", SetLastError = true)]
private static extern int mprotect(nint addr, nuint length, int prot);
[DllImport("libSystem.B.dylib")]
private static extern uint mach_task_self();
[DllImport("libSystem.B.dylib")]
private static extern int mach_vm_allocate(uint target, ref ulong address, ulong size, int flags);
[DllImport("libSystem.B.dylib")]
private static extern int mach_vm_deallocate(uint target, ulong address, ulong size);
}
}
+5 -6
View File
@@ -6,12 +6,11 @@ using System.Collections.Concurrent;
namespace SharpEmu.HLE;
/// <summary>
/// Runs work on the real process main thread. GLFW windowing must live on
/// that thread on macOS (AppKit) and Linux (X11's single event queue), so the
/// CLI moves emulation onto a worker thread, parks the main thread in
/// <see cref="Pump"/>, and the video presenter posts its window loop here. On
/// Windows <see cref="IsAvailable"/> stays false and the window keeps its own
/// thread.
/// Runs work on the real process main thread. macOS only allows AppKit (and
/// therefore GLFW windowing) on that thread, so the CLI moves emulation onto
/// a worker thread, parks the main thread in <see cref="Pump"/>, and the
/// video presenter posts its window loop here. On other platforms
/// <see cref="IsAvailable"/> stays false and nothing changes.
/// </summary>
public static class HostMainThread
{
+498
View File
@@ -0,0 +1,498 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE;
/// <summary>
/// Cross-platform host virtual memory API with Win32 semantics.
/// On Windows this forwards directly to kernel32. On POSIX systems it is
/// implemented over mmap/mprotect/munmap with a shadow region table that
/// answers VirtualQuery-style questions and tracks page protections.
/// POSIX anonymous mappings are demand-paged by the kernel, so Win32
/// "reserve-only" regions are mapped as committed memory directly and
/// commit requests become protection changes.
/// </summary>
public static unsafe class HostMemory
{
public const uint MEM_COMMIT = 0x1000;
public const uint MEM_RESERVE = 0x2000;
public const uint MEM_RELEASE = 0x8000;
public const uint MEM_FREE_STATE = 0x10000;
public const uint MEM_PRIVATE = 0x20000;
public const uint PAGE_NOACCESS = 0x01;
public const uint PAGE_READONLY = 0x02;
public const uint PAGE_READWRITE = 0x04;
public const uint PAGE_EXECUTE = 0x10;
public const uint PAGE_EXECUTE_READ = 0x20;
public const uint PAGE_EXECUTE_READWRITE = 0x40;
private const ulong PageSize = 0x1000;
/// <summary>Win32 MEMORY_BASIC_INFORMATION (64-bit) layout.</summary>
public struct BasicInfo
{
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;
}
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
{
if (OperatingSystem.IsWindows())
{
return Win32VirtualAlloc(address, size, allocationType, protect);
}
return Posix.Alloc(address, size, allocationType, protect);
}
public static bool Free(void* address, nuint size, uint freeType)
{
if (OperatingSystem.IsWindows())
{
return Win32VirtualFree(address, size, freeType);
}
return Posix.Free(address, size, freeType);
}
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
{
if (OperatingSystem.IsWindows())
{
return Win32VirtualProtect(address, size, newProtect, out oldProtect);
}
return Posix.Protect(address, size, newProtect, out oldProtect);
}
public static nuint Query(void* address, out BasicInfo info)
{
if (OperatingSystem.IsWindows())
{
return Win32VirtualQuery(address, out info, (nuint)sizeof(BasicInfo));
}
return Posix.Query(address, out info);
}
public static void FlushInstructionCache(void* address, nuint size)
{
if (OperatingSystem.IsWindows())
{
Win32FlushInstructionCache(Win32GetCurrentProcess(), address, size);
return;
}
// The emulator only executes x86-64 guest code, so a non-Windows host
// is either x86-64 (including Rosetta 2 translation) with a coherent
// instruction cache, or would need sys_icache_invalidate for a future
// arm64 recompiler. Nothing to do today.
}
[DllImport("kernel32.dll", EntryPoint = "VirtualAlloc", SetLastError = true)]
private static extern void* Win32VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll", EntryPoint = "VirtualFree", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Win32VirtualFree(void* lpAddress, nuint dwSize, uint dwFreeType);
[DllImport("kernel32.dll", EntryPoint = "VirtualProtect", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Win32VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll", EntryPoint = "VirtualQuery")]
private static extern nuint Win32VirtualQuery(void* lpAddress, out BasicInfo lpBuffer, nuint dwLength);
[DllImport("kernel32.dll", EntryPoint = "GetCurrentProcess")]
private static extern void* Win32GetCurrentProcess();
[DllImport("kernel32.dll", EntryPoint = "FlushInstructionCache")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool Win32FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
private static class Posix
{
private const int PROT_NONE = 0x0;
private const int PROT_READ = 0x1;
private const int PROT_WRITE = 0x2;
private const int PROT_EXEC = 0x4;
private const int MAP_PRIVATE = 0x02;
private const int MAP_FIXED = 0x10;
private static readonly int MAP_ANON = OperatingSystem.IsMacOS() ? 0x1000 : 0x20;
private static readonly int MAP_NORESERVE = OperatingSystem.IsMacOS() ? 0 : 0x4000;
// Linux-only: fail instead of clobbering an existing mapping.
private const int MAP_FIXED_NOREPLACE = 0x100000;
private static readonly nint MAP_FAILED = -1;
private static readonly object Gate = new();
private static readonly SortedList<ulong, Region> Regions = new();
private sealed class Region
{
public ulong Base;
public ulong Size;
public uint DefaultProtect;
public Dictionary<ulong, uint>? PageProtects;
public ulong End => Base + Size;
public uint ProtectAt(ulong pageAddress)
{
if (PageProtects is not null && PageProtects.TryGetValue(pageAddress, out var overriden))
{
return overriden;
}
return DefaultProtect;
}
}
public static void* Alloc(void* address, nuint size, uint allocationType, uint protect)
{
if (size == 0)
{
return null;
}
var alignedSize = AlignUp((ulong)size, PageSize);
lock (Gate)
{
if (allocationType == MEM_COMMIT && address != null &&
TryFindRegionLocked((ulong)address, out var existing))
{
// Note: MEM_RESERVE requests that overlap an existing
// region must fail like Win32 does; only a pure commit
// may target pages inside a tracked mapping.
// Commit inside an existing mapping: the pages are already
// backed (demand paged), so only apply the protection.
var start = AlignDown((ulong)address, PageSize);
var end = Math.Min(existing.End, AlignUp((ulong)address + alignedSize, PageSize));
if (end <= start)
{
return null;
}
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(protect)) != 0)
{
return null;
}
SetProtectRangeLocked(existing, start, end - start, protect);
return address;
}
if ((allocationType & MEM_RESERVE) == 0)
{
// MEM_COMMIT alone outside any known region is invalid here.
return null;
}
var posixProtect = ToPosixProtect(protect);
var flags = MAP_PRIVATE | MAP_ANON;
if ((allocationType & MEM_COMMIT) == 0)
{
// Reserve-only: keep the requested protection so the region
// is usable without a separate commit step, but tell the
// kernel not to account swap for it where supported.
flags |= MAP_NORESERVE;
}
nint result;
if (address != null)
{
// Win32 maps at exactly the requested address or fails
// without touching existing mappings. Fail up front on
// any overlap we track, then place the mapping: Linux
// gets MAP_FIXED_NOREPLACE (fails cleanly on host
// mappings too). Darwin lacks NOREPLACE and plain
// MAP_FIXED would silently clobber untracked host
// memory (dyld, the runtime's JIT heap, Rosetta), so
// pass the address as a hint instead -- the kernel
// honors it when the range is free and relocates the
// mapping otherwise, which we treat as failure.
if (OverlapsTrackedRegionLocked((ulong)address, alignedSize))
{
Trace($"exact overlap: addr=0x{(ulong)address:X16} size=0x{alignedSize:X}");
return null;
}
var exactFlags = OperatingSystem.IsMacOS() ? flags : flags | MAP_FIXED_NOREPLACE;
result = mmap((nint)address, (nuint)alignedSize, posixProtect, exactFlags, -1, 0);
if (result == MAP_FAILED || (ulong)result != (ulong)address)
{
Trace($"exact mmap failed: addr=0x{(ulong)address:X16} got=0x{(ulong)result:X16} size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
if (result != MAP_FAILED)
{
munmap(result, (nuint)alignedSize);
}
return null;
}
}
else
{
result = mmap(0, (nuint)alignedSize, posixProtect, flags, -1, 0);
if (result == MAP_FAILED)
{
Trace($"mmap failed: size=0x{alignedSize:X} errno={Marshal.GetLastPInvokeError()}");
return null;
}
}
Regions[(ulong)result] = new Region
{
Base = (ulong)result,
Size = alignedSize,
DefaultProtect = protect
};
return (void*)result;
}
}
public static bool Free(void* address, nuint size, uint freeType)
{
_ = size;
_ = freeType;
lock (Gate)
{
if (!Regions.TryGetValue((ulong)address, out var region))
{
return false;
}
Regions.Remove((ulong)address);
return munmap((nint)address, (nuint)region.Size) == 0;
}
}
public static bool Protect(void* address, nuint size, uint newProtect, out uint oldProtect)
{
oldProtect = PAGE_NOACCESS;
if (size == 0)
{
return false;
}
var start = AlignDown((ulong)address, PageSize);
var end = AlignUp((ulong)address + size, PageSize);
lock (Gate)
{
if (!TryFindRegionLocked(start, out var region) || end > region.End)
{
return false;
}
oldProtect = region.ProtectAt(start);
if (mprotect((nint)start, (nuint)(end - start), ToPosixProtect(newProtect)) != 0)
{
return false;
}
SetProtectRangeLocked(region, start, end - start, newProtect);
return true;
}
}
public static nuint Query(void* address, out BasicInfo info)
{
info = default;
var pageAddress = AlignDown((ulong)address, PageSize);
lock (Gate)
{
if (TryFindRegionLocked(pageAddress, out var region))
{
// Win32 VirtualQuery reports a run of pages sharing the
// same protection, so stop the run where it changes. Do
// not walk the whole mapping page-by-page here: PS5 GPU
// apertures can span hundreds of GiB, while protection
// overrides are sparse. A tiny read inside such a mapping
// otherwise performs tens of millions of dictionary
// lookups before it can return.
var pageProtects = region.PageProtects;
uint protect;
ulong runEnd;
if (pageProtects is null || pageProtects.Count == 0)
{
protect = region.DefaultProtect;
runEnd = region.End;
}
else if (pageProtects.TryGetValue(pageAddress, out protect))
{
runEnd = pageAddress + PageSize;
while (runEnd < region.End &&
pageProtects.TryGetValue(runEnd, out var nextProtect) &&
nextProtect == protect)
{
runEnd += PageSize;
}
}
else
{
protect = region.DefaultProtect;
runEnd = region.End;
foreach (var overrideAddress in pageProtects.Keys)
{
if (overrideAddress > pageAddress && overrideAddress < runEnd)
{
runEnd = overrideAddress;
}
}
}
info.BaseAddress = pageAddress;
info.AllocationBase = region.Base;
info.AllocationProtect = region.DefaultProtect;
info.RegionSize = runEnd - pageAddress;
info.State = MEM_COMMIT;
info.Protect = protect;
info.Type = MEM_PRIVATE;
return (nuint)sizeof(BasicInfo);
}
// Untracked host memory (runtime heaps, stacks, libraries) is
// reported as a free block reaching to the next tracked region
// so scanning callers keep advancing.
var nextBase = ulong.MaxValue;
foreach (var regionBase in Regions.Keys)
{
if (regionBase > pageAddress)
{
nextBase = regionBase;
break;
}
}
info.BaseAddress = pageAddress;
info.AllocationBase = 0;
info.AllocationProtect = PAGE_NOACCESS;
info.RegionSize = (nextBase == ulong.MaxValue ? pageAddress + PageSize : nextBase) - pageAddress;
info.State = MEM_FREE_STATE;
info.Protect = PAGE_NOACCESS;
info.Type = 0;
return (nuint)sizeof(BasicInfo);
}
}
private static bool OverlapsTrackedRegionLocked(ulong start, ulong size)
{
var end = start + size;
foreach (var region in Regions.Values)
{
if (region.Base < end && start < region.End)
{
return true;
}
}
return false;
}
private static bool TryFindRegionLocked(ulong address, out Region region)
{
region = null!;
var keys = Regions.Keys;
var low = 0;
var high = keys.Count - 1;
Region? candidate = null;
while (low <= high)
{
var middle = low + ((high - low) >> 1);
var entry = Regions.Values[middle];
if (entry.Base <= address)
{
candidate = entry;
low = middle + 1;
}
else
{
high = middle - 1;
}
}
if (candidate is null || address >= candidate.End)
{
return false;
}
region = candidate;
return true;
}
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
{
if (start == region.Base && size >= region.Size)
{
region.DefaultProtect = protect;
region.PageProtects = null;
return;
}
region.PageProtects ??= new Dictionary<ulong, uint>();
var end = start + size;
for (var pageAddress = start; pageAddress < end; pageAddress += PageSize)
{
if (protect == region.DefaultProtect)
{
region.PageProtects.Remove(pageAddress);
}
else
{
region.PageProtects[pageAddress] = protect;
}
}
}
private static int ToPosixProtect(uint win32Protect)
{
return win32Protect switch
{
PAGE_NOACCESS => PROT_NONE,
PAGE_READONLY => PROT_READ,
PAGE_READWRITE => PROT_READ | PROT_WRITE,
PAGE_EXECUTE => PROT_READ | PROT_EXEC,
PAGE_EXECUTE_READ => PROT_READ | PROT_EXEC,
PAGE_EXECUTE_READWRITE => PROT_READ | PROT_WRITE | PROT_EXEC,
_ => PROT_READ | PROT_WRITE
};
}
private static void Trace(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[HOSTMEM] {message}");
}
}
private static ulong AlignDown(ulong value, ulong alignment) => value & ~(alignment - 1);
private static ulong AlignUp(ulong value, ulong alignment) => checked((value + alignment - 1) & ~(alignment - 1));
[DllImport("libc", SetLastError = true)]
private static extern nint mmap(nint addr, nuint length, int prot, int flags, int fd, long offset);
[DllImport("libc", SetLastError = true)]
private static extern int munmap(nint addr, nuint length);
[DllImport("libc", SetLastError = true)]
private static extern int mprotect(nint addr, nuint length, int prot);
}
}
+2 -3
View File
@@ -1,13 +1,12 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
namespace SharpEmu.HLE;
public interface IModuleManager
{
int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null);
/// <summary>Registers pre-built exports (the compile-time generated registry).</summary>
int RegisterExports(IReadOnlyList<ExportedFunction> exports);
void Freeze();
+16 -145
View File
@@ -13,12 +13,12 @@ public sealed class ModuleManager : IModuleManager
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
private readonly object _registrationGate = new();
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
private readonly HashSet<Assembly> _warmupAssemblies = new();
private bool _isFrozen;
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
public int RegisterExports(IReadOnlyList<ExportedFunction> exports)
{
ArgumentNullException.ThrowIfNull(assembly);
ArgumentNullException.ThrowIfNull(exports);
lock (_registrationGate)
{
@@ -27,48 +27,21 @@ public sealed class ModuleManager : IModuleManager
throw new InvalidOperationException("Module registration is frozen.");
}
// Deduplicated: one assembly is reached through many types.
if (!_scannedAssemblies.Add((assembly, generation)))
{
return 0;
}
var registeredCount = 0;
var instances = new Dictionary<Type, object>();
foreach (var type in assembly.GetTypes())
foreach (var export in exports)
{
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
if (!_dispatchTable.TryAdd(export.Nid, export.Function))
{
var exportAttribute = method.GetCustomAttribute<SysAbiExportAttribute>(inherit: false);
if (exportAttribute is null)
{
continue;
}
var exportInfo = ResolveExportInfo(exportAttribute, method, generation, symbolCatalog);
if (exportInfo is null)
{
continue;
}
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue;
}
_exportTable[exportInfo.Value.Nid] = new ExportedFunction(
exportInfo.Value.LibraryName,
exportInfo.Value.Nid,
exportInfo.Value.ExportName,
exportInfo.Value.Target,
(SysAbiFunction)handler);
_exportNameTable.TryAdd(exportInfo.Value.ExportName, _exportTable[exportInfo.Value.Nid]);
registeredCount++;
Console.Error.WriteLine($"[HLE] Duplicate NID '{export.Nid}' ({export.Name}) — already registered, skipping.");
continue;
}
_exportTable[export.Nid] = export;
_exportNameTable.TryAdd(export.Name, export);
// The warm sweep in Freeze() covers every assembly that contributed a
// handler (generated thunks resolve to their home assembly too).
_warmupAssemblies.Add(export.Function.Method.Module.Assembly);
registeredCount++;
}
return registeredCount;
@@ -92,7 +65,8 @@ public sealed class ModuleManager : IModuleManager
Assembly[] assemblies;
lock (_registrationGate)
{
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
assemblies = new Assembly[_warmupAssemblies.Count];
_warmupAssemblies.CopyTo(assemblies);
}
assemblies = WithGuestReachableDependencies(assemblies);
@@ -328,107 +302,4 @@ public sealed class ModuleManager : IModuleManager
return true;
}
private static Delegate CreateHandler(Type ownerType, MethodInfo method, IDictionary<Type, object> instances)
{
ValidateSignature(method);
object? target = null;
if (!method.IsStatic)
{
if (!instances.TryGetValue(ownerType, out target))
{
target = Activator.CreateInstance(ownerType)
?? throw new InvalidOperationException($"Cannot instantiate module type: {ownerType.FullName}");
instances.Add(ownerType, target);
}
}
var parameterCount = method.GetParameters().Length;
if (parameterCount == 0)
{
var noArg = method.IsStatic
? (Func<int>)method.CreateDelegate(typeof(Func<int>))
: (Func<int>)method.CreateDelegate(typeof(Func<int>), target!);
SysAbiFunction adapter = _ => noArg();
return adapter;
}
return method.IsStatic
? method.CreateDelegate(typeof(SysAbiFunction))
: method.CreateDelegate(typeof(SysAbiFunction), target!);
}
private static void ValidateSignature(MethodInfo method)
{
if (method.ReturnType != typeof(int))
{
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must return int.");
}
var parameters = method.GetParameters();
if (parameters.Length == 0)
{
return;
}
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(CpuContext))
{
return;
}
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must accept no arguments or one {nameof(CpuContext)} argument.");
}
private static ExportInfo? ResolveExportInfo(
SysAbiExportAttribute exportAttribute,
MethodInfo method,
Generation generation,
ISymbolCatalog? symbolCatalog)
{
var target = exportAttribute.Target == Generation.None
? generation
: exportAttribute.Target;
if ((target & generation) == 0)
{
return null;
}
var nid = exportAttribute.Nid;
var exportName = exportAttribute.ExportName;
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName) && symbolCatalog?.TryGetByExportName(exportName, out var byName) == true)
{
nid = byName.Nid;
}
if (!string.IsNullOrWhiteSpace(nid) && symbolCatalog?.TryGetByNid(nid, out var byNid) == true)
{
exportName = string.IsNullOrWhiteSpace(exportName) ? byNid.ExportName : exportName;
target = exportAttribute.Target == Generation.None ? byNid.Target : target;
}
if (string.IsNullOrWhiteSpace(nid))
{
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must define a NID or match one in symbols catalog.");
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
if ((target & generation) == 0)
{
return null;
}
var libraryName = string.IsNullOrWhiteSpace(exportAttribute.LibraryName) ? "libKernel" : exportAttribute.LibraryName;
return new ExportInfo(nid, exportName, libraryName, target);
}
private readonly record struct ExportInfo(string Nid, string ExportName, string LibraryName, Generation Target);
}
+47 -1
View File
@@ -14,6 +14,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ItemGroup>
<InternalsVisibleTo Include="SharpEmu.Core" />
<InternalsVisibleTo Include="SharpEmu.Libs.Tests" />
</ItemGroup>
<ItemGroup>
@@ -21,6 +22,51 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Aerolib\aerolib.bin" />
<!-- Forces build ordering for the aerolib task below; loaded as a build component,
never a runtime dependency. -->
<ProjectReference Include="..\SharpEmu.SourceGenerators\SharpEmu.SourceGenerators.csproj"
ReferenceOutputAssembly="false" />
</ItemGroup>
<!-- aerolib.bin (the runtime NID -> name catalog) is derived data: generated here
from scripts/ps5_names.txt via the same Ps5Nid implementation the analyzers use,
embedded from the intermediate directory, and never committed. Replaces
scripts/generate_aerolib_binary.py. -->
<PropertyGroup>
<SharpEmuAerolibNamesFile>$(MSBuildThisFileDirectory)..\..\scripts\ps5_names.txt</SharpEmuAerolibNamesFile>
<SharpEmuAerolibTaskAssembly>$(MSBuildThisFileDirectory)..\..\artifacts\bin\$(Configuration)\netstandard2.0\SharpEmu.SourceGenerators.dll</SharpEmuAerolibTaskAssembly>
</PropertyGroup>
<UsingTask TaskName="SharpEmu.SourceGenerators.GenerateAerolibBinaryTask"
TaskFactory="TaskHostFactory"
AssemblyFile="$(SharpEmuAerolibTaskAssembly)" />
<!-- Runs after ResolveProjectReferences (which builds the task assembly) and before
resource processing embeds the output. Skipped for design-time builds: the IDE
resolves project references without compiling them, so on a fresh clone the task
assembly does not exist yet and loading it would break the design-time build. -->
<Target Name="GenerateAerolibBinary"
DependsOnTargets="ResolveProjectReferences"
Inputs="$(SharpEmuAerolibNamesFile);$(SharpEmuAerolibTaskAssembly)"
Outputs="$(IntermediateOutputPath)aerolib.bin">
<GenerateAerolibBinaryTask NamesFile="$(SharpEmuAerolibNamesFile)"
OutputFile="$(IntermediateOutputPath)aerolib.bin" />
</Target>
<!-- The resource item is created inside a target (not statically) so design-time
builds never reference a file that was not generated, and in a separate target
from the generation so an up-to-date skip of GenerateAerolibBinary cannot drop
the item (an up-to-date target skips its whole body, ItemGroups included).
Hooked before AssignTargetPaths: dynamic EmbeddedResource items added later
than that miss the resource pipeline entirely. -->
<Target Name="EmbedAerolibBinary"
BeforeTargets="AssignTargetPaths"
DependsOnTargets="GenerateAerolibBinary"
Condition="'$(DesignTimeBuild)' != 'true'">
<ItemGroup>
<EmbeddedResource Include="$(IntermediateOutputPath)aerolib.bin"
LogicalName="SharpEmu.HLE.Aerolib.aerolib.bin"
Visible="false" />
</ItemGroup>
</Target>
</Project>
-10
View File
@@ -1,10 +0,0 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"sharpemu.logging": {
"type": "Project"
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,143 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
public static partial class AgcExports
{
// Exact Gen5 primary register defaults originally reverse-engineered by Kyty (MIT).
// Hashes, offsets, and values match the tables consumed by the PS5 AGC SDK.
private static RegisterDefaultGroup[] CreatePrimaryRegisterDefaults() =>
[
// context register groups
new(0u, 0u, 0xE24F806Du, [new(0x202u, 0x00CC0010u)]), // CB_COLOR_CONTROL
new(0u, 1u, 0xF6C28182u, [new(0x109u, 0x00000000u)]), // CB_DCC_CONTROL
new(0u, 2u, 0x6F6E55A5u, [new(0x104u, 0x00000000u)]), // CB_RMI_GL2_CACHE_CONTROL
new(0u, 3u, 0x0BC65DA4u, [new(0x08Fu, 0x00000000u)]), // CB_SHADER_MASK
new(0u, 4u, 0x9E5AD592u, [new(0x08Eu, 0x0000000Fu)]), // CB_TARGET_MASK
new(0u, 5u, 0xBB513B98u, [new(0x2DCu, 0x0000AA00u)]), // DB_ALPHA_TO_MASK
new(0u, 6u, 0xAB64B23Bu, [new(0x001u, 0x00000000u)]), // DB_COUNT_CONTROL
new(0u, 7u, 0x53C39964u, [new(0x200u, 0x00000000u)]), // DB_DEPTH_CONTROL
new(0u, 8u, 0x01396B11u, [new(0x201u, 0x00000000u)]), // DB_EQAA
new(0u, 9u, 0x7D42019Au, [new(0x000u, 0x00000000u)]), // DB_RENDER_CONTROL
new(0u, 10u, 0x3548F523u, [new(0x006u, 0x00000000u)]), // PS_SHADER_SAMPLE_EXCLUSION_MASK
new(0u, 11u, 0xF43AD28Au, [new(0x01Fu, 0x00000000u)]), // DB_RMI_L2_CACHE_CONTROL
new(0u, 12u, 0x6DE4C312u, [new(0x203u, 0x00000000u)]), // DB_SHADER_CONTROL
new(0u, 13u, 0x00A77AE0u, [new(0x2B0u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE0
new(0u, 14u, 0x00A779B7u, [new(0x2B1u, 0x00000000u)]), // DB_SRESULTS_COMPARE_STATE1
new(0u, 15u, 0x5100100Cu, [new(0x10Cu, 0x00000000u)]), // DB_STENCILREFMASK
new(0u, 16u, 0x59958BBAu, [new(0x10Du, 0x00000000u)]), // DB_STENCILREFMASK_BF
new(0u, 17u, 0x0C06F17Cu, [new(0x10Bu, 0x00000000u)]), // DB_STENCIL_CONTROL
new(0u, 18u, 0x6F104B72u, [new(0x1FFu, 0x00000000u)]), // GE_MAX_OUTPUT_PER_SUBGROUP
new(0u, 19u, 0x25C70D9Cu, [new(0x204u, 0x00000000u)]), // PA_CL_CLIP_CNTL
new(0u, 20u, 0x3881201Eu, [new(0x20Du, 0x00000000u)]), // PA_CL_OBJPRIM_ID_CNTL
new(0u, 21u, 0x09AFDDAFu, [new(0x206u, 0x0000043Fu)]), // PA_CL_VTE_CNTL
new(0u, 22u, 0x367D63CFu, [new(0x2F8u, 0x00000000u)]), // PA_SC_AA_CONFIG
new(0u, 23u, 0x43707DB8u, [new(0x083u, 0x0000FFFFu)]), // PA_SC_CLIPRECT_RULE
new(0u, 24u, 0xF6AE26BAu, [new(0x313u, 0x00000000u)]), // PA_SC_CONSERVATIVE_RASTERIZATION_CNTL
new(0u, 25u, 0x1B917652u, [new(0x800003FEu, 0x00000000u)]), // PA_SC_FSR_ENABLE
new(0u, 26u, 0x94B1E4F7u, [new(0x0EAu, 0x00000000u)]), // PA_SC_HORIZ_GRID
new(0u, 27u, 0xE3661B6Cu, [new(0x0E9u, 0x00000000u)]), // PA_SC_LEFT_VERT_GRID
new(0u, 28u, 0x1EB8D73Au, [new(0x292u, 0x00000002u)]), // PA_SC_MODE_CNTL_0
new(0u, 29u, 0x15051FA3u, [new(0x293u, 0x00000000u)]), // PA_SC_MODE_CNTL_1
new(0u, 30u, 0x9C51A7F1u, [new(0x0E8u, 0x00000000u)]), // PA_SC_RIGHT_VERT_GRID
new(0u, 31u, 0xA20EFC70u, [new(0x080u, 0x00000000u)]), // PA_SC_WINDOW_OFFSET
new(0u, 32u, 0x0EC09F6Eu, [new(0x211u, 0x00000000u)]), // PA_STATE_STEREO_X
new(0u, 33u, 0x34A7D6D3u, [new(0x210u, 0x00000000u)]), // PA_STEREO_CNTL
new(0u, 34u, 0xCE831B94u, [new(0x08Du, 0x00000000u)]), // PA_SU_HARDWARE_SCREEN_OFFSET
new(0u, 35u, 0x5CC72A74u, [new(0x282u, 0x00000008u)]), // PA_SU_LINE_CNTL
new(0u, 36u, 0x3B77713Cu, [new(0x281u, 0xFFFF0000u)]), // PA_SU_POINT_MINMAX
new(0u, 37u, 0x40F64410u, [new(0x280u, 0x00080008u)]), // PA_SU_POINT_SIZE
new(0u, 38u, 0x69441268u, [new(0x2DFu, 0x00000000u)]), // PA_SU_POLY_OFFSET_CLAMP
new(0u, 39u, 0x2E418B83u, [new(0x2DEu, 0x000001E9u)]), // PA_SU_POLY_OFFSET_DB_FMT_CNTL
new(0u, 40u, 0xA00D0C8Du, [new(0x205u, 0x00000240u)]), // PA_SU_SC_MODE_CNTL
new(0u, 41u, 0xB1289FB3u, [new(0x20Cu, 0x00000001u)]), // PA_SU_SMALL_PRIM_FILTER_CNTL
new(0u, 42u, 0x144832FBu, [new(0x2F9u, 0x0000002Du)]), // PA_SU_VTX_CNTL
new(0u, 43u, 0x9890D9FAu, [new(0x1BAu, 0x00000000u)]), // SPI_TMPRING_SIZE
new(0u, 44u, 0x9016FAF1u, [new(0x2A6u, 0x00000000u)]), // VGT_DRAW_PAYLOAD_CNTL
new(0u, 45u, 0x4B73CE27u, [new(0x2CEu, 0x00000400u)]), // VGT_GS_MAX_VERT_OUT
new(0u, 46u, 0x5F5A3E7Bu, [new(0x29Bu, 0x00000002u)]), // VGT_GS_OUT_PRIM_TYPE
new(0u, 47u, 0xD4AF3A51u, [new(0x2D6u, 0x00000000u)]), // VGT_LS_HS_CONFIG
new(0u, 48u, 0x6CF4F543u, [new(0x2A3u, 0xFFFFFFFFu)]), // VGT_PRIMITIVEID_RESET
new(0u, 49u, 0x5FB86CCBu, [new(0x2A1u, 0x00000000u)]), // VGT_PRIMITIVEID_EN
new(0u, 50u, 0xEDEFA188u, [new(0x2ADu, 0x00000000u)]), // VGT_REUSE_OFF
new(0u, 51u, 0xD0DE9EE6u, [new(0x2D5u, 0x00000000u)]), // VGT_SHADER_STAGES_EN
new(0u, 52u, 0xC5831803u, [new(0x2D4u, 0x88101000u)]), // VGT_TESS_DISTRIBUTION
new(0u, 53u, 0x8E6DE84Bu, [new(0x2DBu, 0x00000000u)]), // VGT_TF_PARAM
new(0u, 54u, 0xD0771662u, [new(0x2F5u, 0x00000000u), new(0x2F6u, 0x00000000u)]), // PA_SC_CENTROID_PRIORITY_0, PA_SC_CENTROID_PRIORITY_1
new(0u, 55u, 0x569F7444u, [new(0x2FEu, 0x00000000u)]), // PA_SC_AA_SAMPLE_LOCS_PIXEL_X0Y0_0
new(0u, 56u, 0x5C6637CDu, [new(0x30Eu, 0xFFFFFFFFu), new(0x30Fu, 0xFFFFFFFFu)]), // PA_SC_AA_MASK_X0Y0_X1Y0, PA_SC_AA_MASK_X0Y1_X1Y1
new(0u, 57u, 0xCAE3E690u, [new(0x311u, 0x00000002u), new(0x312u, 0x03FF0080u)]), // PA_SC_BINNER_CNTL_0, PA_SC_BINNER_CNTL_1
new(0u, 58u, 0x43FBD769u, [new(0x105u, 0x00000000u), new(0x107u, 0x00000000u), new(0x106u, 0x00000000u), new(0x108u, 0x00000000u)]), // CB_BLEND_RED, CB_BLEND_BLUE, CB_BLEND_GREEN, CB_BLEND_ALPHA
new(0u, 59u, 0xEF550356u, [new(0x1E0u, 0x20010001u)]), // CB_BLEND0_CONTROL
new(0u, 60u, 0x8F52E279u, [new(0x020u, 0x00000000u), new(0x021u, 0x00000000u)]), // TA_BC_BASE_ADDR, TA_BC_BASE_ADDR_HI
new(0u, 61u, 0x1F2D8149u, [new(0x084u, 0x00000000u), new(0x085u, 0x20002000u)]), // PA_SC_CLIPRECT_0_TL, PA_SC_CLIPRECT_0_BR
new(0u, 62u, 0x853D0614u, [new(0x800003FFu, 0x00000000u)]), // CX_NOP
new(0u, 63u, 0x4413C6F9u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // DB_DEPTH_BOUNDS_MIN, DB_DEPTH_BOUNDS_MAX
new(0u, 64u, 0x67096014u, [new(0x010u, 0x80000000u), new(0x011u, 0x20000000u), new(0x012u, 0x00000000u), new(0x013u, 0x00000000u), new(0x014u, 0x00000000u), new(0x015u, 0x00000000u), new(0x01Au, 0x00000000u), new(0x01Bu, 0x00000000u), new(0x01Cu, 0x00000000u), new(0x01Du, 0x00000000u), new(0x01Eu, 0x00000000u), new(0x002u, 0x00000000u), new(0x005u, 0x00000000u), new(0x007u, 0x00000000u), new(0x00Bu, 0x00000000u), new(0x00Au, 0x00000000u)]), // DB_Z_INFO, DB_STENCIL_INFO, DB_Z_READ_BASE, DB_STENCIL_READ_BASE, DB_Z_WRITE_BASE, DB_STENCIL_WRITE_BASE, DB_Z_READ_BASE_HI, DB_STENCIL_READ_BASE_HI, DB_Z_WRITE_BASE_HI, DB_STENCIL_WRITE_BASE_HI, DB_HTILE_DATA_BASE_HI, DB_DEPTH_VIEW, DB_HTILE_DATA_BASE, DB_DEPTH_SIZE_XY, DB_DEPTH_CLEAR, DB_STENCIL_CLEAR
new(0u, 65u, 0x88F5E915u, [new(0x0EBu, 0xFF00FF00u), new(0x0ECu, 0x00000000u)]), // PA_SC_FOV_WINDOW_LR, PA_SC_FOV_WINDOW_TB
new(0u, 66u, 0x033F1EFFu, [new(0x800003FCu, 0x00000000u), new(0x800003FDu, 0x00000000u)]), // FSR_RECURSIONS0, FSR_RECURSIONS1
new(0u, 67u, 0x918106BBu, [new(0x090u, 0x80000000u), new(0x091u, 0x40004000u)]), // PA_SC_GENERIC_SCISSOR_TL, PA_SC_GENERIC_SCISSOR_BR
new(0u, 68u, 0x95F0E7ACu, [new(0x2FAu, 0x4E7E0000u), new(0x2FBu, 0x4E7E0000u), new(0x2FCu, 0x4E7E0000u), new(0x2FDu, 0x4E7E0000u)]), // PA_CL_GB_VERT_CLIP_ADJ, PA_CL_GB_VERT_DISC_ADJ, PA_CL_GB_HORZ_CLIP_ADJ, PA_CL_GB_HORZ_DISC_ADJ
new(0u, 69u, 0xB48CBAB2u, [new(0x2E2u, 0x00000000u), new(0x2E3u, 0x00000000u)]), // PA_SU_POLY_OFFSET_BACK_SCALE, PA_SU_POLY_OFFSET_BACK_OFFSET
new(0u, 70u, 0x05BB3BC6u, [new(0x2E0u, 0x00000000u), new(0x2E1u, 0x00000000u)]), // PA_SU_POLY_OFFSET_FRONT_SCALE, PA_SU_POLY_OFFSET_FRONT_OFFSET
new(0u, 71u, 0x94FABA07u, [new(0x003u, 0x00000000u), new(0x004u, 0x00000000u)]), // DB_RENDER_OVERRIDE, DB_RENDER_OVERRIDE2
new(0u, 72u, 0x38E92C91u, [new(0x318u, 0x00000000u), new(0x31Bu, 0x00000000u), new(0x31Cu, 0x00000000u), new(0x31Du, 0x00000000u), new(0x31Eu, 0x00000048u), new(0x31Fu, 0x00000000u), new(0x321u, 0x00000000u), new(0x323u, 0x00000000u), new(0x324u, 0x00000000u), new(0x325u, 0x00000000u), new(0x390u, 0x00000000u), new(0x398u, 0x00000000u), new(0x3A0u, 0x00000000u), new(0x3A8u, 0x00000000u), new(0x3B0u, 0x00000000u), new(0x3B8u, 0x0006C000u)]), // CB_COLOR0_BASE, CB_COLOR0_VIEW, CB_COLOR0_INFO, CB_COLOR0_ATTRIB, CB_COLOR0_DCC_CONTROL, CB_COLOR0_CMASK, CB_COLOR0_FMASK, CB_COLOR0_CLEAR_WORD0, CB_COLOR0_CLEAR_WORD1, CB_COLOR0_DCC_BASE, CB_COLOR0_BASE_EXT, CB_COLOR0_CMASK_BASE_EXT, CB_COLOR0_FMASK_BASE_EXT, CB_COLOR0_DCC_BASE_EXT, CB_COLOR0_ATTRIB2, CB_COLOR0_ATTRIB3
new(0u, 73u, 0x0B177B43u, [new(0x00Cu, 0x00000000u), new(0x00Du, 0x40004000u)]), // PA_SC_SCREEN_SCISSOR_TL, PA_SC_SCREEN_SCISSOR_BR
new(0u, 74u, 0x48531062u, [new(0x191u, 0x00000000u)]), // SPI_PS_INPUT_CNTL_0
new(0u, 75u, 0xAAA964B9u, [new(0x16Fu, 0x00000000u), new(0x170u, 0x00000000u), new(0x171u, 0x00000000u), new(0x172u, 0x00000000u)]), // PA_CL_UCP_0_X, PA_CL_UCP_0_Y, PA_CL_UCP_0_Z, PA_CL_UCP_0_W
new(0u, 76u, 0x7690AF6Fu, [new(0x10Fu, 0x4E7E0000u), new(0x111u, 0x4E7E0000u), new(0x113u, 0x4E7E0000u), new(0x110u, 0x00000000u), new(0x112u, 0x00000000u), new(0x114u, 0x00000000u), new(0x094u, 0x80000000u), new(0x095u, 0x40004000u), new(0x0B4u, 0x00000000u), new(0x0B5u, 0x00000000u)]), // PA_CL_VPORT_XSCALE, PA_CL_VPORT_YSCALE, PA_CL_VPORT_ZSCALE, PA_CL_VPORT_XOFFSET, PA_CL_VPORT_YOFFSET, PA_CL_VPORT_ZOFFSET, PA_SC_VPORT_SCISSOR_0_TL, PA_SC_VPORT_SCISSOR_0_BR, PA_SC_VPORT_ZMIN_0, PA_SC_VPORT_ZMAX_0
new(0u, 77u, 0x078D7060u, [new(0x081u, 0x80000000u), new(0x082u, 0x40004000u)]), // PA_SC_WINDOW_SCISSOR_TL, PA_SC_WINDOW_SCISSOR_BR
// shader register groups
new(1u, 0u, 0x5D6E3EC7u, [new(0x212u, 0x00000000u)]), // COMPUTE_PGM_RSRC1
new(1u, 1u, 0x57E7079Au, [new(0x213u, 0x00000000u)]), // COMPUTE_PGM_RSRC2
new(1u, 2u, 0x7467FAFDu, [new(0x228u, 0x00000000u)]), // COMPUTE_PGM_RSRC3
new(1u, 3u, 0x9E826B50u, [new(0x215u, 0x00000000u)]), // COMPUTE_RESOURCE_LIMITS
new(1u, 4u, 0xDC484F18u, [new(0x218u, 0x00000000u)]), // COMPUTE_TMPRING_SIZE
new(1u, 5u, 0x5DA8BCA3u, [new(0x08Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_GS
new(1u, 6u, 0x5CA726D8u, [new(0x10Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_HS
new(1u, 7u, 0x5DD28360u, [new(0x00Au, 0x00000000u)]), // SPI_SHADER_PGM_RSRC1_PS
new(1u, 8u, 0x57EFA0BEu, [new(0x08Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_GS
new(1u, 9u, 0x502363D5u, [new(0x10Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_HS
new(1u, 10u, 0x506D14BDu, [new(0x00Bu, 0x00000000u)]), // SPI_SHADER_PGM_RSRC2_PS
new(1u, 11u, 0xB2609506u, [new(0x224u, 0x00000000u)]), // COMPUTE_USER_ACCUM_0
new(1u, 12u, 0x9E5CFB8Au, [new(0x107u, 0x00000000u), new(0x087u, 0x00000000u), new(0x007u, 0x00000000u)]), // SPI_SHADER_PGM_RSRC3_HS, SPI_SHADER_PGM_RSRC3_GS, SPI_SHADER_PGM_RSRC3_PS
new(1u, 13u, 0xC918DF3Eu, [new(0x20Cu, 0x00000000u), new(0x20Du, 0x00000000u)]), // COMPUTE_PGM_LO, COMPUTE_PGM_HI
new(1u, 14u, 0xC9751C9Cu, [new(0x0C8u, 0x00000000u), new(0x0C9u, 0x00000000u)]), // SPI_SHADER_PGM_LO_ES, SPI_SHADER_PGM_HI_ES
new(1u, 15u, 0xC97EF77Au, [new(0x088u, 0x00000000u), new(0x089u, 0x00000000u)]), // SPI_SHADER_PGM_LO_GS, SPI_SHADER_PGM_HI_GS
new(1u, 16u, 0xC927C6B9u, [new(0x108u, 0x00000000u), new(0x109u, 0x00000000u)]), // SPI_SHADER_PGM_LO_HS, SPI_SHADER_PGM_HI_HS
new(1u, 17u, 0xC92A1EC5u, [new(0x148u, 0x00000000u), new(0x149u, 0x00000000u)]), // SPI_SHADER_PGM_LO_LS, SPI_SHADER_PGM_HI_LS
new(1u, 18u, 0xC9E01B31u, [new(0x008u, 0x00000000u), new(0x009u, 0x00000000u)]), // SPI_SHADER_PGM_LO_PS, SPI_SHADER_PGM_HI_PS
new(1u, 19u, 0x50685F29u, [new(0x800002FFu, 0x00000000u)]), // SH_NOP
new(1u, 20u, 0xB26219CAu, [new(0x0B2u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_ESGS_0
new(1u, 21u, 0xB25B6CF9u, [new(0x132u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_LSHS_0
new(1u, 22u, 0xB2F86101u, [new(0x032u, 0x00000000u)]), // SPI_SHADER_USER_ACCUM_PS_0
new(1u, 23u, 0x07E3B155u, [new(0x082u, 0x00000000u), new(0x083u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_GS, SPI_SHADER_USER_DATA_ADDR_HI_GS
new(1u, 24u, 0x07E383C6u, [new(0x102u, 0x00000000u), new(0x103u, 0x00000000u)]), // SPI_SHADER_USER_DATA_ADDR_LO_HS, SPI_SHADER_USER_DATA_ADDR_HI_HS
new(1u, 25u, 0xBDA98653u, [new(0x240u, 0x00000000u)]), // COMPUTE_USER_DATA_0
new(1u, 26u, 0xBDBD1D0Fu, [new(0x08Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_GS_0
new(1u, 27u, 0xBD946FD4u, [new(0x10Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_HS_0
new(1u, 28u, 0xBDF02A4Cu, [new(0x00Cu, 0x00000000u)]), // SPI_SHADER_USER_DATA_PS_0
// uconfig register groups
new(2u, 0u, 0x19E93E85u, [new(0x41Fu, 0x00000000u)]), // GDS_OA_ADDRESS
new(2u, 1u, 0x3B5C2AF3u, [new(0x41Du, 0x00000000u)]), // GDS_OA_CNTL
new(2u, 2u, 0x47974A35u, [new(0x41Eu, 0x00000000u)]), // GDS_OA_COUNTER
new(2u, 3u, 0x105971C2u, [new(0x25Bu, 0x00000000u)]), // GE_CNTL
new(2u, 4u, 0x7D137765u, [new(0x24Au, 0x00000000u)]), // GE_INDX_OFFSET
new(2u, 5u, 0xD187FEBCu, [new(0x24Bu, 0x00000000u)]), // GE_MULTI_PRIM_IB_RESET_EN
new(2u, 6u, 0x12F854ACu, [new(0x25Fu, 0x00000000u)]), // GE_STEREO_CNTL
new(2u, 7u, 0x40D49AD1u, [new(0x262u, 0x00000000u)]), // GE_USER_VGPR_EN
new(2u, 8u, 0x8C0923DAu, [new(0x80003FF4u, 0x00000000u)]), // FSR_EXTEND_SUBPIXEL_ROUNDING
new(2u, 9u, 0xBB8DF494u, [new(0x80003FFDu, 0x00000000u)]), // TEXTURE_GRADIENT_CONTROL
new(2u, 10u, 0xF6D8A76Eu, [new(0x382u, 0x40000040u)]), // TEXTURE_GRADIENT_FACTORS
new(2u, 11u, 0x7620F1E9u, [new(0x248u, 0x00000000u)]), // VGT_OBJECT_ID
new(2u, 12u, 0x9EBFAB10u, [new(0x242u, 0x00000000u)]), // VGT_PRIMITIVE_TYPE
new(2u, 13u, 0x98A09D0Eu, [new(0x380u, 0x00000000u), new(0x381u, 0x00000000u)]), // TA_CS_BC_BASE_ADDR, TA_CS_BC_BASE_ADDR_HI
new(2u, 14u, 0x195D37D2u, [new(0x80003FF5u, 0x00000000u), new(0x80003FF6u, 0x00000000u)]), // FSR_ALPHA_VALUE0, FSR_ALPHA_VALUE1
new(2u, 15u, 0xF9EC4F85u, [new(0x80003FF7u, 0x00000000u), new(0x80003FF8u, 0x00000000u), new(0x80003FF9u, 0x00000000u), new(0x80003FFAu, 0x00000000u)]), // FSR_CONTROL_POINT0, FSR_CONTROL_POINT1, FSR_CONTROL_POINT2, FSR_CONTROL_POINT3
new(2u, 16u, 0x4626B750u, [new(0x80003FFBu, 0x00000000u), new(0x80003FFCu, 0x00000000u)]), // FSR_WINDOW0, FSR_WINDOW1
new(2u, 17u, 0x4CC673A0u, [new(0x80003FFEu, 0x00000000u)]), // MEMORY_MAPPING_MASK
new(2u, 18u, 0xDE5B3431u, [new(0x80003FFFu, 0x00000000u)]), // UC_NOP
new(2u, 19u, 0x036AC8A6u, [new(0x25Cu, 0x00000000u)]), // GE_USER_VGPR1
];
}
@@ -0,0 +1,33 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.VideoOut;
using SharpEmu.ShaderCompiler;
namespace SharpEmu.Libs.Agc;
/// <summary>
/// Wires the backend-neutral shader compiler to this assembly's HLE services. The
/// module initializer runs before any Libs code can invoke the evaluator, so the hook
/// is always installed first.
/// </summary>
internal static class AgcShaderCompilerHooks
{
[ModuleInitializer]
[SuppressMessage(
"Usage",
"CA2255:The 'ModuleInitializer' attribute should not be used in libraries",
Justification = "This is the rule's intended advanced scenario: the hook must be " +
"installed before any code path can reach the evaluator, and every such path " +
"enters through this assembly.")]
internal static void Install()
{
Gen5ShaderScalarEvaluator.FallbackMemoryReader =
KernelMemoryCompatExports.TryReadTrackedLibcHeap;
Gen5ShaderScalarEvaluator.GlobalMemoryPool =
VulkanVideoPresenter.GuestDataPool;
}
}
File diff suppressed because it is too large Load Diff
+486
View File
@@ -0,0 +1,486 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
/// <summary>
/// Deswizzles RDNA2 (GFX10) tiled texture surfaces into linear layout so they
/// can be uploaded to Vulkan. PS5 stores most textures in a swizzled layout
/// selected by the 5-bit SWIZZLE_MODE in the image descriptor; uploading those
/// bytes verbatim samples as garbage.
///
/// The GFX10 addressing uses power-of-two swizzle blocks (256 B / 4 KiB /
/// 64 KiB) whose internal element order follows the standard (S), display (D),
/// render (R), or z-order/depth (Z) equations. This implements the exact base
/// S and Z 2D single-sample modes plus the PS5's RB+ 64 KiB Z_X/R_X equations;
/// other D/R and pipe/bank-XOR modes stay opt-in while their complete AddrLib
/// equations are being ported.
/// </summary>
internal static class GnmTiling
{
// Oberon uses the 16-pipe / 8-pixel-packer RB+ topology. These are the
// single-sample 64 KiB equations generated by AMD AddrLib for that exact
// topology. Each entry describes one address bit as an XOR of X/Y bits.
private readonly record struct AddressBit(uint XMask, uint YMask);
private static readonly AddressBit[][] RbPlus64KRenderX =
[
// 1 byte/element: nibble01=0, nibble2=307, nibble3=379.
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(6), Y(6), XY(7, 8), XY(8, 7)],
// 2 bytes/element: nibble01=1, nibble2=307, nibble3=389.
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
Y(3), X(6), XY(7, 7), XY(8, 6)],
// 4 bytes/element: nibble01=39, nibble2=307, nibble3=381.
[Zero, Zero, X(0), X(1), Y(0), Y(1), X(2), Y(2),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(3), Y(3), XY(6, 7), XY(7, 6)],
// 8 bytes/element: nibble01=6, nibble2=307, nibble3=382.
[Zero, Zero, Zero, X(0), Y(0), X(1), X(2), Y(1),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
Y(2), X(3), XY(7, 3), XY(6, 6)],
// 16 bytes/element: nibble01=7, nibble2=307, nibble3=390.
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(2), Y(2), XY(6, 3), XY(3, 6)],
];
private static readonly AddressBit[][] RbPlus64KDepthX =
[
// 1 byte/element: nibble01=8, nibble2=306, nibble3=379.
[X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3), Y(3),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(6), Y(6), XY(7, 8), XY(8, 7)],
// 2 bytes/element: nibble01=9, nibble2=306, nibble3=389.
[Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2), X(3),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
Y(3), X(6), XY(7, 7), XY(8, 6)],
// 4 bytes/element: nibble01=10, nibble2=306, nibble3=381.
[Zero, Zero, X(0), Y(0), X(1), Y(1), X(2), Y(2),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(3), Y(3), XY(6, 7), XY(7, 6)],
// 8 bytes/element: nibble01=11, nibble2=307, nibble3=382.
[Zero, Zero, Zero, X(0), Y(0), X(1), Y(1), X(2),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
Y(2), X(3), XY(7, 3), XY(6, 6)],
// 16 bytes/element is identical to R_X for a 2D single-sample image.
[Zero, Zero, Zero, Zero, X(0), Y(0), X(1), Y(1),
XY(7, 4, 7), XY(4, 4), XY(6, 5), XY(5, 6),
X(2), Y(2), XY(6, 3), XY(3, 6)],
];
private static readonly AddressBit[][] RbPlus64KStandard =
[
// GFX10_SW_64K_S_RBPLUS_PATINFO, 1 byte/element.
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
Y(4), X(4), Y(5), X(5), Y(6), X(6), Y(7), X(7)],
// 2 bytes/element.
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
Y(3), X(4), Y(4), X(5), Y(5), X(6), Y(6), X(7)],
// 4 bytes/element.
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
Y(3), X(3), Y(4), X(4), Y(5), X(5), Y(6), X(6)],
// 8 bytes/element (also BC1/BC4 compressed blocks).
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
Y(2), X(3), Y(3), X(4), Y(4), X(5), Y(5), X(6)],
// 16 bytes/element (also 16-byte BC compressed blocks).
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
Y(2), X(2), Y(3), X(3), Y(4), X(4), Y(5), X(5)],
];
// GFX10 4K_S has a separate 12-bit micro-tile equation. It is not the
// generic x/y interleave used by the 64K standard block; using that larger
// equation leaves a regular grid in linearized atlases.
private static readonly AddressBit[][] Standard4K =
[
[X(0), X(1), X(2), X(3), Y(0), Y(1), Y(2), Y(3),
Y(4), X(4), Y(5), X(5)],
[Zero, X(0), X(1), X(2), Y(0), Y(1), Y(2), X(3),
Y(3), X(4), Y(4), X(5)],
[Zero, Zero, X(0), X(1), Y(0), Y(1), Y(2), X(2),
Y(3), X(3), Y(4), X(4)],
[Zero, Zero, Zero, X(0), Y(0), Y(1), X(1), X(2),
Y(2), X(3), Y(3), X(4)],
[Zero, Zero, Zero, Zero, Y(0), Y(1), X(0), X(1),
Y(2), X(2), Y(3), X(3)],
];
private static readonly bool _enabled = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
"1",
StringComparison.Ordinal);
private static readonly bool _disabled = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DETILE"),
"0",
StringComparison.Ordinal);
private static readonly HashSet<uint> _reportedModes = new();
public static bool Enabled => _enabled || !_disabled;
/// <summary>
/// Base S/Z modes and the Oberon RB+ 64 KiB Z_X/R_X modes for which this
/// implementation carries the exact AddrLib address equations. Other
/// bank/pipe-XOR variants remain opt-in.
/// </summary>
private static bool IsTrustedByDefault(uint swizzleMode) =>
// Exact base S/Z modes. D/R use different GFX10 swizzle equations and
// the T/X modes additionally apply pipe/bank XOR between blocks.
swizzleMode is 1 or 4 or 5 or 8 or 9 or 24 or 27;
// Detile a surface when it is verified-correct by default (trusted base mode),
// or when the user opts the approximate modes in with SHARPEMU_DETILE=1.
// SHARPEMU_DETILE=0 forces the old raw-upload behavior for everything.
private static bool ShouldDetile(uint swizzleMode) =>
swizzleMode != 0 && !_disabled && (_enabled || IsTrustedByDefault(swizzleMode));
/// <summary>
/// True when a surface with the given swizzle mode needs deswizzling.
/// Mode 0 is linear and never needs it.
/// </summary>
public static bool NeedsDetile(uint swizzleMode) => ShouldDetile(swizzleMode);
/// <summary>
/// Gets the physical byte span occupied by a tiled mip. GNM allocates whole
/// swizzle blocks even when the logical image is much smaller than a block;
/// reading only the logical texel count truncates most source offsets during
/// detiling (a 64x64 BC1 mode-9 image is 2 KiB logically but occupies one
/// 64 KiB swizzle block).
/// </summary>
public static bool TryGetTiledByteCount(
uint swizzleMode,
int elementsWide,
int elementsHigh,
int bytesPerElement,
out ulong byteCount)
{
byteCount = 0;
if (!ShouldDetile(swizzleMode) ||
elementsWide <= 0 ||
elementsHigh <= 0 ||
bytesPerElement <= 0 ||
!TryGetSwizzleKind(swizzleMode, out _, out var blockBytes))
{
return false;
}
var bppLog2 = BitLog2((uint)bytesPerElement);
if (bppLog2 < 0)
{
return false;
}
var blockElements = blockBytes >> bppLog2;
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
if (blockWidth == 0 || blockHeight == 0)
{
return false;
}
var blocksWide = ((ulong)elementsWide + (ulong)blockWidth - 1) / (ulong)blockWidth;
var blocksHigh = ((ulong)elementsHigh + (ulong)blockHeight - 1) / (ulong)blockHeight;
try
{
byteCount = checked(blocksWide * blocksHigh * (ulong)blockBytes);
return true;
}
catch (OverflowException)
{
byteCount = 0;
return false;
}
}
/// <summary>
/// Deswizzles <paramref name="tiled"/> into linear row-major order.
/// Elements are pixels for uncompressed formats and 4x4 blocks for
/// block-compressed formats, so callers pass the element grid dimensions
/// and the bytes per element. Returns false (leaving output untouched) for
/// unsupported swizzle modes so the caller can fall back to the raw bytes.
/// </summary>
public static bool TryDetile(
ReadOnlySpan<byte> tiled,
Span<byte> linear,
uint swizzleMode,
int elementsWide,
int elementsHigh,
int bytesPerElement)
{
if (!ShouldDetile(swizzleMode) || elementsWide <= 0 || elementsHigh <= 0 || bytesPerElement <= 0)
{
return false;
}
if (!TryGetSwizzleKind(swizzleMode, out var kind, out var blockBytes))
{
ReportUnsupported(swizzleMode);
return false;
}
var bppLog2 = BitLog2((uint)bytesPerElement);
if (bppLog2 < 0)
{
// Non-power-of-two element size (e.g. 24-bit) is not swizzled in a
// way this equation models.
ReportUnsupported(swizzleMode);
return false;
}
// Block dimensions in elements: a swizzle block holds blockBytes bytes,
// laid out as a square-ish power-of-two element grid scaled by bpp.
var blockElements = blockBytes >> bppLog2;
var (blockWidth, blockHeight) = SquareBlockDimensions(blockElements);
if (blockWidth == 0 || blockHeight == 0)
{
ReportUnsupported(swizzleMode);
return false;
}
var blocksPerRow = (elementsWide + blockWidth - 1) / blockWidth;
var requiredLinear = (long)elementsWide * elementsHigh * bytesPerElement;
if (linear.Length < requiredLinear)
{
return false;
}
// Precompute the within-block element offset for each (x, y) inside a
// single block. The swizzle equation only depends on the in-block
// coordinates, so this table is reused for every block — turning the
// per-pixel bit-interleave (a loop + calls) into a single array lookup.
// Detiling a 2048x2048 texture is millions of elements; without this the
// per-pixel math makes DETILE unusably slow during asset streaming.
var hasExactXorPattern = TryGetExactXorPattern(swizzleMode, bppLog2, out var xorPattern);
var blockTable = hasExactXorPattern ? [] : new int[blockWidth * blockHeight];
for (var by = 0; !hasExactXorPattern && by < blockHeight; by++)
{
for (var bx = 0; bx < blockWidth; bx++)
{
blockTable[by * blockWidth + bx] = (int)(kind == SwizzleKind.ZOrder
? MortonInterleave((uint)bx, (uint)by, blockWidth, blockHeight)
: StandardSwizzleOffset((uint)bx, (uint)by, blockWidth, blockHeight));
}
}
for (var y = 0; y < elementsHigh; y++)
{
var blockY = y / blockHeight;
var inBlockY = y % blockHeight;
var rowBlockBase = (long)blockY * blocksPerRow;
var tableRowBase = inBlockY * blockWidth;
var destRowBase = (long)y * elementsWide * bytesPerElement;
for (var x = 0; x < elementsWide; x++)
{
var blockX = x / blockWidth;
var inBlockX = x % blockWidth;
var blockIndex = rowBlockBase + blockX;
var sourceByte = hasExactXorPattern
? blockIndex * blockBytes + ComputePatternOffset((uint)x, (uint)y, xorPattern)
: (blockIndex * blockElements + blockTable[tableRowBase + inBlockX]) *
(long)bytesPerElement;
var destByte = destRowBase + (long)x * bytesPerElement;
if (sourceByte + bytesPerElement > tiled.Length ||
destByte + bytesPerElement > linear.Length)
{
continue;
}
tiled.Slice((int)sourceByte, bytesPerElement)
.CopyTo(linear.Slice((int)destByte, bytesPerElement));
}
}
return true;
}
private enum SwizzleKind
{
Standard,
ZOrder,
}
private static readonly AddressBit Zero = new(0, 0);
private static AddressBit X(int bit) => new(1u << bit, 0);
private static AddressBit Y(int bit) => new(0, 1u << bit);
private static AddressBit XY(int xBit, int yBit) => new(1u << xBit, 1u << yBit);
private static AddressBit XY(int xBit, int yBit1, int yBit2) =>
new(1u << xBit, (1u << yBit1) | (1u << yBit2));
private static bool TryGetExactXorPattern(
uint swizzleMode,
int bytesPerElementLog2,
out AddressBit[] pattern)
{
pattern = [];
if ((uint)bytesPerElementLog2 >= RbPlus64KRenderX.Length)
{
return false;
}
pattern = swizzleMode switch
{
5 => Standard4K[bytesPerElementLog2],
9 => RbPlus64KStandard[bytesPerElementLog2],
24 => RbPlus64KDepthX[bytesPerElementLog2],
27 => RbPlus64KRenderX[bytesPerElementLog2],
_ => [],
};
return pattern.Length != 0;
}
private static long ComputePatternOffset(uint x, uint y, AddressBit[] pattern)
{
uint offset = 0;
for (var bit = 0; bit < pattern.Length; bit++)
{
var equation = pattern[bit];
var parity = (System.Numerics.BitOperations.PopCount(x & equation.XMask) +
System.Numerics.BitOperations.PopCount(y & equation.YMask)) & 1;
offset |= (uint)parity << bit;
}
return offset;
}
private static bool TryGetSwizzleKind(uint swizzleMode, out SwizzleKind kind, out int blockBytes)
{
// GFX10 AddrLib SWIZZLE_MODE enumeration:
// 1-3 = 256 B S/D/R
// 4-7 = 4 KiB Z/S/D/R
// 8-11 = 64 KiB Z/S/D/R
// 16-19 = 64 KiB Z/S/D/R _T
// 20-23 = 4 KiB Z/S/D/R _X
// 24-27 = 64 KiB Z/S/D/R _X
// The pipe/bank XOR (_T/_X) affects which block a given tile lands in,
// but the *within-block* element order matches the base S/Z equation,
// which is what dominates visible correctness. We model the block
// interior and treat blocks as linear-ordered.
kind = SwizzleKind.Standard;
blockBytes = 0;
switch (swizzleMode)
{
case 4: case 8: case 16: case 20: case 24:
kind = SwizzleKind.ZOrder;
break;
case 1: case 2: case 3:
case 5: case 6: case 7:
case 9: case 10: case 11:
case 17: case 18: case 19:
case 21: case 22: case 23:
case 25: case 26: case 27:
kind = SwizzleKind.Standard;
break;
default:
return false;
}
blockBytes = swizzleMode switch
{
>= 1 and <= 3 => 256,
>= 4 and <= 7 => 4096,
>= 20 and <= 23 => 4096,
_ => 65536,
};
return true;
}
// Standard (S) 2D swizzle: within a block, the element order interleaves x
// and y bits with x taking the low bit — the AMD "standard" microtile.
private static long StandardSwizzleOffset(uint x, uint y, int blockWidth, int blockHeight)
{
var widthBits = BitLog2((uint)blockWidth);
var heightBits = BitLog2((uint)blockHeight);
long offset = 0;
var outBit = 0;
var xi = 0;
var yi = 0;
while (xi < widthBits || yi < heightBits)
{
if (xi < widthBits)
{
offset |= (long)((x >> xi) & 1u) << outBit++;
xi++;
}
if (yi < heightBits)
{
offset |= (long)((y >> yi) & 1u) << outBit++;
yi++;
}
}
return offset;
}
// Z-order (Morton) swizzle: pure bit interleave, y taking the low bit.
private static long MortonInterleave(uint x, uint y, int blockWidth, int blockHeight)
{
var widthBits = BitLog2((uint)blockWidth);
var heightBits = BitLog2((uint)blockHeight);
long offset = 0;
var outBit = 0;
var xi = 0;
var yi = 0;
while (xi < widthBits || yi < heightBits)
{
if (yi < heightBits)
{
offset |= (long)((y >> yi) & 1u) << outBit++;
yi++;
}
if (xi < widthBits)
{
offset |= (long)((x >> xi) & 1u) << outBit++;
xi++;
}
}
return offset;
}
private static (int Width, int Height) SquareBlockDimensions(int blockElements)
{
if (blockElements <= 0 || (blockElements & (blockElements - 1)) != 0)
{
return (0, 0);
}
var totalBits = BitLog2((uint)blockElements);
// Split as evenly as possible with width >= height (x gets the extra bit).
var widthBits = (totalBits + 1) / 2;
var heightBits = totalBits - widthBits;
return (1 << widthBits, 1 << heightBits);
}
private static int BitLog2(uint value)
{
if (value == 0 || (value & (value - 1)) != 0)
{
return -1;
}
return System.Numerics.BitOperations.TrailingZeroCount(value);
}
private static void ReportUnsupported(uint swizzleMode)
{
lock (_reportedModes)
{
if (!_reportedModes.Add(swizzleMode))
{
return;
}
}
Console.Error.WriteLine(
$"[LOADER][WARN] GNM detile: unsupported swizzle mode {swizzleMode}; texture uploaded linear.");
}
}
+179 -16
View File
@@ -3,10 +3,17 @@
namespace SharpEmu.Libs.Agc;
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
// AgcExports re-checks every waiter against guest memory on each submit and resumes
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
// DmaData packets or by direct CPU writes).
/// <summary>
/// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM
/// condition. AgcExports re-checks every waiter against guest memory on each
/// submit and resumes the ones whose condition became true (labels are advanced
/// by ReleaseMem / WriteData / DmaData packets, or by direct CPU writes).
///
/// This preserves cross-submit ordering: the work that follows a wait inside a
/// DCB is only queued once the awaited completion label is genuinely written,
/// instead of being force-satisfied at parse time and running ahead of the
/// compute/graphics work it depends on (which produced a black composite).
/// </summary>
internal static class GpuWaitRegistry
{
public struct WaitingDcb
@@ -19,13 +26,56 @@ internal static class GpuWaitRegistry
public ulong ReferenceValue;
public ulong Mask;
public uint CompareFunction;
public uint ControlValue;
public bool Is64Bit;
public bool IsStandard;
public object? Memory;
public string? QueueName;
public ulong SubmissionId;
// Stopwatch timestamp captured at registration. Stale waiters remain
// registered; this only controls one-shot diagnostics.
public long RegisteredTicks;
public bool StaleReported;
public object? State;
}
private static readonly object _gate = new();
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
public static int Count
{
get
{
lock (_gate)
{
var total = 0;
foreach (var (_, list) in _waiters)
{
total += list.Count;
}
return total;
}
}
}
public static int CountForMemory(object memory)
{
lock (_gate)
{
var total = 0;
foreach (var (_, list) in _waiters)
{
foreach (var waiter in list)
{
total += ReferenceEquals(waiter.Memory, memory) ? 1 : 0;
}
}
return total;
}
}
public static void Register(ulong address, WaitingDcb waiter)
{
waiter.WaitAddress = address;
@@ -41,9 +91,15 @@ internal static class GpuWaitRegistry
}
}
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
// returns null when the memory is unreadable; such waiters are kept registered.
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
/// <summary>
/// Re-evaluates every registered waiter. <paramref name="readValue"/>
/// receives (address, is64Bit) and returns null when the memory is
/// unreadable; such waiters are kept registered. Returns the waiters whose
/// condition is now satisfied (removed from the registry), or null.
/// </summary>
public static List<WaitingDcb>? CollectSatisfied(
object memory,
Func<ulong, bool, ulong?> readValue)
{
List<WaitingDcb>? woken = null;
lock (_gate)
@@ -53,6 +109,11 @@ internal static class GpuWaitRegistry
{
for (var i = list.Count - 1; i >= 0; i--)
{
if (!ReferenceEquals(list[i].Memory, memory))
{
continue;
}
var value = readValue(address, list[i].Is64Bit);
if (value is null || !Compare(list[i], value.Value))
{
@@ -71,7 +132,7 @@ internal static class GpuWaitRegistry
}
}
if (emptied != null)
if (emptied is not null)
{
foreach (var address in emptied)
{
@@ -83,20 +144,122 @@ internal static class GpuWaitRegistry
return woken;
}
/// <summary>
/// Returns waiters that have remained unsatisfied longer than
/// <paramref name="maxAgeTicks"/> exactly once, without removing them or
/// changing their labels. Missing GPU work must fail closed: advancing a
/// command buffer without its real producer corrupts cross-queue ordering.
/// </summary>
public static List<WaitingDcb>? CollectUnreportedStale(
object memory,
long nowTicks,
long maxAgeTicks)
{
List<WaitingDcb>? stale = null;
lock (_gate)
{
foreach (var (_, list) in _waiters)
{
for (var i = list.Count - 1; i >= 0; i--)
{
var waiter = list[i];
if (!ReferenceEquals(waiter.Memory, memory) ||
waiter.StaleReported ||
nowTicks - waiter.RegisteredTicks < maxAgeTicks)
{
continue;
}
stale ??= new List<WaitingDcb>();
waiter.StaleReported = true;
list[i] = waiter;
stale.Add(waiter);
}
}
}
return stale;
}
/// <summary>
/// Returns watched labels overlapped by a newly discovered producer. Used
/// only for diagnostics; producer completion still wakes through the
/// normal CollectSatisfied path after the ordered memory write executes.
/// </summary>
public static List<(ulong Address, int Count)> SnapshotInRange(
object memory,
ulong start,
ulong length)
{
var matches = new List<(ulong Address, int Count)>();
if (length == 0)
{
return matches;
}
var end = start > ulong.MaxValue - length ? ulong.MaxValue : start + length;
lock (_gate)
{
foreach (var (address, list) in _waiters)
{
var matchingCount = 0;
var any64Bit = false;
foreach (var waiter in list)
{
if (!ReferenceEquals(waiter.Memory, memory))
{
continue;
}
matchingCount++;
any64Bit |= waiter.Is64Bit;
}
if (matchingCount == 0)
{
continue;
}
var width = any64Bit
? sizeof(ulong)
: sizeof(uint);
var waitEnd = address > ulong.MaxValue - (ulong)width
? ulong.MaxValue
: address + (ulong)width;
if (start < waitEnd && address < end)
{
matches.Add((address, matchingCount));
}
}
}
return matches;
}
public static bool Compare(in WaitingDcb waiter, ulong value)
{
var masked = value & waiter.Mask;
var reference = waiter.ReferenceValue & waiter.Mask;
return waiter.CompareFunction switch
{
1 => masked < waiter.ReferenceValue,
2 => masked <= waiter.ReferenceValue,
3 => masked == waiter.ReferenceValue,
4 => masked != waiter.ReferenceValue,
5 => masked >= waiter.ReferenceValue,
6 => masked > waiter.ReferenceValue,
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
// satisfied keeps a malformed packet from suspending its DCB forever.
0 => true,
1 => masked < reference,
2 => masked <= reference,
3 => masked == reference,
4 => masked != reference,
5 => masked >= reference,
6 => masked > reference,
// 7 is reserved; treating it as satisfied keeps a malformed packet
// from suspending forever.
_ => true,
};
}
public static void Clear()
{
lock (_gate)
{
_waiters.Clear();
}
}
}
+2 -22
View File
@@ -7,31 +7,11 @@ namespace SharpEmu.Libs.Ajm;
public static class AjmExports
{
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
private static int _nextContextId;
[SysAbiExport(
Nid = "dl+4eHSzUu4",
ExportName = "sceAjmInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int Initialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outContextAddress = ctx[CpuRegister.Rsi];
// AJM requires a zero reserved argument.
if (reserved != 0 || outContextAddress == 0)
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
if (!ctx.TryWriteUInt32(outContextAddress, contextId))
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
return ctx.SetReturn(0);
}
public static int Initialize(CpuContext ctx) =>
SharpEmu.Libs.Audio.AjmExports.AjmInitialize(ctx);
}
+51 -39
View File
@@ -36,6 +36,7 @@ public static class AmprExports
public ulong Buffer;
public ulong Size;
public ulong WriteOffset;
public ulong CommandCount;
}
private sealed class CachedHostFile
@@ -267,53 +268,19 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong bytesRead = 0;
// Unregistered/missing files are zero-filled instead of failing: games queue
// speculative reads and only consume the bytes on success paths.
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
{
if (destination != 0 && size > 0)
{
int chunkSize = (int)Math.Min(size, 4096);
Span<byte> zeros = stackalloc byte[chunkSize];
zeros.Clear();
while (bytesRead < size)
{
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
{
break;
}
bytesRead += (ulong)currentChunk;
}
}
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
// Offset -1 means "continue after the previous read of this file id".
if (fileOffset == unchecked((ulong)(long)-1))
{
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
}
else if (fileOffset > long.MaxValue)
{
fileOffset = 0;
}
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
return result;
}
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -418,6 +385,35 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "gzndltBEzWc",
ExportName = "sceAmprCommandBufferGetNumCommands",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferGetNumCommands(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong commandCount;
lock (state)
{
commandCount = state.CommandCount;
}
TraceAmpr(ctx, "get_num_commands", commandBuffer, commandCount, 0);
ctx[CpuRegister.Rax] = commandCount;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "H896Pt-yB4I",
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
@@ -533,7 +529,7 @@ public static class AmprExports
var offset = 0UL;
while (offset < writeOffset)
{
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -649,6 +645,7 @@ public static class AmprExports
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = writeOffset;
state.CommandCount = 0;
}
}
@@ -681,6 +678,7 @@ public static class AmprExports
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = 0;
state.CommandCount = 0;
}
return true;
@@ -911,6 +909,7 @@ public static class AmprExports
}
state.WriteOffset += recordSize;
state.CommandCount++;
}
return true;
@@ -963,6 +962,19 @@ public static class AmprExports
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
{
if (!_traceAmpr)
+104
View File
@@ -0,0 +1,104 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Audio;
public static class AjmExports
{
private static readonly ConcurrentDictionary<uint, byte> Contexts = new();
private static int _nextContextId;
public static int AjmInitialize(CpuContext ctx)
{
var reserved = ctx[CpuRegister.Rdi];
var outputAddress = ctx[CpuRegister.Rsi];
if (reserved != 0 || outputAddress == 0)
{
return unchecked((int)0x806A0001);
}
var contextId = unchecked((uint)Interlocked.Increment(ref _nextContextId));
Span<byte> value = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(value, contextId);
if (!ctx.Memory.TryWrite(outputAddress, value))
{
return unchecked((int)0x806A0001);
}
Contexts[contextId] = 0;
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] ajm.initialize reserved={reserved} out=0x{outputAddress:X16} context={contextId}");
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "MHur6qCsUus",
ExportName = "sceAjmFinalize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmFinalize(CpuContext ctx)
{
Contexts.TryRemove(unchecked((uint)ctx[CpuRegister.Rdi]), out _);
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "Q3dyFuwGn64",
ExportName = "sceAjmModuleRegister",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmModuleRegister(CpuContext ctx)
{
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
var reserved = ctx[CpuRegister.Rdx];
if (reserved != 0 || !Contexts.ContainsKey(contextId))
{
return unchecked((int)0x806A0001);
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] ajm.module_register context={contextId} codec={codecType} reserved={reserved}");
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "Wi7DtlLV+KI",
ExportName = "sceAjmModuleUnregister",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmModuleUnregister(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "MmpF1XsQiHw",
ExportName = "sceAjmBatchInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmBatchInitialize(CpuContext ctx)
{
// The caller owns and initializes the batch storage. This API resets
// its submission cursor on hardware; FMOD does not consume a return
// value or an additional output object here.
ctx[CpuRegister.Rax] = 0;
return 0;
}
}
+214 -39
View File
@@ -3,22 +3,70 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
namespace SharpEmu.Libs.Audio;
public static class AudioOut2Exports
{
// Sized from guest evidence, not SDK headers: Quake keeps its
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
// and an earlier 0x80-byte ResetParam write zeroed that canary
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
// the whole audio init). Stay well below 0x60 and only write the prefix we
// populate.
private const int AudioOut2ContextParamSize = 0x30;
// FMOD's PS5 backend allocates this ABI structure as four 16-byte lanes.
// Clearing 0x80 bytes here overwrote the caller's stack canary immediately
// following the 0x40-byte parameter block.
private const int AudioOut2ContextParamSize = 0x40;
private const int AudioOut2ContextMemorySize = 0x10000;
private const int AudioOut2ContextMemoryAlignment = 0x10000;
private static long _nextContextHandle = 1;
private static long _nextUserHandle = 1;
private static int _nextPortId;
private static long _pushTraceCount;
// Per-context audio parameters captured at ContextCreate so ContextAdvance
// can pace to the real playback cadence (grain samples at the sample rate).
private static readonly ConcurrentDictionary<ulong, ContextState> Contexts = new();
private sealed class ContextState
{
private readonly object _paceGate = new();
private long _nextAdvanceTimestamp;
public ContextState(uint frequency, uint channels, uint grainSamples)
{
Frequency = frequency == 0 ? 48000 : frequency;
Channels = channels == 0 ? 2 : channels;
GrainSamples = grainSamples == 0 ? 256 : grainSamples;
}
public uint Frequency { get; }
public uint Channels { get; }
public uint GrainSamples { get; }
// Blocks the advancing thread until one grain worth of wall-clock time
// has elapsed since the previous advance, matching hardware timing so
// audio-gated titles neither spin nor drift ahead.
public void PaceAdvance()
{
long delay;
lock (_paceGate)
{
var now = Stopwatch.GetTimestamp();
if (_nextAdvanceTimestamp < now)
{
_nextAdvanceTimestamp = now;
}
delay = _nextAdvanceTimestamp - now;
_nextAdvanceTimestamp += checked(
(long)Math.Ceiling(Stopwatch.Frequency * (double)GrainSamples / Frequency));
}
if (delay > 0)
{
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
}
}
}
[SysAbiExport(
Nid = "g2tViFIohHE",
@@ -41,7 +89,7 @@ public static class AudioOut2Exports
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
@@ -52,8 +100,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
return ctx.Memory.TryWrite(paramAddress, param)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -64,15 +112,22 @@ public static class AudioOut2Exports
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || outMemorySizeAddress == 0)
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
Span<byte> memoryInfo = stackalloc byte[0x20];
memoryInfo.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -88,13 +143,34 @@ public static class AudioOut2Exports
var outContextAddress = ctx[CpuRegister.Rcx];
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// Read channels/frequency/grain from the reset-param blob so the
// context can pace advances to the real audio cadence.
uint channels = 2;
uint frequency = 48000;
uint grain = 256;
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
if (ctx.Memory.TryRead(paramAddress, param))
{
var pc = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
var pf = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
var pg = BinaryPrimitives.ReadUInt32LittleEndian(param[0x0C..]);
if (pc is > 0 and <= 8) channels = pc;
if (pf is >= 8000 and <= 192000) frequency = pf;
// Values below one cache line are flags/counts in observed PS5
// callers, not audio grains. Keep the hardware-sized default.
if (pg is >= 64 and <= 0x4000) grain = pg;
TraceAudioOut2($"context-param address=0x{paramAddress:X} bytes={Convert.ToHexString(param)}");
}
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
return ctx.TryWriteUInt64(outContextAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
Contexts[handle] = new ContextState(frequency, channels, grain);
TraceAudioOut2($"context-create handle=0x{handle:X} frequency={frequency} channels={channels} grain={grain} memory=0x{memoryAddress:X} size=0x{memorySize:X}");
return TryWriteUInt64(ctx, outContextAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -102,7 +178,77 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2ContextDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2ContextDestroy(CpuContext ctx)
{
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "DxGyV8dtOR8",
ExportName = "sceAudioOut2ContextBedWrite",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "aII9h5nli9U",
ExportName = "sceAudioOut2ContextPush",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextPush(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var traceCount = Interlocked.Increment(ref _pushTraceCount);
if (traceCount <= 16)
{
TraceAudioOut2($"context-push count={traceCount} rdi=0x{handle:X} rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{ctx[CpuRegister.Rcx]:X}");
}
if (Contexts.TryGetValue(handle, out var context))
{
// FMOD's PS5 output path uses ContextPush as the submission clock
// and does not call ContextAdvance. Pace pushes to one hardware
// grain so the feeder cannot outrun playback and starve the game.
context.PaceAdvance();
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "PE2zHMqLSHs",
ExportName = "sceAudioOut2ContextAdvance",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextAdvance(CpuContext ctx)
{
// Advancing renders one grain of audio on hardware; pace it to the same
// wall-clock cadence so the guest audio thread runs at the right speed.
if (Contexts.TryGetValue(ctx[CpuRegister.Rdi], out var context))
{
context.PaceAdvance();
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "R7d0F1g2qsU",
ExportName = "sceAudioOut2ContextGetQueueLevel",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextGetQueueLevel(CpuContext ctx)
{
// The advance path paces synchronously, so the queue is always drained.
var levelAddress = ctx[CpuRegister.Rsi];
if (levelAddress != 0)
{
_ = TryWriteUInt64(ctx, levelAddress, 0);
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "JK2wamZPzwM",
@@ -117,16 +263,23 @@ public static class AudioOut2Exports
var contextAddress = ctx[CpuRegister.Rcx];
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return ctx.TryWriteUInt64(outPortAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return TryWriteUInt64(ctx, outPortAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "8XTArSPyWHk",
ExportName = "sceAudioOut2PortSetAttributes",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortSetAttributes(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "gatEUKG+Ea4",
ExportName = "sceAudioOut2PortGetState",
@@ -138,7 +291,7 @@ public static class AudioOut2Exports
var stateAddress = ctx[CpuRegister.Rsi];
if (handle == 0 || stateAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var type = (int)((handle >> 16) & 0xFF);
@@ -151,8 +304,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
return ctx.Memory.TryWrite(stateAddress, state)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -165,7 +318,7 @@ public static class AudioOut2Exports
var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> info = stackalloc byte[0x40];
@@ -175,8 +328,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
return ctx.Memory.TryWrite(infoAddress, info)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -184,14 +337,14 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "IaZXJ9M79uo",
ExportName = "sceAudioOut2UserDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "xywYcRB7nbQ",
@@ -202,14 +355,36 @@ public static class AudioOut2Exports
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 255) || outUserAddress == 0)
if ((userId != 0 && userId != 1 && userId != 1000 && userId != 0x10000000 && userId != 255) ||
outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return ctx.TryWriteUInt64(outUserAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return TryWriteUInt64(ctx, outUserAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceAudioOut2(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT2"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] audio_out2.{message}");
}
}
}
+40 -5
View File
@@ -50,6 +50,7 @@ public static class AudioOutExports
public int BytesPerSample { get; }
public bool IsFloat { get; }
public IHostAudioStream? Backend { get; }
public volatile float Volume = 1.0f;
public int BufferByteLength =>
checked((int)BufferLength * Channels * BytesPerSample);
@@ -198,7 +199,8 @@ public static class AudioOutExports
checked((int)port.BufferLength),
port.Channels,
port.BytesPerSample,
port.IsFloat);
port.IsFloat,
port.Volume);
if (!port.Backend.Submit(output.AsSpan(0, outputLength)))
{
port.PaceSilence();
@@ -225,10 +227,43 @@ public static class AudioOutExports
public static int AudioOutSetVolume(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return ctx.SetReturn(
Ports.ContainsKey(handle)
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
var channelFlags = unchecked((uint)ctx[CpuRegister.Rsi]);
var volumeArrayAddress = ctx[CpuRegister.Rdx];
if (!Ports.TryGetValue(handle, out var port))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
const int unityVolume = 32768;
var maxVolume = 0;
var found = false;
if (volumeArrayAddress != 0)
{
Span<byte> raw = stackalloc byte[sizeof(int)];
for (var channel = 0; channel < 8; channel++)
{
if ((channelFlags & (1u << channel)) == 0)
{
continue;
}
if (!ctx.Memory.TryRead(volumeArrayAddress + (ulong)(channel * sizeof(int)), raw))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var value = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(raw);
maxVolume = Math.Max(maxVolume, value);
found = true;
}
}
if (found)
{
port.Volume = Math.Clamp(maxVolume / (float)unityVolume, 0f, 1f);
}
return ctx.SetReturn(0);
}
public static void ShutdownAllPorts()
+23 -3
View File
@@ -21,7 +21,8 @@ internal static class AudioPcmConversion
int frames,
int channels,
int bytesPerSample,
bool isFloat)
bool isFloat,
float volume)
{
var sourceFrameSize = checked(channels * bytesPerSample);
for (var frame = 0; frame < frames; frame++)
@@ -31,6 +32,8 @@ internal static class AudioPcmConversion
var right = channels == 1
? left
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
left = ApplyVolume(left, volume);
right = ApplyVolume(right, volume);
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * OutputFrameSize)..], left);
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * OutputFrameSize) + 2)..], right);
}
@@ -49,7 +52,24 @@ internal static class AudioPcmConversion
}
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
return checked((short)MathF.Round(value * short.MaxValue));
return ConvertFloatSample(BitConverter.Int32BitsToSingle(bits));
}
private static short ConvertFloatSample(float value)
{
if (float.IsNaN(value))
{
return 0;
}
value = Math.Clamp(value, -1.0f, 1.0f);
var scale = value < 0.0f ? 32768.0f : short.MaxValue;
return checked((short)MathF.Round(value * scale));
}
private static short ApplyVolume(short sample, float volume)
{
var scaled = MathF.Round(sample * Math.Clamp(volume, 0.0f, 1.0f));
return (short)Math.Clamp(scaled, short.MinValue, short.MaxValue);
}
}
@@ -0,0 +1,47 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Audio;
public static class FmodCompatExports
{
private static readonly ConcurrentDictionary<ulong, int> SetOutputCalls = new();
[SysAbiExport(
Nid = "uPLTdl3psGk",
Target = Generation.Gen5,
LibraryName = "libfmod")]
public static int FmodSystemSetOutput(CpuContext ctx)
{
var system = ctx[CpuRegister.Rdi];
var output = unchecked((int)ctx[CpuRegister.Rsi]);
var callCount = SetOutputCalls.AddOrUpdate(system, 1, static (_, count) => count + 1);
var resetPrematureInit = callCount <= 2;
if (resetPrematureInit && system != 0)
{
Span<byte> zeroByte = stackalloc byte[1];
Span<byte> outputBytes = stackalloc byte[sizeof(int)];
Span<byte> zeroInt = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(outputBytes, output);
_ = ctx.Memory.TryWrite(system + 0x08, zeroByte);
_ = ctx.Memory.TryWrite(system + 0x116D0, outputBytes);
_ = ctx.Memory.TryWrite(system + 0x116D4, zeroInt);
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FMOD"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fmod.system_set_output system=0x{system:X16} output={output} call={callCount} reset_premature_init={resetPrematureInit} -> 0");
}
// The PS5 FMOD object arrives with its initialized byte set before Unity has
// applied the startup output configuration. Preserve those settings while
// clearing the premature marker so Studio can execute the real core init.
ctx[CpuRegister.Rax] = 0;
return 0;
}
}
File diff suppressed because it is too large Load Diff
+403
View File
@@ -0,0 +1,403 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Bink;
/// <summary>
/// Optional host-side Bink 2 bridge for games that ship a static Bink player.
///
/// The game in that case never imports libSceVideodec, so an HLE video-decoder
/// export cannot see its movie frames. Kernel file opens identify the active
/// .bk2 file and the presenter requests BGRA frames from a tiny native adapter.
/// The adapter is deliberately a separate, user-supplied library: Bink 2 is a
/// proprietary SDK and SharpEmu must neither bundle it nor depend on its ABI.
/// </summary>
internal static class Bink2MovieBridge
{
private const uint MaxDimension = 16384;
private static readonly object Gate = new();
private static NativeAdapter? _adapter;
private static string? _activePath;
private static IntPtr _activeMovie;
private static Bink2MovieInfo _activeInfo;
private static byte[]? _frameBuffer;
private static bool _usingDummyMovie;
private static bool _loadAttempted;
private static bool _availabilityReported;
/// <summary>
/// Returns true when the guest should receive a normal "file not found"
/// result for a Bink movie. This is the safe default without a decoder:
/// games that treat movies as optional fall through to their next state
/// rather than submitting an empty Bink GPU texture forever.
/// </summary>
internal static bool ShouldSkipGuestMovie(string hostPath) =>
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
ResolveMode() == MovieMode.Skip;
internal static void ObserveGuestMovie(string hostPath)
{
if (!hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) ||
!File.Exists(hostPath))
{
return;
}
lock (Gate)
{
if (string.Equals(_activePath, hostPath, StringComparison.OrdinalIgnoreCase))
{
return;
}
if (ResolveMode() == MovieMode.Dummy)
{
AttachDummyMovieLocked(hostPath);
return;
}
var adapter = GetAdapterLocked();
if (adapter is null)
{
return;
}
CloseActiveLocked();
if (!adapter.TryOpen(hostPath, out var movie, out var info))
{
Console.Error.WriteLine(
"[LOADER][WARN] Bink2 bridge could not open movie '" +
Path.GetFileName(hostPath) + "'.");
return;
}
if (!IsValid(info))
{
adapter.Close(movie);
Console.Error.WriteLine(
"[LOADER][WARN] Bink2 bridge rejected invalid movie dimensions for '" +
Path.GetFileName(hostPath) + "'.");
return;
}
_activePath = hostPath;
_activeMovie = movie;
_activeInfo = info;
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
Console.Error.WriteLine(
"[LOADER][INFO] Bink2 bridge attached: " + Path.GetFileName(hostPath) + " " +
info.Width + "x" + info.Height + " @ " +
info.FramesPerSecondNumerator + "/" + info.FramesPerSecondDenominator + " fps.");
}
}
internal static bool TryDecodeNextFrame(
out byte[] pixels,
out uint width,
out uint height)
{
lock (Gate)
{
pixels = [];
width = 0;
height = 0;
if (_adapter is null || _activeMovie == IntPtr.Zero || _frameBuffer is null)
{
if (_usingDummyMovie && _frameBuffer is not null)
{
pixels = _frameBuffer;
width = _activeInfo.Width;
height = _activeInfo.Height;
return true;
}
return false;
}
unsafe
{
fixed (byte* destination = _frameBuffer)
{
if (!_adapter.DecodeNextBgra(
_activeMovie,
(IntPtr)destination,
_activeInfo.Width * 4,
(uint)_frameBuffer.Length))
{
return false;
}
}
}
pixels = _frameBuffer;
width = _activeInfo.Width;
height = _activeInfo.Height;
return true;
}
}
private static bool IsValid(Bink2MovieInfo info) =>
info.Width > 0 && info.Height > 0 &&
info.Width <= MaxDimension && info.Height <= MaxDimension &&
(ulong)info.Width * info.Height * 4 <= int.MaxValue;
private static int GetFrameBufferLength(Bink2MovieInfo info) =>
checked((int)((ulong)info.Width * info.Height * 4));
private static MovieMode ResolveMode()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK_MODE");
if (string.Equals(configured, "dummy", StringComparison.OrdinalIgnoreCase))
{
return MovieMode.Dummy;
}
if (string.Equals(configured, "native", StringComparison.OrdinalIgnoreCase))
{
return MovieMode.Native;
}
if (string.Equals(configured, "skip", StringComparison.OrdinalIgnoreCase))
{
return MovieMode.Skip;
}
// With no SDK adapter present, returning "not found" makes optional
// cinematics advance. Supplying either an explicit path or the normal
// side-by-side adapter enables native playback automatically.
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
EnumerateAdapterCandidates().Any(File.Exists))
{
return MovieMode.Native;
}
return MovieMode.Skip;
}
private static void AttachDummyMovieLocked(string hostPath)
{
if (!TryReadBinkInfo(hostPath, out var info) || !IsValid(info))
{
Console.Error.WriteLine(
"[LOADER][WARN] Bink dummy could not read movie header '" +
Path.GetFileName(hostPath) + "'.");
return;
}
CloseActiveLocked();
_activePath = hostPath;
_activeInfo = info;
_frameBuffer = GC.AllocateUninitializedArray<byte>(GetFrameBufferLength(info));
FillDummyFrame(_frameBuffer, info.Width, info.Height);
_usingDummyMovie = true;
Console.Error.WriteLine(
"[LOADER][INFO] Bink dummy attached: " + Path.GetFileName(hostPath) + " " +
info.Width + "x" + info.Height + ".");
}
private static bool TryReadBinkInfo(string path, out Bink2MovieInfo info)
{
info = default;
Span<byte> header = stackalloc byte[32];
try
{
using var stream = File.OpenRead(path);
if (stream.Read(header) != header.Length ||
!header[..4].SequenceEqual("KB2j"u8))
{
return false;
}
info = new Bink2MovieInfo(
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x14, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x18, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(0x1C, 4)),
1);
return true;
}
catch (IOException)
{
return false;
}
}
private static void FillDummyFrame(byte[] pixels, uint width, uint height)
{
for (var y = 0u; y < height; y++)
{
for (var x = 0u; x < width; x++)
{
var offset = checked((int)(((ulong)y * width + x) * 4));
var band = ((x / 96) + (y / 96)) & 1;
pixels[offset] = band == 0 ? (byte)0x28 : (byte)0x18;
pixels[offset + 1] = band == 0 ? (byte)0x18 : (byte)0x28;
pixels[offset + 2] = 0x10;
pixels[offset + 3] = 0xFF;
}
}
}
private static NativeAdapter? GetAdapterLocked()
{
if (_loadAttempted)
{
return _adapter;
}
_loadAttempted = true;
foreach (var candidate in EnumerateAdapterCandidates())
{
if (!NativeLibrary.TryLoad(candidate, out var library))
{
continue;
}
if (NativeAdapter.TryCreate(library, out var adapter))
{
_adapter = adapter;
Console.Error.WriteLine("[LOADER][INFO] Bink2 bridge loaded: " + candidate);
return adapter;
}
NativeLibrary.Free(library);
}
if (!_availabilityReported)
{
_availabilityReported = true;
Console.Error.WriteLine(
"[LOADER][INFO] Bink2 bridge unavailable; install the licensed adapter and set SHARPEMU_BINK2_BRIDGE.");
}
return null;
}
private static IEnumerable<string> EnumerateAdapterCandidates()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE");
if (!string.IsNullOrWhiteSpace(configured))
{
yield return configured;
}
var baseDirectory = AppContext.BaseDirectory;
if (OperatingSystem.IsMacOS())
{
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.dylib");
}
else if (OperatingSystem.IsWindows())
{
yield return Path.Combine(baseDirectory, "sharpemu_bink2_bridge.dll");
}
else
{
yield return Path.Combine(baseDirectory, "libsharpemu_bink2_bridge.so");
}
}
private static void CloseActiveLocked()
{
if (_activeMovie != IntPtr.Zero)
{
_adapter?.Close(_activeMovie);
}
_activePath = null;
_activeMovie = IntPtr.Zero;
_activeInfo = default;
_frameBuffer = null;
_usingDummyMovie = false;
}
[StructLayout(LayoutKind.Sequential)]
private readonly struct Bink2MovieInfo
{
public readonly uint Width;
public readonly uint Height;
public readonly uint FramesPerSecondNumerator;
public readonly uint FramesPerSecondDenominator;
internal Bink2MovieInfo(
uint width,
uint height,
uint framesPerSecondNumerator,
uint framesPerSecondDenominator)
{
Width = width;
Height = height;
FramesPerSecondNumerator = framesPerSecondNumerator;
FramesPerSecondDenominator = framesPerSecondDenominator;
}
}
private enum MovieMode
{
Skip,
Dummy,
Native,
}
private sealed class NativeAdapter
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int OpenUtf8Delegate(IntPtr pathUtf8, out IntPtr movie, out Bink2MovieInfo info);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate int DecodeNextBgraDelegate(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void CloseDelegate(IntPtr movie);
private readonly OpenUtf8Delegate _openUtf8;
private readonly DecodeNextBgraDelegate _decodeNextBgra;
private readonly CloseDelegate _close;
private NativeAdapter(
OpenUtf8Delegate openUtf8,
DecodeNextBgraDelegate decodeNextBgra,
CloseDelegate close)
{
_openUtf8 = openUtf8;
_decodeNextBgra = decodeNextBgra;
_close = close;
}
internal static bool TryCreate(IntPtr library, out NativeAdapter? adapter)
{
adapter = null;
if (!NativeLibrary.TryGetExport(library, "sharpemu_bink2_open_utf8", out var open) ||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_decode_next_bgra", out var decode) ||
!NativeLibrary.TryGetExport(library, "sharpemu_bink2_close", out var close))
{
return false;
}
adapter = new NativeAdapter(
Marshal.GetDelegateForFunctionPointer<OpenUtf8Delegate>(open),
Marshal.GetDelegateForFunctionPointer<DecodeNextBgraDelegate>(decode),
Marshal.GetDelegateForFunctionPointer<CloseDelegate>(close));
return true;
}
internal bool TryOpen(string path, out IntPtr movie, out Bink2MovieInfo info)
{
var utf8 = Marshal.StringToCoTaskMemUTF8(path);
try
{
return _openUtf8(utf8, out movie, out info) != 0 && movie != IntPtr.Zero;
}
finally
{
Marshal.FreeCoTaskMem(utf8);
}
}
internal bool DecodeNextBgra(IntPtr movie, IntPtr destination, uint stride, uint destinationBytes) =>
_decodeNextBgra(movie, destination, stride, destinationBytes) != 0;
internal void Close(IntPtr movie) => _close(movie);
}
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Codec;
/// <summary>
/// libSceVideodec / libSceAudiodec handle management. Actual H.264/HEVC and
/// AAC/AT9 decoding requires an external codec, which is out of scope; these
/// exports keep the decoder lifecycle resolvable (create/decode/flush/delete)
/// and report "no output produced" so guests advance instead of failing on
/// unresolved imports.
/// </summary>
public static class CodecExports
{
private const int Ok = 0;
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
private const int AudiodecErrorInvalidArg = unchecked((int)0x807F0002);
private static readonly ConcurrentDictionary<ulong, byte> VideoDecoders = new();
private static readonly ConcurrentDictionary<ulong, byte> AudioDecoders = new();
private static long _nextHandle = 1;
// ---- Video decoder ----
[SysAbiExport(Nid = "qkgRiwHyheU", ExportName = "sceVideodecCreateDecoder",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
public static int VideodecCreateDecoder(CpuContext ctx)
{
var outHandleAddress = ctx[CpuRegister.Rdx];
if (outHandleAddress == 0)
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
VideoDecoders[handle] = 1;
return TryWriteHandle(ctx, outHandleAddress, handle) ? SetReturn(ctx, Ok) : SetReturn(ctx, VideodecErrorInvalidArg);
}
[SysAbiExport(Nid = "q0W5GJMovMs", ExportName = "sceVideodecDecode",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
public static int VideodecDecode(CpuContext ctx)
{
// No decoder is present: report success with no picture produced.
return SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
}
[SysAbiExport(Nid = "jeigLlKdp5I", ExportName = "sceVideodecFlush",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
public static int VideodecFlush(CpuContext ctx) =>
SetReturn(ctx, VideoDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : VideodecErrorInvalidArg);
[SysAbiExport(Nid = "U0kpGF1cl90", ExportName = "sceVideodecDeleteDecoder",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceVideodec")]
public static int VideodecDeleteDecoder(CpuContext ctx)
{
VideoDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetReturn(ctx, Ok);
}
// ---- Audio decoder ----
[SysAbiExport(Nid = "O3f1sLMWRvs", ExportName = "sceAudiodecCreateDecoder",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
public static int AudiodecCreateDecoder(CpuContext ctx)
{
var handle = (ulong)Interlocked.Increment(ref _nextHandle);
AudioDecoders[handle] = 1;
// sceAudiodec returns the handle directly (>= 0) or a negative error.
ctx[CpuRegister.Rax] = handle;
return unchecked((int)handle);
}
[SysAbiExport(Nid = "KHXHMDLkILw", ExportName = "sceAudiodecDecode",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
public static int AudiodecDecode(CpuContext ctx)
{
// No decoder present: report success with zero output samples so the
// caller treats the frame as silent rather than erroring.
return SetReturn(ctx, AudioDecoders.ContainsKey(ctx[CpuRegister.Rdi]) ? Ok : AudiodecErrorInvalidArg);
}
[SysAbiExport(Nid = "Tp+ZEy69mLk", ExportName = "sceAudiodecDeleteDecoder",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceAudiodec")]
public static int AudiodecDeleteDecoder(CpuContext ctx)
{
AudioDecoders.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetReturn(ctx, Ok);
}
private static bool TryWriteHandle(CpuContext ctx, ulong address, ulong handle) =>
ctx.TryWriteUInt64(address, handle);
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -0,0 +1,106 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Threading;
namespace SharpEmu.Libs.CommonDialog;
/// <summary>
/// libSceErrorDialog. There is no host error dialog, so an opened dialog
/// immediately reports finished — this keeps guests that block on the dialog
/// status (a common fatal-error path) from spinning forever.
/// </summary>
public static class ErrorDialogExports
{
private const int AlreadyInitialized = unchecked((int)0x80ED0001);
private const int NotInitialized = unchecked((int)0x80ED0002);
private const int ArgNull = unchecked((int)0x80ED0005);
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusFinished = 3;
private static int _initialized;
private static int _status;
[SysAbiExport(
Nid = "I88KChlynSs",
ExportName = "sceErrorDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogInitialize(CpuContext ctx)
{
var result = Interlocked.Exchange(ref _initialized, 1) == 0 ? 0 : AlreadyInitialized;
if (result == 0)
{
Volatile.Write(ref _status, StatusInitialized);
}
return SetReturn(ctx, result);
}
[SysAbiExport(
Nid = "M2ZF-ClLhgY",
ExportName = "sceErrorDialogOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogOpen(CpuContext ctx)
{
if (ctx[CpuRegister.Rdi] == 0)
{
return SetReturn(ctx, ArgNull);
}
if (Volatile.Read(ref _initialized) == 0)
{
return SetReturn(ctx, NotInitialized);
}
Volatile.Write(ref _status, StatusFinished);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "t2FvHRXzgqk",
ExportName = "sceErrorDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "WWiGuh9XfgQ",
ExportName = "sceErrorDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "ekXHb1kDBl0",
ExportName = "sceErrorDialogClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogClose(CpuContext ctx)
{
Volatile.Write(ref _status, StatusFinished);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "9XAxK2PMwk8",
ExportName = "sceErrorDialogTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceErrorDialog")]
public static int ErrorDialogTerminate(CpuContext ctx)
{
Volatile.Write(ref _status, StatusNone);
Interlocked.Exchange(ref _initialized, 0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.ContentExport;
// No host media library exists to export captures into, so initialization
// reports success.
public static class ContentExportExports
{
[SysAbiExport(
Nid = "0GnN4QCgIfs",
ExportName = "sceContentExportInit2",
Target = Generation.Gen5,
LibraryName = "libSceContentExport")]
public static int ContentExportInit2(CpuContext ctx) => ctx.SetReturn(0);
}
@@ -48,19 +48,25 @@ public static class DiscMapExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
#pragma warning disable SHEM006
[SysAbiExport(
Nid = "fJgP+wqifno",
ExportName = "sceDiscMapUnknownFJgP",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
#pragma warning restore SHEM006
// Synthetic label for an uncatalogued NID (the Unknown* convention); the NID is authoritative.
#pragma warning disable SHEM006
[SysAbiExport(
Nid = "ioKMruft1ek",
ExportName = "sceDiscMapUnknownIoKM",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceDiscMap")]
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
#pragma warning restore SHEM006
private static int WriteMappingTriple(CpuContext ctx, string exportName)
{
+379 -182
View File
@@ -3,6 +3,7 @@
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using SharpEmu.HLE;
@@ -22,6 +23,10 @@ public static class FiberExports
private const uint FiberStateIdle = 2;
private const uint FiberStateTerminated = 3;
private const uint FiberFlagContextSizeCheck = 0x10;
private const uint FiberFlagSetFpuRegs = 0x100;
private const uint Firmware350BuildVersion = 0x03500000;
private const ushort InitialFpuControlWord = 0x037F;
private const uint InitialMxcsr = 0x9FC0;
private const int FiberErrorNull = unchecked((int)0x80590001);
private const int FiberErrorAlignment = unchecked((int)0x80590002);
@@ -45,13 +50,26 @@ public static class FiberExports
private static int _contextSizeCheck;
[ThreadStatic]
private static ulong _currentFiberAddress;
private static readonly object _fiberGate = new();
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
private static readonly ConcurrentDictionary<ulong, FiberThreadState> _threadStates = new();
static FiberExports()
{
RunFiberSelfChecks();
}
public static void ResetRuntimeState()
{
lock (_fiberGate)
{
_continuations.Clear();
_stackRanges.Clear();
_threadStates.Clear();
Volatile.Write(ref _contextSizeCheck, 0);
}
}
[SysAbiExport(
Nid = "hVYD7Ou2pCQ",
@@ -61,6 +79,7 @@ public static class FiberExports
public static int FiberInitialize(CpuContext ctx)
{
var optParam = ReadStackArg64(ctx, 0);
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 1));
return FiberInitializeCore(
ctx,
ctx[CpuRegister.Rdi],
@@ -70,7 +89,8 @@ public static class FiberExports
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
optParam,
flags: 0);
flags: 0,
buildVersion);
}
[SysAbiExport(
@@ -82,6 +102,7 @@ public static class FiberExports
{
var optParam = ReadStackArg64(ctx, 0);
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
var buildVersion = unchecked((uint)ReadStackArg64(ctx, 2));
return FiberInitializeCore(
ctx,
ctx[CpuRegister.Rdi],
@@ -91,7 +112,8 @@ public static class FiberExports
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
optParam,
flags);
flags,
buildVersion);
}
[SysAbiExport(
@@ -104,17 +126,17 @@ public static class FiberExports
var optParam = ctx[CpuRegister.Rdi];
if (optParam == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if ((optParam & 7) != 0)
{
return ctx.SetReturn(FiberErrorAlignment);
return SetReturn(ctx, FiberErrorAlignment);
}
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -127,24 +149,23 @@ public static class FiberExports
var fiber = ctx[CpuRegister.Rdi];
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (state != FiberStateIdle)
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
_continuations.TryRemove(fiber, out _);
_returnTargets.TryRemove(fiber, out _);
_stackRanges.TryRemove(fiber, out _);
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
return ctx.SetReturn(0);
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -229,74 +250,47 @@ public static class FiberExports
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
var returnArgument = ctx[CpuRegister.Rdi];
var argOnRunAddress = ctx[CpuRegister.Rsi];
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
{
return ctx.SetReturn(FiberErrorInvalid);
}
GuestCpuContinuation transferTarget;
ulong previousFiber;
lock (_fiberGate)
{
_continuations[fiberAddress] = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
argOnRunAddress);
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
var threadKey = GetThreadKey(ctx);
if (!_threadStates.TryGetValue(threadKey, out var threadState) ||
!TryWriteResumeArgument(ctx, threadState.RootContinuation, returnArgument))
{
_continuations.TryRemove(fiberAddress, out _);
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
previousFiber = returnTarget.PreviousFiber;
if (previousFiber != 0)
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
{
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
{
_continuations.TryRemove(fiberAddress, out _);
return ctx.SetReturn(FiberErrorState);
}
transferTarget = previousContinuation.Context with { Rax = 0 };
}
else
{
if (!returnTarget.ThreadContinuation.HasValue ||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
{
_continuations.TryRemove(fiberAddress, out _);
return ctx.SetReturn(FiberErrorState);
}
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
{
return ctx.SetReturn(FiberErrorInvalid);
}
transferTarget = threadState.RootContinuation.Context with { Rax = 0 };
_threadStates.TryRemove(threadKey, out _);
}
_currentFiberAddress = previousFiber;
_ = GuestThreadExecution.EnterFiber(previousFiber);
_ = GuestThreadExecution.EnterFiber(0);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
$"return-to-thread fiber=0x{fiberAddress:X16} " +
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -309,18 +303,18 @@ public static class FiberExports
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
return ctx.TryWriteUInt64(outAddress, fiberAddress)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt64(ctx, outAddress, fiberAddress)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -334,35 +328,35 @@ public static class FiberExports
var info = ctx[CpuRegister.Rsi];
if (info == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
!TryWriteName(ctx, info + 40, fields.Name) ||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -376,22 +370,22 @@ public static class FiberExports
var nameAddress = ctx[CpuRegister.Rsi];
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (nameAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return TryWriteName(ctx, fiber + FiberNameOffset, name)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -403,12 +397,12 @@ public static class FiberExports
{
if (ctx[CpuRegister.Rdi] != 0)
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorState);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorState);
}
[SysAbiExport(
@@ -419,8 +413,8 @@ public static class FiberExports
public static int FiberStopContextSizeCheck(CpuContext ctx)
{
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorState);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorState);
}
[SysAbiExport(
@@ -433,17 +427,18 @@ public static class FiberExports
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (ResolveCurrentFiberAddress(ctx) == 0)
if (ResolveCurrentFiberAddress(ctx) == 0 ||
!_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt64(ctx, outAddress, threadState.RootContinuation.Context.Rbp)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
private static int FiberInitializeCore(
@@ -455,69 +450,70 @@ public static class FiberExports
ulong contextAddress,
ulong contextSize,
ulong optParam,
uint flags)
uint flags,
uint buildVersion)
{
if (fiber == 0 || nameAddress == 0 || entry == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if ((fiber & 7) != 0 ||
(contextAddress & 15) != 0 ||
(optParam & 7) != 0)
{
return ctx.SetReturn(FiberErrorAlignment);
return SetReturn(ctx, FiberErrorAlignment);
}
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
{
return ctx.SetReturn(FiberErrorRange);
return SetReturn(ctx, FiberErrorRange);
}
if ((contextSize & 15) != 0 ||
(contextAddress == 0 && contextSize != 0) ||
(contextAddress != 0 && contextSize == 0))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (optParam != 0 &&
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (Volatile.Read(ref _contextSizeCheck) != 0)
{
flags |= FiberFlagContextSizeCheck;
}
flags = ApplyInitializationFlags(
flags,
buildVersion,
Volatile.Read(ref _contextSizeCheck) != 0);
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (contextAddress != 0)
{
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if ((flags & FiberFlagContextSizeCheck) != 0)
@@ -531,8 +527,10 @@ public static class FiberExports
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
}
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
return ctx.SetReturn(0);
TraceFiber(
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
return SetReturn(ctx, 0);
}
private static int FiberRunCore(
@@ -547,12 +545,12 @@ public static class FiberExports
{
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (attachContextAddress != 0 || attachContextSize != 0)
@@ -560,7 +558,7 @@ public static class FiberExports
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
if (attachResult != 0)
{
return ctx.SetReturn(attachResult);
return SetReturn(ctx, attachResult);
}
}
@@ -568,78 +566,96 @@ public static class FiberExports
if ((isSwitch && previousFiber == 0) ||
(!isSwitch && previousFiber != 0))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
if (previousFiber == fiber)
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
GuestCpuContinuation transferTarget;
var resumed = false;
lock (_fiberGate)
{
var threadKey = GetThreadKey(ctx);
if (!TryReadFiberFields(ctx, fiber, out fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (fields.State != FiberStateIdle)
{
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
FiberContinuation targetContinuation;
FiberContinuation targetContinuation = default;
if (_continuations.TryGetValue(fiber, out var savedContinuation))
{
targetContinuation = savedContinuation;
resumed = true;
}
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
{
return ctx.SetReturn(FiberErrorInvalid);
}
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
{
return ctx.SetReturn(FiberErrorInvalid);
}
var callerContinuation = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
outArgumentAddress);
if (!resumed)
{
var rootStackTop = _threadStates.TryGetValue(threadKey, out var existingThreadState)
? existingThreadState.RootContinuation.Context.Rsp
: callerContinuation.Context.Rsp;
if (!TryCreateInitialContinuation(
ctx,
fields,
argOnRun,
rootStackTop,
out targetContinuation))
{
return SetReturn(ctx, FiberErrorInvalid);
}
}
else if (!TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (previousFiber != 0)
{
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
previousState != FiberStateRun ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
_continuations[previousFiber] = callerContinuation;
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
}
else
{
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
if (_threadStates.ContainsKey(threadKey))
{
return SetReturn(ctx, FiberErrorPermission);
}
_threadStates[threadKey] = new FiberThreadState(callerContinuation, fiber, previousFiber);
}
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
{
if (previousFiber != 0)
{
_continuations.TryRemove(previousFiber, out _);
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
}
_returnTargets.TryRemove(fiber, out _);
return ctx.SetReturn(FiberErrorInvalid);
else
{
_threadStates.TryRemove(threadKey, out _);
}
return SetReturn(ctx, FiberErrorInvalid);
}
if (resumed)
@@ -648,35 +664,66 @@ public static class FiberExports
}
transferTarget = targetContinuation.Context with { Rax = 0 };
if (_threadStates.TryGetValue(threadKey, out var activeState))
{
_threadStates[threadKey] = activeState with
{
CurrentFiber = fiber,
PreviousFiber = previousFiber,
};
}
}
_currentFiberAddress = fiber;
_ = GuestThreadExecution.EnterFiber(fiber);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
private static bool TryCreateInitialContinuation(
CpuContext ctx,
FiberFields fields,
ulong argOnRun,
ulong rootStackTop,
out FiberContinuation continuation)
{
continuation = default;
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
ulong stackEnd;
if (fields.ContextAddress == 0)
{
if (rootStackTop < 32)
{
return false;
}
// Contextless fibers are specified to execute on the root
// sceFiberRun stack. Keep the suspended import return record
// above the entry stack and use a synthetic transfer slot below it.
stackEnd = rootStackTop & ~15UL;
}
else
{
if (fields.ContextSize < FiberContextMinimumSize)
{
return false;
}
stackEnd = fields.ContextAddress + fields.ContextSize;
}
if (!TryCalculateInitialStackLayout(stackEnd, out var entryRsp, out var transferSlot))
{
return false;
}
if (!TryWriteUInt64(ctx, transferSlot, fields.Entry) ||
!TryWriteUInt64(ctx, entryRsp, 0))
{
return false;
}
var stackEnd = fields.ContextAddress + fields.ContextSize;
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
if (!ctx.TryWriteUInt64(entryRsp, 0))
{
return false;
}
var setFpuRegisters = (fields.Flags & FiberFlagSetFpuRegs) != 0;
continuation = new FiberContinuation(
new GuestCpuContinuation(
@@ -698,7 +745,12 @@ public static class FiberExports
0,
0,
0,
0),
0,
0,
0,
setFpuRegisters ? InitialFpuControlWord : ctx.FpuControlWord,
setFpuRegisters ? InitialMxcsr : ctx.Mxcsr,
RestoreFullFpuState: setFpuRegisters),
0);
return true;
}
@@ -708,7 +760,7 @@ public static class FiberExports
FiberContinuation continuation,
ulong argument) =>
continuation.ArgOnRunAddress == 0 ||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
private static GuestCpuContinuation CaptureContinuation(
CpuContext ctx,
@@ -731,26 +783,99 @@ public static class FiberExports
ctx[CpuRegister.Rdi],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
ctx[CpuRegister.R10],
ctx[CpuRegister.R11],
ctx[CpuRegister.R12],
ctx[CpuRegister.R13],
ctx[CpuRegister.R14],
ctx[CpuRegister.R15]);
ctx[CpuRegister.R15],
ctx.FpuControlWord,
ctx.Mxcsr,
RestoreFullFpuState: false);
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
{
if (_currentFiberAddress != 0)
{
return _currentFiberAddress;
}
if (GuestThreadExecution.CurrentFiberAddress != 0)
{
return GuestThreadExecution.CurrentFiberAddress;
}
if (_threadStates.TryGetValue(GetThreadKey(ctx), out var threadState) &&
threadState.CurrentFiber != 0)
{
return threadState.CurrentFiber;
}
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
}
private static ulong GetThreadKey(CpuContext ctx)
{
var handle = GuestThreadExecution.CurrentGuestThreadHandle;
if (handle != 0)
{
return handle;
}
// The main guest thread does not always have a scheduler handle. Its
// ABI thread pointer is stable across native/managed transitions.
return ctx.FsBase != 0 ? ctx.FsBase : 1;
}
private static uint ApplyInitializationFlags(
uint flags,
uint buildVersion,
bool contextSizeCheck)
{
if (buildVersion >= Firmware350BuildVersion)
{
flags |= FiberFlagSetFpuRegs;
}
if (contextSizeCheck)
{
flags |= FiberFlagContextSizeCheck;
}
return flags;
}
private static bool TryCalculateInitialStackLayout(
ulong stackEnd,
out ulong entryRsp,
out ulong transferSlot)
{
var alignedEnd = stackEnd & ~15UL;
if (alignedEnd < 2 * sizeof(ulong))
{
entryRsp = 0;
transferSlot = 0;
return false;
}
entryRsp = alignedEnd - sizeof(ulong);
transferSlot = entryRsp - sizeof(ulong);
return true;
}
[Conditional("DEBUG")]
private static void RunFiberSelfChecks()
{
Debug.Assert(
ApplyInitializationFlags(0, Firmware350BuildVersion - 1, false) == 0,
"Pre-3.50 fibers unexpectedly enable SetFpuRegs.");
Debug.Assert(
ApplyInitializationFlags(0, Firmware350BuildVersion, false) == FiberFlagSetFpuRegs,
"3.50+ fibers must initialize x87/MXCSR control state.");
Debug.Assert(
ApplyInitializationFlags(1, Firmware350BuildVersion, true) ==
(1u | FiberFlagSetFpuRegs | FiberFlagContextSizeCheck),
"Fiber initialization discarded caller/internal flags.");
Debug.Assert(
TryCalculateInitialStackLayout(0x1017, out var rsp, out var slot) &&
rsp == 0x1008 && slot == 0x1000 && (rsp & 15) == 8,
"Initial fiber transfer slot does not produce SysV entry alignment.");
}
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
ResolveCurrentFiberAddress(ctx);
@@ -779,11 +904,11 @@ public static class FiberExports
return FiberErrorInvalid;
}
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
{
return FiberErrorInvalid;
}
@@ -839,8 +964,8 @@ public static class FiberExports
return false;
}
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
magicStart != FiberSignature0 ||
magicEnd != FiberSignature1)
{
@@ -855,12 +980,12 @@ public static class FiberExports
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
{
fields = default;
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
{
return false;
@@ -898,6 +1023,46 @@ public static class FiberExports
return 0;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
{
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
@@ -925,6 +1090,37 @@ public static class FiberExports
return true;
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
{
var bytes = new byte[capacity];
Span<byte> current = stackalloc byte[1];
for (var index = 0; index < bytes.Length; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
value = string.Empty;
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceFiber(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
@@ -946,9 +1142,10 @@ public static class FiberExports
GuestCpuContinuation Context,
ulong ArgOnRunAddress);
private readonly record struct FiberReturnTarget(
ulong PreviousFiber,
FiberContinuation? ThreadContinuation);
private sealed record FiberThreadState(
FiberContinuation RootContinuation,
ulong CurrentFiber,
ulong PreviousFiber);
private readonly record struct FiberStackRange(ulong Start, ulong Size)
{
+372
View File
@@ -0,0 +1,372 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Font;
public static class FontExports
{
private static readonly object AllocationGate = new();
private static ulong _librarySelectionAddress;
private static ulong _rendererSelectionAddress;
[SysAbiExport(
Nid = "whrS4oksXc4",
ExportName = "sceFontMemoryInit",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int MemoryInit(CpuContext ctx)
{
var descriptorAddress = ctx[CpuRegister.Rdi];
var regionAddress = ctx[CpuRegister.Rsi];
var regionSize = (uint)ctx[CpuRegister.Rdx];
var interfaceAddress = ctx[CpuRegister.Rcx];
var mspaceAddress = ctx[CpuRegister.R8];
var destroyCallback = ctx[CpuRegister.R9];
if (descriptorAddress == 0 ||
!TryWriteUInt32(ctx, descriptorAddress, 0x00000F00) ||
!TryWriteUInt32(ctx, descriptorAddress + 0x04, regionSize) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x08, regionAddress) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x10, mspaceAddress) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x18, interfaceAddress) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x20, destroyCallback) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x28, 0) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x30, 0) ||
!ctx.TryWriteUInt64(descriptorAddress + 0x38, mspaceAddress))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetSuccess(ctx);
}
[SysAbiExport(
Nid = "oM+XCzVG3oM",
ExportName = "sceFontSelectLibraryFt",
Target = Generation.Gen5,
LibraryName = "libSceFontFt")]
public static int SelectLibraryFt(CpuContext ctx) =>
ReturnSelection(ctx, ref _librarySelectionAddress, 0x38);
[SysAbiExport(
Nid = "Xx974EW-QFY",
ExportName = "sceFontSelectRendererFt",
Target = Generation.Gen5,
LibraryName = "libSceFontFt")]
public static int SelectRendererFt(CpuContext ctx) =>
ReturnSelection(ctx, ref _rendererSelectionAddress, 0x100);
[SysAbiExport(
Nid = "n590hj5Oe-k",
ExportName = "sceFontCreateLibraryWithEdition",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int CreateLibraryWithEdition(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F01);
[SysAbiExport(
Nid = "WaSFJoRWXaI",
ExportName = "sceFontCreateRendererWithEdition",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int CreateRendererWithEdition(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.Rcx], 0x100, magic: 0x0F07);
[SysAbiExport(
Nid = "3OdRkSjOcog",
ExportName = "sceFontBindRenderer",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int BindRenderer(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "N1EBMeGhf7E",
ExportName = "sceFontSetScalePixel",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SetScalePixel(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "TMtqoFQjjbA",
ExportName = "sceFontSetEffectSlant",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SetEffectSlant(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "v0phZwa4R5o",
ExportName = "sceFontSetEffectWeight",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SetEffectWeight(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "6vGCkkQJOcI",
ExportName = "sceFontSetupRenderScalePixel",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SetupRenderScalePixel(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "lz9y9UFO2UU",
ExportName = "sceFontSetupRenderEffectSlant",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SetupRenderEffectSlant(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "XIGorvLusDQ",
ExportName = "sceFontSetupRenderEffectWeight",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SetupRenderEffectWeight(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "imxVx8lm+KM",
ExportName = "sceFontGetHorizontalLayout",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int GetHorizontalLayout(CpuContext ctx)
{
var layoutAddress = ctx[CpuRegister.Rsi];
if (layoutAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// Baseline, line advance, decoration extent: the same invented geometry
// as GetRenderCharGlyphMetrics.
var values = new[] { 12.0f, 16.0f, 0.0f };
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
ctx,
layoutAddress + (ulong)(index * sizeof(float)),
BitConverter.SingleToUInt32Bits(values[index])))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
return SetSuccess(ctx);
}
[SysAbiExport(
Nid = "cKYtVmeSTcw",
ExportName = "sceFontOpenFontSet",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int OpenFontSet(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
[SysAbiExport(
Nid = "KXUpebrFk1U",
ExportName = "sceFontOpenFontMemory",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int OpenFontMemory(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
[SysAbiExport(
Nid = "JzCH3SCFnAU",
ExportName = "sceFontOpenFontInstance",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int OpenFontInstance(CpuContext ctx)
{
var sourceHandle = ctx[CpuRegister.Rdi];
var setupHandle = ctx[CpuRegister.Rsi];
var outputAddress = ctx[CpuRegister.Rdx];
if (outputAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (setupHandle != 0)
{
return ctx.TryWriteUInt64(outputAddress, setupHandle)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!TryAllocateOpaque(ctx, 0x100, out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (sourceHandle != 0)
{
Span<byte> source = stackalloc byte[0x100];
if (ctx.Memory.TryRead(sourceHandle, source))
{
_ = ctx.Memory.TryWrite(handle, source);
}
}
_ = TryWriteUInt16(ctx, handle, 0x0F02);
return ctx.TryWriteUInt64(outputAddress, handle)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "SsRbbCiWoGw",
ExportName = "sceFontSupportSystemFonts",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SupportSystemFonts(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "mz2iTY0MK4A",
ExportName = "sceFontSupportExternalFonts",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SupportExternalFonts(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "CUKn5pX-NVY",
ExportName = "sceFontAttachDeviceCacheBuffer",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int AttachDeviceCacheBuffer(CpuContext ctx) => SetSuccess(ctx);
[SysAbiExport(
Nid = "IQtleGLL5pQ",
ExportName = "sceFontGetRenderCharGlyphMetrics",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int GetRenderCharGlyphMetrics(CpuContext ctx)
{
var metricsAddress = ctx[CpuRegister.Rdx];
if (metricsAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
ctx,
metricsAddress + (ulong)(index * sizeof(float)),
BitConverter.SingleToUInt32Bits(values[index])))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
return SetSuccess(ctx);
}
[SysAbiExport(
Nid = "gdUCnU0gHdI",
ExportName = "sceFontRenderSurfaceInit",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int RenderSurfaceInit(CpuContext ctx)
{
var surfaceAddress = ctx[CpuRegister.Rdi];
var bufferAddress = ctx[CpuRegister.Rsi];
var widthBytes = (uint)ctx[CpuRegister.Rdx];
var pixelBytes = (uint)ctx[CpuRegister.Rcx] & 0xFF;
var width = (uint)ctx[CpuRegister.R8];
var height = (uint)ctx[CpuRegister.R9];
if (surfaceAddress == 0 ||
!ctx.TryWriteUInt64(surfaceAddress, bufferAddress) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x08, widthBytes) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x0C, pixelBytes) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x10, width) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x14, height) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x18, 0) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x1C, 0) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x20, width) ||
!TryWriteUInt32(ctx, surfaceAddress + 0x24, height))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetSuccess(ctx);
}
private static int ReturnSelection(CpuContext ctx, ref ulong selectionAddress, uint objectSize)
{
if (ctx[CpuRegister.Rdi] != 0)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
lock (AllocationGate)
{
if (selectionAddress == 0)
{
if (!TryAllocateOpaque(ctx, 0x20, out selectionAddress) ||
!TryWriteUInt32(ctx, selectionAddress, 0) ||
!TryWriteUInt32(ctx, selectionAddress + 4, objectSize))
{
selectionAddress = 0;
}
}
}
ctx[CpuRegister.Rax] = selectionAddress;
return 0;
}
private static int CreateOpaqueHandle(CpuContext ctx, ulong outputAddress, int size, ushort magic)
{
if (outputAddress == 0 || !TryAllocateOpaque(ctx, size, out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!TryWriteUInt16(ctx, handle, magic) || !ctx.TryWriteUInt64(outputAddress, handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetSuccess(ctx);
}
private static bool TryAllocateOpaque(CpuContext ctx, int size, out ulong address)
{
address = 0;
if (ctx.Memory is not IGuestMemoryAllocator allocator ||
!allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address))
{
return false;
}
Span<byte> bytes = stackalloc byte[size];
bytes.Clear();
return ctx.Memory.TryWrite(address, bytes);
}
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetSuccess(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.Vulkan;
namespace SharpEmu.Libs.Gpu;
/// <summary>
/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
/// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>.
/// Vulkan is the only backend today; Metal/DX12 slot in here.
/// </summary>
internal static class GuestGpu
{
private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
public static IGuestGpuBackend Current => Instance.Value;
}
+164
View File
@@ -0,0 +1,164 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Gpu;
// The types that cross the guest-GPU backend seam. Every field is either a neutral
// primitive (dimensions, counts, host pixel bytes) or a raw guest/AGC value (guest
// addresses, guest format and number-type codes, guest register bitfields). Host
// graphics-API values must never appear here: each backend owns the guest -> native
// translation for its API.
/// <summary>A guest texture referenced by a draw or dispatch. Format/NumberType/
/// TileMode/DstSelect are raw guest descriptor codes.</summary>
internal sealed record GuestDrawTexture(
ulong Address,
uint Width,
uint Height,
uint Format,
uint NumberType,
byte[] RgbaPixels,
bool IsFallback,
bool IsStorage,
uint MipLevels = 1,
uint MipLevel = 0,
uint BaseMipLevel = 0,
uint ResourceMipLevels = 1,
uint Pitch = 0,
uint TileMode = 0,
uint DstSelect = 0xFAC,
GuestSampler Sampler = default);
/// <summary>Raw guest sampler descriptor dwords, copied verbatim from guest memory.</summary>
internal readonly record struct GuestSampler(
uint Word0,
uint Word1,
uint Word2,
uint Word3);
internal sealed record GuestMemoryBuffer(
ulong BaseAddress,
byte[] Data,
int Length,
bool Pooled,
bool Writable = false,
bool WriteBackToGuest = true);
/// <summary>DataFormat/NumberFormat are raw guest vertex-attribute codes.</summary>
internal sealed record GuestVertexBuffer(
uint Location,
uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress,
uint Stride,
uint OffsetBytes,
byte[] Data,
int Length,
bool Pooled);
internal sealed record GuestIndexBuffer(
byte[] Data,
int Length,
bool Is32Bit,
bool Pooled);
internal readonly record struct GuestRect(
int X,
int Y,
uint Width,
uint Height);
internal readonly record struct GuestViewport(
float X,
float Y,
float Width,
float Height,
float MinDepth,
float MaxDepth);
internal readonly record struct GuestRasterState(
bool CullFront,
bool CullBack,
bool FrontFaceClockwise,
bool Wireframe)
{
public static GuestRasterState Default { get; } = new(false, false, false, false);
}
// CompareOp uses the GCN DB_DEPTH_CONTROL ZFUNC encoding, which matches the
// Vulkan CompareOp ordering (0=Never through 7=Always).
internal readonly record struct GuestDepthState(
bool TestEnable,
bool WriteEnable,
uint CompareOp,
bool ClearEnable = false)
{
public static GuestDepthState Default { get; } = new(false, false, 7, false);
}
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
/// defaults (1/0) are the guest ONE/ZERO codes.</summary>
internal readonly record struct GuestBlendState(
bool Enable,
uint ColorSrcFactor,
uint ColorDstFactor,
uint ColorFunc,
uint AlphaSrcFactor,
uint AlphaDstFactor,
uint AlphaFunc,
bool SeparateAlphaBlend,
uint WriteMask)
{
public static GuestBlendState Default { get; } = new(
Enable: false,
ColorSrcFactor: 1,
ColorDstFactor: 0,
ColorFunc: 0,
AlphaSrcFactor: 1,
AlphaDstFactor: 0,
AlphaFunc: 0,
SeparateAlphaBlend: false,
WriteMask: 0xFu);
}
internal sealed record GuestRenderState(
IReadOnlyList<GuestBlendState> Blends,
GuestRect? Scissor,
GuestViewport? Viewport,
GuestRasterState Raster,
GuestDepthState Depth)
{
public static GuestRenderState Default { get; } = new(
[GuestBlendState.Default],
Scissor: null,
Viewport: null,
GuestRasterState.Default,
GuestDepthState.Default);
public GuestBlendState Blend =>
Blends.Count == 0 ? GuestBlendState.Default : Blends[0];
}
/// <summary>Format/NumberType are raw guest render-target register codes.</summary>
internal sealed record GuestRenderTarget(
ulong Address,
uint Width,
uint Height,
uint Format,
uint NumberType,
uint MipLevels = 1);
/// <summary>Guest DB surface bound alongside a color render target.</summary>
internal sealed record GuestDepthTarget(
ulong ReadAddress,
ulong WriteAddress,
uint Width,
uint Height,
uint GuestFormat,
uint SwizzleMode,
float ClearDepth,
bool ReadOnly)
{
public ulong Address => WriteAddress != 0 ? WriteAddress : ReadAddress;
}
@@ -0,0 +1,19 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Gpu;
/// <summary>
/// A guest shader compiled by a backend, opaque to the export layers: only the backend
/// that produced it can submit it. <see cref="Payload"/> is the backend-defined compiled
/// bytes (SPIR-V words for Vulkan; MSL/DXIL for future backends), exposed solely for
/// diagnostics dumps and size traces — callers must never interpret it.
/// </summary>
internal interface IGuestCompiledShader
{
byte[] Payload { get; }
/// <summary>File extension for diagnostics dumps of <see cref="Payload"/> ("spv",
/// "msl", ...), so dumps stay honestly labeled whatever the backend.</summary>
string PayloadFileExtension { get; }
}
+191
View File
@@ -0,0 +1,191 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.ShaderCompiler;
namespace SharpEmu.Libs.Gpu;
/// <summary>
/// The guest-GPU backend seam: everything the AGC/VideoOut export layers need from a
/// host renderer, expressed in guest-domain terms so Vulkan, Metal, and DX12 backends
/// can each translate to their native API. Two rules keep it that way: no host-API
/// value (formats, blend enums, barrier or pass concepts) may cross this interface,
/// and submission is coarse-grained — synchronization is a backend-internal concern.
///
/// Shader compilation also lives behind the seam: the backend owns its codegen and
/// returns opaque <see cref="IGuestCompiledShader"/> handles that only it can submit.
/// </summary>
internal interface IGuestGpuBackend
{
/// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary>
void EnsureStarted(uint width, uint height);
// Shader compilation. The optional base/index parameters describe how a multi-stage
// draw lays both stages' resources into one flat per-role slot space (buffers,
// images, scalar-spill slots); each backend maps those slots to its own API binding
// model. -1 keeps the emitter's single-stage defaults.
bool TryCompileVertexShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
out IGuestCompiledShader? shader,
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1,
int requiredVertexOutputCount = 0,
ulong storageBufferOffsetAlignment = 1);
bool TryCompilePixelShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
IReadOnlyList<Gen5PixelOutputBinding> outputs,
out IGuestCompiledShader? shader,
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1,
uint pixelInputEnable = 0,
uint pixelInputAddress = 0,
ulong storageBufferOffsetAlignment = 1);
bool TryCompileComputeShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
out IGuestCompiledShader? shader,
out string error,
int totalGlobalBufferCount = -1,
int initialScalarBufferIndex = -1,
uint waveLaneCount = 32,
ulong storageBufferOffsetAlignment = 1);
/// <summary>Returns the backend's no-color-output fragment shader.</summary>
IGuestCompiledShader GetDepthOnlyFragmentShader();
void HideSplashScreen();
/// <summary>Presents one CPU-produced BGRA frame.</summary>
void Submit(byte[] bgraFrame, uint width, uint height);
/// <summary>Presents a recognized fixed-function guest draw (see GuestDrawKind).</summary>
void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height);
void SubmitTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint width,
uint height,
uint attributeCount,
IGuestCompiledShader? vertexShader = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null);
void SubmitDepthOnlyTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
GuestDepthTarget depthTarget,
IGuestCompiledShader? vertexShader = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
ulong shaderAddress = 0);
void SubmitOffscreenTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
IReadOnlyList<GuestRenderTarget> targets,
IGuestCompiledShader? vertexShader = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0);
void SubmitStorageTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
uint width,
uint height,
ulong shaderAddress = 0);
long SubmitComputeDispatch(
ulong shaderAddress,
IGuestCompiledShader computeShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint groupCountX,
uint groupCountY,
uint groupCountZ,
uint baseGroupX,
uint baseGroupY,
uint baseGroupZ,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
bool isIndirect,
bool writesGlobalMemory,
uint threadCountX = uint.MaxValue,
uint threadCountY = uint.MaxValue,
uint threadCountZ = uint.MaxValue);
bool TrySubmitGuestImage(
ulong address,
uint width,
uint height,
uint pitchInPixel);
bool TrySubmitOrderedGuestImageFlip(
int videoOutHandle,
int displayBufferIndex,
ulong address,
uint width,
uint height,
uint pitchInPixel);
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);
/// <summary>Format/numberType are raw guest texture descriptor codes.</summary>
bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType);
bool TrySubmitGuestImageBlit(
ulong sourceAddress,
uint sourceWidth,
uint sourceHeight,
uint sourceFormat,
uint sourceNumberType,
ulong destinationAddress,
uint destinationWidth,
uint destinationHeight,
uint destinationFormat,
uint destinationNumberType);
/// <summary>
/// Whether the backend supports the guest render-target format, and how its pixel
/// outputs are typed. Deliberately does not expose the backend's native format —
/// the guest codes cross the seam and each backend maps them internally.
/// </summary>
bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
}
@@ -0,0 +1,12 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Gpu.Vulkan;
/// <summary>The Vulkan backend's compiled shader: raw SPIR-V words.</summary>
internal sealed record VulkanCompiledGuestShader(byte[] Spirv) : IGuestCompiledShader
{
public byte[] Payload => Spirv;
public string PayloadFileExtension => "spv";
}
@@ -0,0 +1,351 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.VideoOut;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
namespace SharpEmu.Libs.Gpu.Vulkan;
/// <summary>
/// Vulkan backend for the guest-GPU seam: SPIR-V codegen via
/// SharpEmu.ShaderCompiler.Vulkan, rendering via a thin adapter over the existing
/// VulkanVideoPresenter statics (folding the presenter into an instance type is
/// follow-up work, not a seam concern).
/// </summary>
internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
{
private static readonly IGuestCompiledShader DepthOnlyFragmentShader =
new VulkanCompiledGuestShader(SpirvFixedShaders.CreateDepthOnlyFragment());
public bool TryCompileVertexShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
out IGuestCompiledShader? shader,
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1,
int requiredVertexOutputCount = 0,
ulong storageBufferOffsetAlignment = 1)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompileVertexShader(
state,
evaluation,
out var compiled,
out error,
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase,
scalarRegisterBufferIndex,
requiredVertexOutputCount,
storageBufferOffsetAlignment))
{
return false;
}
shader = new VulkanCompiledGuestShader(compiled.Spirv);
return true;
}
public bool TryCompilePixelShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
IReadOnlyList<Gen5PixelOutputBinding> outputs,
out IGuestCompiledShader? shader,
out string error,
int globalBufferBase = 0,
int totalGlobalBufferCount = -1,
int imageBindingBase = 0,
int scalarRegisterBufferIndex = -1,
uint pixelInputEnable = 0,
uint pixelInputAddress = 0,
ulong storageBufferOffsetAlignment = 1)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompilePixelShader(
state,
evaluation,
outputs,
out var compiled,
out error,
globalBufferBase,
totalGlobalBufferCount,
imageBindingBase,
scalarRegisterBufferIndex,
pixelInputEnable,
pixelInputAddress,
storageBufferOffsetAlignment))
{
return false;
}
shader = new VulkanCompiledGuestShader(compiled.Spirv);
return true;
}
public bool TryCompileComputeShader(
Gen5ShaderState state,
Gen5ShaderEvaluation evaluation,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
out IGuestCompiledShader? shader,
out string error,
int totalGlobalBufferCount = -1,
int initialScalarBufferIndex = -1,
uint waveLaneCount = 32,
ulong storageBufferOffsetAlignment = 1)
{
shader = null;
if (!Gen5SpirvTranslator.TryCompileComputeShader(
state,
evaluation,
localSizeX,
localSizeY,
localSizeZ,
out var compiled,
out error,
totalGlobalBufferCount,
initialScalarBufferIndex,
waveLaneCount,
storageBufferOffsetAlignment))
{
return false;
}
shader = new VulkanCompiledGuestShader(compiled.Spirv);
return true;
}
public IGuestCompiledShader GetDepthOnlyFragmentShader() =>
DepthOnlyFragmentShader;
public void EnsureStarted(uint width, uint height) =>
VulkanVideoPresenter.EnsureStarted(width, height);
public void HideSplashScreen() =>
VulkanVideoPresenter.HideSplashScreen();
public void Submit(byte[] bgraFrame, uint width, uint height) =>
VulkanVideoPresenter.Submit(bgraFrame, width, height);
public void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height) =>
VulkanVideoPresenter.SubmitGuestDraw(drawKind, width, height);
public void SubmitTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint width,
uint height,
uint attributeCount,
IGuestCompiledShader? vertexShader = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null) =>
VulkanVideoPresenter.SubmitTranslatedDraw(
Spirv(pixelShader),
textures,
globalMemoryBuffers,
width,
height,
attributeCount,
vertexShader is null ? null : Spirv(vertexShader),
vertexCount,
instanceCount,
primitiveType,
indexBuffer,
vertexBuffers,
renderState);
public void SubmitDepthOnlyTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
GuestDepthTarget depthTarget,
IGuestCompiledShader? vertexShader = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
ulong shaderAddress = 0) =>
VulkanVideoPresenter.SubmitDepthOnlyTranslatedDraw(
Spirv(pixelShader),
textures,
globalMemoryBuffers,
attributeCount,
depthTarget,
vertexShader is null ? null : Spirv(vertexShader),
vertexCount,
instanceCount,
primitiveType,
indexBuffer,
vertexBuffers,
renderState,
shaderAddress);
public void SubmitOffscreenTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
IReadOnlyList<GuestRenderTarget> targets,
IGuestCompiledShader? vertexShader = null,
uint vertexCount = 3,
uint instanceCount = 1,
uint primitiveType = 4,
GuestIndexBuffer? indexBuffer = null,
IReadOnlyList<GuestVertexBuffer>? vertexBuffers = null,
GuestRenderState? renderState = null,
GuestDepthTarget? depthTarget = null,
ulong shaderAddress = 0) =>
VulkanVideoPresenter.SubmitOffscreenTranslatedDraw(
Spirv(pixelShader),
textures,
globalMemoryBuffers,
attributeCount,
targets,
vertexShader is null ? null : Spirv(vertexShader),
vertexCount,
instanceCount,
primitiveType,
indexBuffer,
vertexBuffers,
renderState,
depthTarget,
shaderAddress);
public void SubmitStorageTranslatedDraw(
IGuestCompiledShader pixelShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint attributeCount,
uint width,
uint height,
ulong shaderAddress = 0) =>
VulkanVideoPresenter.SubmitStorageTranslatedDraw(
Spirv(pixelShader),
textures,
globalMemoryBuffers,
attributeCount,
width,
height,
shaderAddress);
public long SubmitComputeDispatch(
ulong shaderAddress,
IGuestCompiledShader computeShader,
IReadOnlyList<GuestDrawTexture> textures,
IReadOnlyList<GuestMemoryBuffer> globalMemoryBuffers,
uint groupCountX,
uint groupCountY,
uint groupCountZ,
uint baseGroupX,
uint baseGroupY,
uint baseGroupZ,
uint localSizeX,
uint localSizeY,
uint localSizeZ,
bool isIndirect,
bool writesGlobalMemory,
uint threadCountX = uint.MaxValue,
uint threadCountY = uint.MaxValue,
uint threadCountZ = uint.MaxValue) =>
VulkanVideoPresenter.SubmitComputeDispatch(
shaderAddress,
Spirv(computeShader),
textures,
globalMemoryBuffers,
groupCountX,
groupCountY,
groupCountZ,
baseGroupX,
baseGroupY,
baseGroupZ,
localSizeX,
localSizeY,
localSizeZ,
isIndirect,
writesGlobalMemory,
threadCountX,
threadCountY,
threadCountZ);
public bool TrySubmitGuestImage(
ulong address,
uint width,
uint height,
uint pitchInPixel) =>
VulkanVideoPresenter.TrySubmitGuestImage(address, width, height, pitchInPixel);
public bool TrySubmitOrderedGuestImageFlip(
int videoOutHandle,
int displayBufferIndex,
ulong address,
uint width,
uint height,
uint pitchInPixel) =>
VulkanVideoPresenter.TrySubmitOrderedGuestImageFlip(
videoOutHandle,
displayBufferIndex,
address,
width,
height,
pitchInPixel);
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
public bool IsGpuGuestImageAvailable(ulong address, uint format, uint numberType) =>
VulkanVideoPresenter.IsGpuGuestImageAvailable(address, format, numberType);
public bool TrySubmitGuestImageBlit(
ulong sourceAddress,
uint sourceWidth,
uint sourceHeight,
uint sourceFormat,
uint sourceNumberType,
ulong destinationAddress,
uint destinationWidth,
uint destinationHeight,
uint destinationFormat,
uint destinationNumberType) =>
VulkanVideoPresenter.TrySubmitGuestImageBlit(
sourceAddress,
sourceWidth,
sourceHeight,
sourceFormat,
sourceNumberType,
destinationAddress,
destinationWidth,
destinationHeight,
destinationFormat,
destinationNumberType);
public bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind)
{
if (VulkanVideoPresenter.TryDecodeRenderTargetFormat(dataFormat, numberType, out var format))
{
outputKind = format.OutputKind;
return true;
}
outputKind = default;
return false;
}
private static byte[] Spirv(IGuestCompiledShader shader) =>
shader is VulkanCompiledGuestShader vulkanShader
? vulkanShader.Spirv
: throw new InvalidOperationException(
$"shader handle of type {shader.GetType().Name} was not compiled by the Vulkan backend");
}
+82
View File
@@ -0,0 +1,82 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.Libs;
/// <summary>
/// High-resolution host sleeps for guest pacing. <see cref="Thread.Sleep(int)"/>
/// routinely overshoots by a scheduler quantum, which turns per-frame waits
/// (flip pacing, usleep-based game loops) into a hard frame-rate cap; these
/// helpers sleep coarsely for the bulk of the interval and yield-spin the
/// remainder so wakeups land within tens of microseconds of the target.
/// </summary>
internal static class HostTiming
{
/// <summary>
/// Blocks until <see cref="Stopwatch.GetTimestamp"/> reaches
/// <paramref name="targetTimestamp"/>.
/// </summary>
public static void SleepUntil(long targetTimestamp)
{
while (true)
{
var remainingTicks = targetTimestamp - Stopwatch.GetTimestamp();
if (remainingTicks <= 0)
{
return;
}
if (remainingTicks > Stopwatch.Frequency * 60)
{
// Far-future target: coarse sleep avoids overflowing the
// microsecond conversion below and needs no precision.
Thread.Sleep(30_000);
continue;
}
var remainingMicroseconds = remainingTicks * 1_000_000 / Stopwatch.Frequency;
if (remainingMicroseconds > 2200)
{
// Coarse sleep for the bulk; macOS overshoots ~0.5-1.5 ms.
Thread.Sleep((int)((remainingMicroseconds - 1200) / 1000));
}
else if (remainingMicroseconds > 1200)
{
// A 1 ms nap typically wakes within the margin and costs no
// CPU, unlike yield-spinning through the whole tail.
Thread.Sleep(1);
}
else if (remainingMicroseconds > 100)
{
Thread.Sleep(0);
}
else
{
Thread.SpinWait(64);
}
}
}
/// <summary>Blocks for the given number of microseconds.</summary>
public static void SleepMicroseconds(long microseconds)
{
if (microseconds <= 0)
{
return;
}
if (microseconds >= 10_000_000)
{
// Long/sentinel sleeps (usleep(-1) parks): sub-millisecond
// precision is irrelevant and the tick conversion below would
// overflow, which used to turn the park into a hot spin.
Thread.Sleep((int)Math.Min(microseconds / 1000, int.MaxValue));
return;
}
var ticks = microseconds * Stopwatch.Frequency / 1_000_000;
SleepUntil(Stopwatch.GetTimestamp() + ticks);
}
}
+540 -21
View File
@@ -2,12 +2,34 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Linq;
using System.Text;
using System.Text.Json;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Json;
public static class JsonExports
{
private const int ValueObjectSize = 0x20;
private const int StringObjectSize = 0x08;
private const ulong MaximumJsonBufferSize = 16 * 1024 * 1024;
private const int SceJsonParserErrorInvalidToken = unchecked((int)0x80920101);
private const int SceJsonParserErrorEmptyBuffer = unchecked((int)0x80920105);
private sealed record JsonValueState(JsonElement Element);
private sealed record JsonStringState(
string Value,
ulong GuestBufferAddress = 0,
int GuestBufferCapacity = 0);
private static readonly ConcurrentDictionary<ulong, JsonValueState> _values = new();
private static readonly ConcurrentDictionary<ulong, JsonStringState> _strings = new();
private static readonly JsonElement _nullElement = CreateNullElement();
[SysAbiExport(
Nid = "-hJRce8wn1U",
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
@@ -78,9 +100,6 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
// sce::Json::Initializer::setGlobalNullAccessCallback(const Value& (*)(ValueType, const Value*, void*), void*)
// Registers the guest hook invoked when a Value is accessed as the wrong type. Quake calls it
// during kexPSNWebAPI::Initialize and treats a non-zero return as a fatal init failure.
[SysAbiExport(
Nid = "+drDFyAS6u4",
ExportName = "_ZN3sce4Json11Initializer27setGlobalNullAccessCallbackEPFRKNS0_5ValueENS0_9ValueTypeEPS3_PvES7_",
@@ -91,26 +110,38 @@ public static class JsonExports
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;
return SetReturn(ctx, (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;
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2Constructor(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// The PS5 ABI object occupies 0x28 bytes in the caller's frame. Its
// setters below replace the allocator and file-buffer fields.
Span<byte> parameter = stackalloc byte[0x28];
parameter.Clear();
if (!ctx.Memory.TryWrite(thisAddress, parameter))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceJson("InitParameter2.ctor", thisAddress, 0);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -118,12 +149,24 @@ public static class JsonExports
[SysAbiExport(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2SetAllocator(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setAllocator", thisAddress, ctx[CpuRegister.Rsi]);
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> fields = stackalloc byte[sizeof(ulong) * 2];
BinaryPrimitives.WriteUInt64LittleEndian(fields, ctx[CpuRegister.Rsi]);
BinaryPrimitives.WriteUInt64LittleEndian(fields[sizeof(ulong)..], ctx[CpuRegister.Rdx]);
if (!ctx.Memory.TryWrite(thisAddress, fields))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -131,12 +174,16 @@ public static class JsonExports
[SysAbiExport(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2SetFileBufferSize(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("InitParameter2.setFileBufferSize", thisAddress, ctx[CpuRegister.Rsi]);
if (thisAddress == 0 || !ctx.TryWriteUInt64(thisAddress + 0x10, ctx[CpuRegister.Rsi]))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -144,14 +191,474 @@ public static class JsonExports
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson2")]
public static int Initializer2Constructor(CpuContext ctx)
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerInitialize2(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
TraceJson("Initializer2.ctor", thisAddress, ctx[CpuRegister.Rsi]);
var initParameterAddress = ctx[CpuRegister.Rsi];
if (thisAddress == 0 || initParameterAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
TraceJson("Initializer.initialize2", thisAddress, initParameterAddress);
return SetReturn(ctx, 0);
}
public static int ValueConstructor(CpuContext ctx)
{
_ = ConstructValue(ctx);
return JsonValueExports.ValueDefaultConstructor(ctx);
}
[SysAbiExport(
Nid = "-wa17B7TGnw",
ExportName = "_ZN3sce4Json5ValueC2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueBaseConstructor(CpuContext ctx) => ConstructValue(ctx);
public static int ValueDestructor(CpuContext ctx)
{
_ = DestroyValue(ctx);
return JsonValueExports.ValueDestructor(ctx);
}
[SysAbiExport(
Nid = "0eUrW9JAxM0",
ExportName = "_ZN3sce4Json5ValueD2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueBaseDestructor(CpuContext ctx) => DestroyValue(ctx);
[SysAbiExport(
Nid = "S5JxQnoGF3E",
ExportName = "_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ParserParseBuffer(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var bufferAddress = ctx[CpuRegister.Rsi];
var bufferSize = ctx[CpuRegister.Rdx];
if (valueAddress == 0 || bufferAddress == 0 || bufferSize == 0)
{
return SetReturn(ctx, SceJsonParserErrorEmptyBuffer);
}
if (bufferSize > MaximumJsonBufferSize || bufferSize > int.MaxValue)
{
return SetReturn(ctx, SceJsonParserErrorInvalidToken);
}
var buffer = new byte[(int)bufferSize];
if (!ctx.Memory.TryRead(bufferAddress, buffer))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
try
{
using var document = JsonDocument.Parse(buffer);
var element = document.RootElement.Clone();
StoreValue(ctx, valueAddress, element);
TraceJsonText("Parser.parse", valueAddress, Encoding.UTF8.GetString(buffer));
return SetReturn(ctx, 0);
}
catch (JsonException)
{
return SetReturn(ctx, SceJsonParserErrorInvalidToken);
}
}
[SysAbiExport(
Nid = "SHtAad20YYM",
ExportName = "_ZNK3sce4Json5Value7getTypeEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetType(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var element = GetValue(valueAddress);
ctx[CpuRegister.Rax] = (ulong)GetValueType(element);
return 0;
}
[SysAbiExport(
Nid = "RBw+4NukeGQ",
ExportName = "_ZNK3sce4Json5Value5countEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueCount(CpuContext ctx)
{
var element = GetValue(ctx[CpuRegister.Rdi]);
ctx[CpuRegister.Rax] = element.ValueKind switch
{
System.Text.Json.JsonValueKind.Array => (ulong)element.GetArrayLength(),
System.Text.Json.JsonValueKind.Object => (ulong)element.EnumerateObject().Count(),
_ => 0,
};
return 0;
}
[SysAbiExport(
Nid = "zTwZdI8AZ5Y",
ExportName = "_ZNK3sce4Json5Value10getBooleanEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetBoolean(CpuContext ctx) => ReturnValueStorage(ctx);
[SysAbiExport(
Nid = "DIxvoy7Ngvk",
ExportName = "_ZNK3sce4Json5Value10getIntegerEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetInteger(CpuContext ctx) => ReturnValueStorage(ctx);
[SysAbiExport(
Nid = "sn4HNCtNRzY",
ExportName = "_ZNK3sce4Json5Value11getUIntegerEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetUnsignedInteger(CpuContext ctx) => ReturnValueStorage(ctx);
[SysAbiExport(
Nid = "3qrge7L-AU4",
ExportName = "_ZNK3sce4Json5Value7getRealEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetReal(CpuContext ctx) => ReturnValueStorage(ctx);
[SysAbiExport(
Nid = "HwDt5lD9Bfo",
ExportName = "_ZNK3sce4Json5ValueixEPKc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueIndexCString(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var keyAddress = ctx[CpuRegister.Rsi];
if (!TryReadUtf8CString(ctx, keyAddress, 4096, out var key) ||
!TryAllocateGuestObject(ctx, ValueObjectSize, out var childAddress))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
var parent = GetValue(valueAddress);
var child = parent.ValueKind == System.Text.Json.JsonValueKind.Object &&
parent.TryGetProperty(key, out var property)
? property.Clone()
: _nullElement;
StoreValue(ctx, childAddress, child);
ctx[CpuRegister.Rax] = childAddress;
TraceJsonText("Value.index", valueAddress, key);
return 0;
}
[SysAbiExport(
Nid = "XlWbvieLj2M",
ExportName = "_ZNK3sce4Json5ValueixEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueIndexPosition(CpuContext ctx) => ReturnIndexedValue(ctx);
[SysAbiExport(
Nid = "0YqYAoO-+Uo",
ExportName = "_ZNK3sce4Json5Value8getValueEm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueGetPosition(CpuContext ctx) => ReturnIndexedValue(ctx);
[SysAbiExport(
Nid = "4zrm6VrgIAw",
ExportName = "_ZN3sce4Json5ValueaSERKS1_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueAssignment(CpuContext ctx)
{
var destinationAddress = ctx[CpuRegister.Rdi];
var sourceAddress = ctx[CpuRegister.Rsi];
if (destinationAddress != 0)
{
StoreValue(ctx, destinationAddress, GetValue(sourceAddress));
}
ctx[CpuRegister.Rax] = destinationAddress;
return 0;
}
public static int StringConstructor(CpuContext ctx)
{
_ = ConstructString(ctx);
return JsonValueExports.StringDefaultConstructor(ctx);
}
[SysAbiExport(
Nid = "eG9E9M6XvTM",
ExportName = "_ZN3sce4Json6StringC2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int StringBaseConstructor(CpuContext ctx) => ConstructString(ctx);
public static int StringDestructor(CpuContext ctx)
{
_ = DestroyString(ctx);
return JsonValueExports.StringDestructor(ctx);
}
[SysAbiExport(
Nid = "Ui7YFnSTCBw",
ExportName = "_ZN3sce4Json6StringD2Ev",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int StringBaseDestructor(CpuContext ctx) => DestroyString(ctx);
[SysAbiExport(
Nid = "Ncel8t2Rrpc",
ExportName = "_ZNK3sce4Json5Value8toStringERNS0_6StringE",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int ValueToString(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var stringAddress = ctx[CpuRegister.Rsi];
if (stringAddress != 0)
{
var element = GetValue(valueAddress);
var value = element.ValueKind == System.Text.Json.JsonValueKind.String
? element.GetString() ?? string.Empty
: element.GetRawText();
_strings[stringAddress] = new JsonStringState(value);
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
[SysAbiExport(
Nid = "L1KAkYWml-M",
ExportName = "_ZNK3sce4Json6String5c_strEv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int StringCStr(CpuContext ctx)
{
var stringAddress = ctx[CpuRegister.Rdi];
if (!_strings.TryGetValue(stringAddress, out var state))
{
state = new JsonStringState(string.Empty);
}
var bytes = Encoding.UTF8.GetBytes(state.Value + '\0');
var guestBufferAddress = state.GuestBufferAddress;
if (guestBufferAddress == 0 || state.GuestBufferCapacity < bytes.Length)
{
if (!TryAllocateGuestObject(ctx, bytes.Length, out guestBufferAddress))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
}
if (!ctx.Memory.TryWrite(guestBufferAddress, bytes))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
_strings[stringAddress] = state with
{
GuestBufferAddress = guestBufferAddress,
GuestBufferCapacity = bytes.Length,
};
ctx.TryWriteUInt64(stringAddress, guestBufferAddress);
ctx[CpuRegister.Rax] = guestBufferAddress;
TraceJsonText("String.c_str", stringAddress, state.Value);
return 0;
}
private static int ConstructValue(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
StoreValue(ctx, thisAddress, _nullElement);
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
return 0;
}
private static int ReturnIndexedValue(CpuContext ctx)
{
var valueAddress = ctx[CpuRegister.Rdi];
var position = ctx[CpuRegister.Rsi];
if (!TryAllocateGuestObject(ctx, ValueObjectSize, out var childAddress))
{
ctx[CpuRegister.Rax] = 0;
return 0;
}
var parent = GetValue(valueAddress);
var child = parent.ValueKind == System.Text.Json.JsonValueKind.Array &&
position < (ulong)parent.GetArrayLength()
? parent[(int)position].Clone()
: _nullElement;
StoreValue(ctx, childAddress, child);
ctx[CpuRegister.Rax] = childAddress;
return 0;
}
private static int ReturnValueStorage(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
ctx[CpuRegister.Rax] = thisAddress == 0 ? 0 : thisAddress + 0x10;
return 0;
}
private static int DestroyValue(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
_values.TryRemove(thisAddress, out _);
if (thisAddress != 0)
{
Span<byte> empty = stackalloc byte[ValueObjectSize];
empty.Clear();
ctx.Memory.TryWrite(thisAddress, empty);
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
private static int ConstructString(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
_strings[thisAddress] = new JsonStringState(string.Empty);
ctx.TryWriteUInt64(thisAddress, 0);
ctx[CpuRegister.Rax] = thisAddress;
return 0;
}
private static int DestroyString(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
_strings.TryRemove(thisAddress, out _);
if (thisAddress != 0)
{
ctx.TryWriteUInt64(thisAddress, 0);
}
ctx[CpuRegister.Rax] = 0;
return 0;
}
private static JsonElement CreateNullElement()
{
using var document = JsonDocument.Parse("null");
return document.RootElement.Clone();
}
private static JsonElement GetValue(ulong address) =>
address != 0 && _values.TryGetValue(address, out var state)
? state.Element
: _nullElement;
private static void StoreValue(CpuContext ctx, ulong address, JsonElement element)
{
if (address == 0)
{
return;
}
var clone = element.Clone();
_values[address] = new JsonValueState(clone);
Span<byte> mirror = stackalloc byte[ValueObjectSize];
mirror.Clear();
var type = GetValueType(clone);
BinaryPrimitives.WriteInt32LittleEndian(mirror[0x1C..], type);
switch (clone.ValueKind)
{
case System.Text.Json.JsonValueKind.True:
mirror[0x10] = 1;
break;
case System.Text.Json.JsonValueKind.Number when clone.TryGetInt64(out var integer):
BinaryPrimitives.WriteInt64LittleEndian(mirror[0x10..], integer);
break;
case System.Text.Json.JsonValueKind.Number when clone.TryGetUInt64(out var unsignedInteger):
BinaryPrimitives.WriteUInt64LittleEndian(mirror[0x10..], unsignedInteger);
break;
case System.Text.Json.JsonValueKind.Number:
BinaryPrimitives.WriteInt64LittleEndian(
mirror[0x10..],
BitConverter.DoubleToInt64Bits(clone.GetDouble()));
break;
}
ctx.Memory.TryWrite(address, mirror);
}
private static int GetValueType(JsonElement element) => element.ValueKind switch
{
System.Text.Json.JsonValueKind.True or System.Text.Json.JsonValueKind.False => 1,
System.Text.Json.JsonValueKind.Number when element.TryGetInt64(out _) => 2,
System.Text.Json.JsonValueKind.Number when element.TryGetUInt64(out _) => 3,
System.Text.Json.JsonValueKind.Number => 4,
System.Text.Json.JsonValueKind.String => 5,
System.Text.Json.JsonValueKind.Array => 6,
System.Text.Json.JsonValueKind.Object => 7,
_ => 0,
};
private static bool TryAllocateGuestObject(CpuContext ctx, int size, out ulong address)
{
address = 0;
return size > 0 &&
ctx.Memory is IGuestMemoryAllocator allocator &&
allocator.TryAllocateGuestMemory((ulong)size, 0x10, out address);
}
private static bool TryReadUtf8CString(
CpuContext ctx,
ulong address,
int maximumLength,
out string value)
{
value = string.Empty;
if (address == 0 || maximumLength <= 0)
{
return false;
}
var bytes = new byte[maximumLength];
Span<byte> current = stackalloc byte[1];
for (var index = 0; index < bytes.Length; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
return false;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
@@ -164,4 +671,16 @@ public static class JsonExports
Console.Error.WriteLine(
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} arg=0x{argument:X16}");
}
private static void TraceJsonText(string operation, ulong thisAddress, string value)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
{
return;
}
var preview = value.Length <= 128 ? value : value[..128];
Console.Error.WriteLine(
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} value={preview}");
}
}
@@ -1,8 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
@@ -25,6 +25,11 @@ public static class KernelEventFlagCompatExports
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
private static long _nextEventFlagHandle = 1;
// Cached once: gating every call site avoids building the interpolated
// trace string (and FormatFrameChain/FormatGuestWaitObject) when disabled.
private static readonly bool _traceEventFlag = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal);
private sealed class EventFlagState
{
public required string Name { get; init; }
@@ -34,45 +39,6 @@ public static class KernelEventFlagCompatExports
public object Gate { get; } = new();
}
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; }
// 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(
Nid = "BpFoboUJoZU",
ExportName = "sceKernelCreateEventFlag",
@@ -91,17 +57,17 @@ public static class KernelEventFlagCompatExports
optionAddress != 0 ||
!IsValidAttributes(attributes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
@@ -115,11 +81,11 @@ public static class KernelEventFlagCompatExports
if (!ctx.TryWriteUInt64(outAddress, handle))
{
_eventFlags.TryRemove(handle, out _);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
if (_traceEventFlag) TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -132,7 +98,7 @@ public static class KernelEventFlagCompatExports
var handle = ctx[CpuRegister.Rdi];
if (!_eventFlags.TryRemove(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -140,8 +106,8 @@ public static class KernelEventFlagCompatExports
Monitor.PulseAll(state.Gate);
}
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
if (_traceEventFlag) TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -156,18 +122,18 @@ public static class KernelEventFlagCompatExports
var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
state.Bits |= pattern;
Monitor.PulseAll(state.Gate);
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
if (_traceEventFlag) TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -181,16 +147,16 @@ public static class KernelEventFlagCompatExports
var pattern = ctx[CpuRegister.Rsi];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
state.Bits &= pattern;
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
if (_traceEventFlag) TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -207,29 +173,29 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (state.Gate)
{
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
ApplyClearMode(state, pattern, waitMode);
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
if (_traceEventFlag) TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -249,18 +215,18 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Monitor.Enter(state.Gate);
@@ -268,77 +234,76 @@ public static class KernelEventFlagCompatExports
{
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{
return ctx.SetReturn(immediateWaitResult);
return SetReturn(ctx, immediateWaitResult);
}
if (timeoutAddress != 0)
{
if (timeoutUsec == 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout=0 ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
var deadline = GuestThreadExecution.ComputeDeadlineTimestamp(
TimeSpan.FromTicks((long)timeoutUsec * 10L));
var timedWaiter = new EventFlagWaiter
{
Ctx = ctx,
State = state,
Pattern = pattern,
WaitMode = waitMode,
ResultAddress = resultAddress,
Timed = true,
TimeoutAddress = timeoutAddress,
DeadlineTimestamp = deadline,
};
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
timedWaiter,
blockDeadlineTimestamp: deadline))
{
state.WaitingThreads++;
TraceEventFlag($"wait-block-timed handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout-host handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
// Timed waits block on a deadline instead of returning TIMED_OUT
// immediately; a zero-microsecond timeout still degrades to an
// instant poll because the deadline is already in the past.
var deadline = timeoutAddress != 0
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
var hostDeadlineMs = timeoutAddress != 0
? Environment.TickCount64 + (timeoutUsec == 0
? 0L
: Math.Max(1L, (timeoutUsec + 999L) / 1000L))
: long.MaxValue;
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId;
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
var satisfied = false;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
new EventFlagWaiter
() =>
{
Ctx = ctx,
State = state,
Pattern = pattern,
WaitMode = waitMode,
ResultAddress = resultAddress,
});
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)}");
if (satisfied)
{
return (int)blockedWaitResult;
}
// Deadline expiry: report timeout with the current bits.
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
},
() =>
{
if (!TryPrepareBlockedWait(
ctx,
state,
pattern,
waitMode,
resultAddress,
out var preparedResult))
{
return false;
}
blockedWaitResult = preparedResult;
satisfied = true;
return true;
},
deadline);
if (_traceEventFlag) 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)}");
if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock)
{
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
state.WaitingThreads++;
TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
if (_traceEventFlag) TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
var releaseWaiter = true;
try
{
@@ -358,11 +323,22 @@ public static class KernelEventFlagCompatExports
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return ctx.SetReturn(pumpedWaitResult);
if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return SetReturn(ctx, pumpedWaitResult);
}
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
var remaining = hostDeadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(state.Gate, (int)Math.Min(remaining, HostWaitPumpMilliseconds));
}
}
finally
@@ -375,8 +351,8 @@ public static class KernelEventFlagCompatExports
}
state.WaitingThreads++;
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
finally
{
@@ -396,24 +372,26 @@ public static class KernelEventFlagCompatExports
var waiterCountAddress = ctx[CpuRegister.Rdx];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
if (waiterCountAddress != 0 &&
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
state.Bits = setPattern;
state.WaitingThreads = 0;
Monitor.PulseAll(state.Gate);
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
if (_traceEventFlag) TraceEventFlag(
$"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " +
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static bool IsValidAttributes(uint attributes)
@@ -503,107 +481,99 @@ public static class KernelEventFlagCompatExports
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
TraceEventFlag(
if (_traceEventFlag) TraceEventFlag(
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
return true;
}
}
private static bool TryCompleteBlockedTimedWait(
CpuContext ctx,
EventFlagState state,
EventFlagWaiter waiter,
ulong pattern,
uint waitMode,
ulong resultAddress)
{
lock (state.Gate)
{
if (waiter.Result is not null)
{
return true;
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_OK
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
ApplyClearMode(state, pattern, waitMode);
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
return true;
}
}
private static int CompleteBlockedTimedWait(
CpuContext ctx,
EventFlagState state,
EventFlagWaiter waiter,
ulong pattern,
uint waitMode,
ulong resultAddress,
ulong timeoutAddress,
long deadlineTimestamp)
{
lock (state.Gate)
{
if (waiter.Result is null)
{
if (IsSatisfied(state.Bits, pattern, waitMode))
{
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_OK
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
ApplyClearMode(state, pattern, waitMode);
}
}
else
{
waiter.Result = TryWriteResultPattern(ctx, resultAddress, state.Bits)
? OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT
: OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
}
}
if (waiter.Result == OrbisGen2Result.ORBIS_GEN2_OK)
{
var remainingTicks = deadlineTimestamp - Stopwatch.GetTimestamp();
var remainingMicros = remainingTicks <= 0
? 0u
: (uint)Math.Min(
uint.MaxValue,
remainingTicks / (double)Stopwatch.Frequency * 1_000_000d);
_ = ctx.TryWriteUInt32(timeoutAddress, remainingMicros);
}
else
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
}
return (int)waiter.Result.Value;
}
private static string GetEventFlagWakeKey(ulong handle) =>
$"event_flag:0x{handle:X16}";
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits);
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
{
var bytes = new byte[capacity];
Span<byte> current = stackalloc byte[1];
for (var index = 0; index < bytes.Length; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, current))
{
value = string.Empty;
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, index);
return true;
}
bytes[index] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static void TraceEventFlag(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
if (_traceEventFlag)
{
Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}");
}
@@ -684,7 +654,7 @@ public static class KernelEventFlagCompatExports
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadByte(address, out var value))
if (TryReadByte(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
@@ -692,7 +662,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt32(address, out var value))
if (TryReadUInt32(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
@@ -700,7 +670,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt64(address, out var value))
if (TryReadUInt64(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
@@ -395,13 +395,13 @@ public static class KernelEventQueueCompatExports
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -450,7 +450,7 @@ public static class KernelEventQueueCompatExports
}
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -589,6 +589,120 @@ public static class KernelEventQueueCompatExports
return triggeredCount;
}
/// <summary>
/// Triggers every registered event on every queue that matches <paramref name="filter"/>
/// regardless of the registration's <c>ident</c>. This is a workaround for PS5 AGC command
/// buffers, where <c>IT_EVENT_WRITE</c> carries a hardware <c>EVENT_TYPE</c> that does not
/// match the <c>eventId</c> the guest registered with <c>sceAgcDriverAddEqEvent</c>.
/// See issue #173.
/// </summary>
public static int TriggerRegisteredEventsByFilter(
short filter,
ulong data)
{
List<ulong>? wakeHandles = null;
var triggeredCount = 0;
lock (_eventQueueGate)
{
foreach (var (handle, registrations) in _registeredEvents)
{
foreach (var registration in registrations.Values)
{
if (registration.Filter != filter)
{
continue;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new KernelEventDeque();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
0,
1,
data,
registration.UserData));
(wakeHandles ??= new List<ulong>()).Add(handle);
triggeredCount++;
// A single queue only needs to be woken once, even if multiple
// registrations matched.
break;
}
}
}
if (wakeHandles is not null)
{
foreach (var handle in wakeHandles)
{
WakeEventQueue(handle);
}
}
return triggeredCount;
}
/// <summary>
/// Queues one event for every registration using <paramref name="filter"/>.
/// Unlike <see cref="TriggerRegisteredEvents"/>, this preserves distinct
/// event identifiers registered on the same queue. AGC driver completion
/// queues use this form because the driver, rather than a packet-provided
/// identifier, announces that the whole submission reached end-of-pipe.
/// </summary>
public static int TriggerRegisteredEventsDistinct(short filter)
{
HashSet<ulong>? wakeHandles = null;
var triggeredCount = 0;
lock (_eventQueueGate)
{
foreach (var (handle, registrations) in _registeredEvents)
{
foreach (var registration in registrations.Values)
{
if (registration.Filter != filter)
{
continue;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new KernelEventDeque();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
0,
1,
registration.Ident,
registration.UserData));
(wakeHandles ??= []).Add(handle);
triggeredCount++;
}
}
}
if (wakeHandles is not null)
{
foreach (var handle in wakeHandles)
{
WakeEventQueue(handle);
}
}
return triggeredCount;
}
private static bool TriggerRegisteredEvent(
ulong handle,
ulong ident,
@@ -692,7 +806,7 @@ public static class KernelEventQueueCompatExports
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -805,8 +919,29 @@ public static class KernelEventQueueCompatExports
return;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: thread=0x{KernelPthreadState.GetCurrentThreadHandle():X16} handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
}

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