Compare commits

..

473 Commits

Author SHA1 Message Date
ParantezTech 0666071f79 [Tests] Cover backward red-zone spans, direct memory no-space results and the mount info size 2026-10-09 01:27:52 +03:00
ParantezTech ee9660d81d [Loader] Patch red-zone sites from the previous instruction when every indirect branch is resolved 2026-10-09 01:27:52 +03:00
ParantezTech 28b2f5709d [Kernel] Add sceKernelSync
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-09 01:01:48 +03:00
ParantezTech f7e8f54dbc [Stubs] Add sceTextToSpeech2Cancel and sceTextToSpeech2GetSpeechStatus
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-09 01:01:48 +03:00
ParantezTech afd0dffcf6 [SaveData] Write the mount info structure at its 48-byte guest size 2026-10-09 01:01:48 +03:00
ParantezTech cde859c5e5 [Tests] Cover random devices, POSIX open, UDS initialization, pad class data and exception contexts
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:41 +03:00
ParantezTech 757567bd15 [Pad] Add scePadDeviceClassParseData
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:41 +03:00
ParantezTech 62bbf51bbc [Pad] Write the device class extended information at its guest size
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech c1a0da742f [NP] Reset Universal Data System state with the runtime
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech ad0fd8be7b [NP] Create Universal Data System contexts before explicit initialization
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech 6f2937936a [NP] Keep Universal Data System handles across repeated initialization
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech 13b77eedcb [Cpu] Expose the guest exception context writer to tests
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech a7bfef97ac [Cpu] Report the flags and Sony stack alias in guest exception contexts
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech 10254a3c7e [Cpu] Place the guest exception mcontext at the start of the context
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech 3d65921eb7 [Kernel] Report random device descriptors in fstat
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:40 +03:00
ParantezTech cd77a68c87 [Kernel] Close random device descriptors
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech a662295409 [Kernel] Read random device descriptors from the host RNG
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech 4d8f5a75d5 [Kernel] Open /dev/random and /dev/urandom as host RNG descriptors
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech d3deab28aa [Kernel] Return POSIX results from _open and keep sceKernelOpen raw
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech 8c0ca1a2a7 [Kernel] Clear the outputs and report no space when no direct memory span is free
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 23:44:39 +03:00
ParantezTech 9d530f63f9 [Tests] Cover controller ports, user exports, trials and open-file renames
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 18:25:29 +03:00
ParantezTech f945215935 [Kernel] Allow renaming and unlinking open guest files
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 18:25:29 +03:00
ParantezTech c37f76f12d [NP] Expose the game trials flag 2026-10-08 18:25:28 +03:00
ParantezTech 73666f529e [UserService] Add user number and termination exports
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 18:25:27 +03:00
ParantezTech 0e55fa924c [Pad] Track independent standard, special and remote port handles
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 18:25:26 +03:00
ParantezTech d3f584a2f1 [Tests] Match red-zone coverage to forward-only patching 2026-10-08 18:25:26 +03:00
ParantezTech dd6a936a12 [Tests] Cover Gen5 tiling, DCC clears and offline authentication
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:32:02 +03:00
ParantezTech c0028d304b [GPU] Materialize DCC slice clears from guest metadata
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:32:01 +03:00
ParantezTech ef10b4a1d8 [GPU] Track DCC metadata coverage for color targets and volume textures
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:32:00 +03:00
ParantezTech 7be8a4b389 [GPU] Decode DCC fixed and register clear values
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:31:58 +03:00
ParantezTech 1984901a31 [GPU] Use Gen5 render-target tile equations on the CPU and GPU
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:31:57 +03:00
ParantezTech 7b30868b03 [SpirvTranslator] Preserve F32 signed zero, infinity and NaN when supported
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:31:37 +03:00
ParantezTech dc3b1d305d [Loader] Report patching progress for large executables 2026-10-08 17:31:36 +03:00
ParantezTech 7866bd4217 [Loader] Preserve switch targets while patching guest red-zone accesses
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:31:35 +03:00
ParantezTech 30429400ed [PlayGo] Add install and optional chunk queries
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:30:10 +03:00
ParantezTech 78792d7a5a [PlayGo] Expose runtime state reset
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:30:09 +03:00
ParantezTech 7c08757173 [PlayGo] Keep chunk metadata parsing in the metadata loader
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:30:07 +03:00
ParantezTech 2a2ea5a35a [PlayGo] Keep unknown chunk lists empty
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:29 +03:00
ParantezTech 34e8900035 [Pad] Trace input state changes when requested 2026-10-08 17:28:28 +03:00
ParantezTech 923f75d340 [Pad] Add the n3kSX62fgNo compatibility export 2026-10-08 17:28:27 +03:00
ParantezTech 946bd0a777 [Json] Add the special-float format setter
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:26 +03:00
ParantezTech d31138fcca [Json] Track the global special-float handler
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:24 +03:00
ParantezTech 077c9a940f [Network] Add Ethernet address string conversion
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:23 +03:00
ParantezTech ea1dcbcea1 [Network] Close POSIX socket descriptors through sceNetSocketClose
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:22 +03:00
ParantezTech 53f233da9b [Network] Add socket epoll registration and readiness waits
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:28:05 +03:00
ParantezTech b368247d64 [SpirvTranslator] Match guest F32 denormal and trigonometric semantics
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:59 +03:00
ParantezTech c566dba5a1 [VideoOut] Add deduplicated volume texture binding traces 2026-10-08 17:26:58 +03:00
ParantezTech 8d884f2692 [GPU] Skip piece hashing for stencil association proxies 2026-10-08 17:26:56 +03:00
ParantezTech 679ea0f355 [Kernel] Warn when APR cannot resolve a resource path 2026-10-08 17:26:55 +03:00
ParantezTech 124dfa8afd [Np] Complete offline authentication requests as signed out 2026-10-08 17:26:54 +03:00
ParantezTech 1b66707daf [SharePlay] Report inactive share-play state
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:53 +03:00
ParantezTech 619ac2b367 [Network] Add the RUDP context lifecycle
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:52 +03:00
ParantezTech a1c0d7d36d [Videodec2] Decode guest access units into pitched NV12 frames
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:51 +03:00
ParantezTech 6cc753e0fc [AppContent] Report missing add-on content entitlements
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:50 +03:00
ParantezTech beedb746d9 [Kernel] Expose the process argument count and vector
Co-authored-by: Sekai <140012992+SekaixX@users.noreply.github.com>
2026-10-08 17:26:49 +03:00
ParantezTech 82c9731f74 [Libc] Add file-lock compatibility exports 2026-10-08 17:26:48 +03:00
ParantezTech d708227541 [Cpu] Prefer LLE for libc FILE-object exports 2026-10-08 17:26:47 +03:00
Berk b02c06181a
Unity fixes 1016p2 (#1062)
* [Audio] Snapshot AudioOut2 PCM when the guest submits it

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Drop the ports of a destroyed AudioOut2 context

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Accept sceAudioOut2MasteringSetParam

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Resolve AudioOut2 out-buffers from either ABI slot

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Report the AudioOut2 context memory size through its out pointer

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Keep the AudioOut2 queue level and availability pointers apart

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Report full volume in the AudioOut2 port state

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Trace AudioOut2 port, attribute and bed calls

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Let tests replace the AudioOut2 host streams

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Audio] Add the AudioOut2 vibration port type

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [SaveData] Lay out save data events with their title ID

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [SaveData] Add sceSaveDataBackup

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [SaveData] Add sceSaveDataDirNameSearch2

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Network] Track Http2 template and request options

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Network] Add Http2 option, cookie and request exports

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [CommonDialog] Add the web browser dialog lifecycle

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Json] Add the InitParameter2 base constructor

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Json] Add InitParameterRtti2::setAllocatorRtti

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [AvPlayer] Accept available bandwidth hints

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Tests] Cover the AudioOut2, save data, Http2 and dialog exports

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

---------

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-07 18:47:42 +03:00
kuba e1edda7772
Revert the 18 Silent Hill rendering and performance merges (#1065)
Restore review-only delivery at the user request. Reverts #1024, #1025, #1031, #1032, #1033, #1034, #1035, #1036, #1037, #1038, #1039, #1040, #1041, #1051, #1054, #1055, #1058, and #1059. The resulting tree matches 3e2ad633 exactly.
2026-10-07 12:15:45 +02:00
kuba 775141d58c
[GPU] Sweep CPU writes at memory visibility points instead of before every device-address program (#1033)
* [GPU] Sweep CPU writes for device-address programs at memory visibility points

A device-address program may read any mapped page, so before each such
draw or dispatch the buffer cache uploaded every CPU-dirty page whenever
the global CPU-dirty epoch had moved. A guest that streams its per-draw
ring while the command stream is being translated moves the epoch between
almost every pair of draws, and each upload ended the rendering scope and
drained the GPU with an ALL_COMMANDS barrier pair - about 670 times per
frame in Silent Hill: The Short Message's menu.

CPU writes only need to be visible at memory visibility points: when a
submission slice starts, when a wait packet is satisfied, and after a
command-processor memory write (later commands may read those bytes through
device addresses). The sweep now runs at the first device-address program
after such a point, and its uploads share one barrier pair instead of one
per buffer. SHARPEMU_BDA_SWEEP_EVERY_DRAW=1 restores the per-draw sweep.
Menu GPU time drops from about 435 to 290 ms per frame.

(cherry picked from commit fafe92b71271cf750dd89251891bac20fb87f1ff)

* [GPU] Let command-processor DMA writes turn pages CPU-dirty instead of copying

A DMA fill or copy into memory without GPU-written bytes is executed on
the CPU, and the bytes were then pushed into every overlapping cache
buffer through a staging copy and a barrier pair, which ends the
rendering scope (~60 per frame in Silent Hill: The Short Message's menu).

When no GPU-written byte shares the written pages, the pages now simply
turn CPU-dirty: every later GPU reader synchronizes CPU-dirty pages before
reading (bound buffers on obtain, device-address programs at the next
memory visibility point, which the write now raises), so the bytes still
arrive in command order. Pages that hold GPU-written bytes keep the
staging copy. SHARPEMU_DEFER_HOST_WRITES=0 restores the copy.

(cherry picked from commit 3fe21faa3821551fa99b9398064f0652e45d2e84)

* [GPU] Keep hot CPU-written pages writable across device-address sweeps

A page the guest rewrites every frame turns hot after two upload/write
cycles, and the bound-buffer path then serves it from the stream ring so
the page stays writable. The device-address sweep and the range
synchronization still re-protected hot pages after every upload, so the
guest's next write to each ring page took a protection fault - about
5,000 faults per second on Silent Hill: The Short Message's render
thread, each a signal round trip under Rosetta plus page-guard lock
traffic shared with the command worker.

Both sweeps now preserve hot pages: they stay dirty and writable and are
copied again on the next sweep, a few hundred KiB of ring memory per
sweep. Faults drop to a third and the menu goes from 3.8 to 4.2 FPS.
SHARPEMU_BDA_SWEEP_REPROTECT_HOT=1 restores the re-protection.

(cherry picked from commit 65d2b55ddc07644d52e5ad3112cbe323a2421abd)

* [GPU] Refresh writable hot pages at every BDA visibility point
2026-10-07 11:52:12 +02:00
kuba 38f7196470
[GPU] Add internal render resolution scaling (#1038)
* [VideoOut] Add an internal render resolution scale option

Adds the host-side setting an internal-resolution implementation needs:
--render-scale on the command line, SHARPEMU_RENDER_SCALE for scripted
runs, and a global and per-game choice in the GUI. The value is a host
resolution multiplier of the guest render targets, normalized to a
hundredth in [0.25, 2] so the command line, the GUI and the environment
agree bit for bit; the renderer compares scales when it decides whether
two views of the same guest memory match.

RenderScalePolicy holds the decision rule. It is a pure function of the
guest image description, so every view, alias, copy and descriptor of
one guest range reaches the same answer without consulting the cache.
Only screen-class, single-level, single-sample, uncompressed 2D render
and depth targets scale; block grids, multisample footprints, HTile and
DCC coverage, mip chains, cube and array faces, asset tilings and small
surfaces (shadow maps, lookup tables, eye-adaptation readbacks) keep
their guest resolution. The environment variable wins over the stored
setting so test runs do not depend on the user's configuration.

No renderer consumes the scale yet; a scale of one is the current
behaviour everywhere.

* [GPU] Hold scalable guest images at a host render resolution

The backing of an image the scaling rules accept is created at
ceil(guest * scale) while its ImageDescription keeps the guest geometry,
so the cache, the overlap rules and the page tracker go on reasoning in
guest bytes.

Everything that moves guest bytes now goes through a guest-resolution
twin of the scaled image: an upload fills the twin from the copy regions
the tiler built and blits into the backing, a download blits the backing
into the twin and copies out of it. A copy whose two images disagree on
host resolution runs at guest resolution the same way, so callers keep
describing rectangles in guest pixels; the destination seeds its twin
first because a partial copy is blitted back whole. The multisample
depth conversion takes a guest-sized source for the same reason, and the
resolve region crosses into the destination's resolution instead of
silently resolving the top-left corner.

Two fallbacks keep correctness ahead of the saving. A format that cannot
blit keeps its guest resolution, since the twin could never reach it.
A storage image drops back to guest resolution the first time a compute
shader binds it: the dispatch grid does not shrink with the host image,
so the shader would write it with holes above 1.0 and redundantly below.
The guest range is remembered, so every later image over that memory is
created unscaled too.

* [GPU] Scale the render area, viewport and scissor with the attachments

A resolved target now reports the host extent of its attachment beside
the guest one, and the three places that size rasterization follow it:
the dynamic rendering area, which also bounds every load-op clear; the
guest viewport transform; and the guest scissor, whose edges round
outward so no guest pixel the game asked for loses host coverage.

All attachments of a pass share one rule, so the first bound target
decides the factor.

The clip-disable path needs care because the vertex program bakes the
viewport transform against a reference extent. Dividing that reference
by an upscale keeps the host viewport (reference x scale) inside the
device limit while the baked transform stays scale independent, so the
same program maps guest pixels onto host positions at either resolution.
At a scale of one the reference is the device limit, exactly as before.

* [SpirvTranslator] Translate pixel positions and image coordinates across the render scale

A program expresses its pixel position, its integer image coordinates
and the sizes it queries in guest pixels, but the host may hold the
image behind any of them at another size. Which images are scaled is a
per-draw fact, so the factors travel in shader data rather than in a
permutation: the coordinate factor, its reciprocal, a 64-bit mask of
scaled image resources and the factor the pixel position is divided by.

With that:

- gl_FragCoord is divided by the attachments' factor as it is loaded, so
  POS_X/POS_Y, POS_FIXED_PT and everything derived from them - screen
  UVs above all - stay in guest pixels.
- An integer image coordinate is mapped onto host texels through the
  bound image's factor. Only the two scaled axes move; an array layer or
  a volume slice is not a pixel.
- A resource-info query reports the guest size, because the program
  derives coordinates and loop bounds from it.

A select against the mask makes an unscaled binding cost one
instruction, and the layout only carries the dwords when the scale is
not one, so nothing changes at native resolution.

* [GPU] Keep one host resolution across the attachments of a pass

A rendering scope has one render area, one viewport and one scissor, so
its attachments have to agree on a host resolution. They disagree when
one of them cannot be scaled at all - a depth target with a stencil
plane next to a scalable colour target, say - and the pass then renders
a fraction of its area. The draw now drops every scaled attachment back
to guest resolution and resolves its targets again; the guest ranges are
remembered, so the decision converges after one draw and cannot
oscillate.

That fallback only works if the executor knows what the cache actually
gave it, which is not what the scaling rules would answer: a demoted
range still looks scalable to them. The resolved targets therefore carry
the image's own factor, and the render area, viewport, scissor and the
depth/stencil copy extent read it from there.

Two rules change with it. DCC only describes guest bytes the host never
stores, so a colour target that carries it can still be scaled; HTile
encodes guest depth blocks the renderer reads back as clear state and
stays native. And a storage image only drops back when the program
actually writes it, since a read-only load is mapped correctly.

Scaling now also requires a backend that installed the format
predicate - the Vulkan device - so the Metal host is unaffected.

* [Docs] Describe internal resolution scaling and its limits

Replaces the note saying arbitrary internal resolution scaling is not
implemented: it is, for screen-class render and depth targets. Records
what the renderer translates (rasterization, shader coordinates, guest
transfers, presentation), the exact rule for what scales, every case
that falls back to guest resolution, the artefacts measured on Silent
Hill, and the per-scope GPU times at 50, 100 and 200 percent.

* [SpirvTranslator] Preserve guest texel offsets at scaled resolutions
2026-10-07 11:47:58 +02:00
kuba 8acc081205
[GPU/HLE] Cut per-draw and per-call managed allocations on hot paths (#1054)
* [GPU/HLE] Cut per-draw and per-call allocations on hot paths

* [HLE] Keep nested tracker uploads from sharing rollback scratch
2026-10-07 11:36:27 +02:00
kuba 98f223ce69
[GPU] Restore scene visibility and half-wave shuffles (#1025)
* [Agc] Emit addressed draw events for occlusion query counters

* [Gpu] Keep fallback occlusion results coherent and conservatively visible

* [Gpu] Honor disabled color buffers during depth-only draws

* [Gpu] Upload complete unchecked constant vertex attributes

* [Agc] Emit direct draws for every draw modifier

sceAgcDcbDrawIndex and sceAgcDcbDrawIndexAuto returned without writing a
packet unless the modifier was exactly 0x40000000. The modifier names the
shader stage and user registers of the bound pipeline, so any other
pipeline lost its draws without a trace. Unreal bakes its color-grading
LUT through a geometry-stage pipeline, which left the LUT unwritten and
tinted every 3D frame of Poppy Playtime blue.

Both draws now emit their packets for any modifier. Modifiers other than
the vertex-stage default are logged once each.

(cherry picked from commit 8d46a6068b7d1a999662eeac1ee17f9e6d87c2eb)

* [VideoOut] Separate guest display mode from presentation buffer dimensions

* [GPU] Map half-wave shuffle lanes into the host subgroup

* [Docs] Explain display modes and internal resolution limitations

* [GUI] Translate guest display mode settings in all supported languages
2026-10-07 11:21:15 +02:00
kuba 8b631d3b03
[GPU] Avoid command-worker readbacks for clean bytes on GPU-dirty pages (#1059)
* [GPU] Read clean command bytes without faulting on neighbouring GPU data

* [Tests] Enable portable Vulkan devices and release fixture slabs
2026-10-07 11:21:09 +02:00
kuba a9356eb8d2
[GPU] Read guest buffers back from unified memory without a copy (#1058)
A CPU read of GPU-written guest memory recorded a copy at the tail of
the current command buffer and waited for it, so it also waited for
every draw recorded after the writer. In Silent Hill's title menu most
of those reads targeted buffers whose writer had retired long before,
and UE's render thread spent a third of its time in them.

On a device with device-local, host-visible, coherent memory (Apple
GPUs), guest buffers are now allocated there. A readback whose writers
retired copies straight out of the buffer's mapping; one whose writer
is submitted but still running waits for that submission alone and then
copies; the command worker submits a writer that is still recording
and reads after it. A guest-thread wait also marks its window hot, so
the next GPU write there is submitted right after its command.
SHARPEMU_UNIFIED_BUFFERS=0 keeps device-local buffers.
2026-10-07 11:02:22 +02:00
kuba a73e424a27
[macOS] Fix the Rosetta GC wedge, a CPU-flip buffer-wait deadlock and frozen host movies (#1051)
* [CLI] Restart under Rosetta with a 64 MiB gen0 GC budget

Every collection suspends the runtime through FlushProcessWriteBuffers,
which Rosetta 2 services by parking each thread in its runtime
(notify_thread_waiters). A signal that reaches a parked thread trips
Rosetta's "expected saved LR to be in translated code" assertion and
wedges the process; Demon's Souls hit it in about half of its 3-5 minute
runs while collecting almost twice a second. The gen0 budget is only read
from the environment, so the process re-executes itself once with
DOTNET_GCgen0size set (same pid). With 64 MiB, Demon's Souls collects
about once every 6 seconds, gen0 and gen1 only, 0.26 s of pauses in
110 s. A 512 MiB budget made every collection a blocking gen1/gen2 pass
of about a second. SHARPEMU_ROSETTA_GC_BUDGET=0 opts out.

* [VideoOut] Let a buffer wait ignore CPU flips the graphics queue has not reached

sceVideoOutSubmitFlip reserves its flip request, may sleep for pacing,
and only then queues the flip preparation on the graphics queue. A
command buffer submitted before it that starts with
sceAgcDcbWaitUntilSafeForRendering on the same display buffer saw the
reserved request as pending and waited for a preparation queued behind
itself: the graphics queue never advanced and every compute queue waiting
on its labels stalled with it. Demon's Souls hung this way on the boot
splash in about one run in three.

A CPU flip now counts for the wait only once the graphics queue runs its
preparation; GPU flips are ordered from their packet.

(cherry picked from commit 8915df35abad5fe8b15cc6418f1e563785d6e008)

* [Media] Expire a host movie only once its playback stops advancing

Host playback follows the guest's own Bink decode and audio clock, so on
a slow host a movie plays slower than real time. The watchdog fired at
the movie's duration plus a minute regardless, marked the file timed out
and forced EOF on the guest's reads while its decoder was still
mid-stream: Demon's Souls' 171 s attract movie froze on its last
host frame for good. The watchdog now re-arms while frames keep
advancing and expires a movie only after a minute without progress.

(cherry picked from commit daad0f097d0c6369097aeef32b55c9f2d171110a)

* [CLI] Preserve the managed entry point when restarting under Rosetta
2026-10-07 11:02:17 +02:00
kuba a0fd9dcb83
[GPU] Restore half-float arithmetic and image readback state (#1024)
* [Gpu] Cover the whole tile plan when a buffer reads back an image

TrySynchronizeBufferFromImage sized the copy from the mip layout, but the
tile plan of a fully fitting image can extend past it; Silent Hill hit a
128x128 slice planned at the exact end of the mip chain and the tiler
aborted the process. Grow the copy to the plan's tiled end when the
buffer holds it, otherwise keep the guest bytes.

* [SpirvTranslator] Read half-selected V_FMA_MIX literals as f16

op_sel_hi marks a V_FMA_MIX operand as f16 for literals too, but literals
were always read as f32 bit patterns. Silent Hill's deferred lighting
weights a luminance with the f16 literals 0x34CD/0x38B8/0x2F0A (0.30, 0.59,
0.11); as f32 denormals they made the luminance zero, the shader clamped it
to 1e-4 and divided by it, and the title-menu scene came out thousands of
times too bright, which auto exposure then turned black.

* [Gpu] Materialize every fixed DCC clear code on a sampled image

A DCC fast clear stays pending until the surface binds as a colour target;
when a shader samples the surface first, only the clear-to-zero code was
materialized. The other fixed codes (alpha 1, rgb 1, rgba 1) carry their
own colour too. Silent Hill fast-clears UE's separate-translucency target
to (0,0,0,1) and only samples it, so the post composite read stale zeros
and the whole title-menu scene came out black.

* [SpirvTranslator] Keep wave64 LDS phase barriers when a half-mask plan applies

The half-mask plan's barriers order its own mask exchanges, not the
program's shared-memory traffic, yet planning a program switched the LDS
phase ordering off. UE's FFT bloom (wave64, 512 threads, LDS butterflies)
then read half-written rows: Silent Hill's convolution output collapsed
into a flat haze (max 56 for a scene peaking above 1500) and the histogram
fed by it never adapted exposure.

* [Gpu] Publish linear GPU-written images to guest memory by default

Unreal reads its eye-adaptation result back on the CPU through a linear
staging texture (FRHIGPUTextureReadback) and derives View.PreExposure from
it. The image cache was built with linear readback disabled, so the CPU
always read zeros, PreExposure stayed 1.0 and the scene rendered without
pre-exposure. FFT bloom's bright-pixel prefilter, which works in
pre-exposed space, then flattened every pixel to its max luma and the
Silent Hill title menu came out nearly black. SHARPEMU_READBACK_LINEAR_IMAGES=0
restores the old behaviour.

* [Tests] Enable MoltenVK portability devices in headless harness

* [SpirvTranslator] Saturate V_CVT_PKRTZ_F16_F32 at 65504 instead of overflowing to infinity

Rounding toward zero never produces infinity from a finite value: past the
f16 range the result is +-65504. We truncated the mantissa and let
packHalf2x16 round, so large values became infinite. Silent Hill's deferred
lighting packs title-menu values past 65504; its height fog then multiplies
scene color by a tiny transmittance, which keeps 65504 finite but leaves an
infinity infinite, a later pass turns it into NaN and the whole 3D menu
background renders black. Subnormal results now also keep whole 2^-24 steps.
A MoltenVK device test covers overflow, truncation, subnormals, NaN and inf.

* [Tests] Wait for the blocked worker before publishing its signal
2026-10-07 10:59:57 +02:00
kuba 4e6d1e0d9a
[HLE/Cpu] Remove host lock contention from POSIX memory queries, thread resumes and timed waits (#1031)
* [HLE] Answer POSIX host memory queries without the mapping lock

Every HLE import that reads stack arguments queries the guest stack's
protection first. On POSIX the query took the same lock Protect holds
across its mprotect call, and GPU write tracking protects pages
constantly, so each query waited tens of microseconds; Silent Hill: The
Short Message spent up to a second of every five seconds in queries.

Regions are now also published as an immutable snapshot (replaced under
the lock when a region is added or removed), and a query of untracked
memory or of a region without per-page overrides is answered from it
without the lock. Regions with overrides keep the locked path, which now
finds where a protection run ends through an ordered set of overridden
pages instead of visiting every override (write tracking leaves tens of
thousands), and the next region by binary search. Query time drops from
200-1000 ms to 5-50 ms per 5 s.

This is the POSIX counterpart of replacing page-by-page VirtualQuery
scans on Windows.

(cherry picked from commit b6ffe31f28fa93842726b71315c7731f97b5fda8)

* [Cpu] Sleep through nanosleep, resumed on EINTR, in the POSIX Sleep stub

The POSIX stand-in for kernel32!Sleep (which the native sceKernelUsleep
intrinsic calls with milliseconds) converted to microseconds and called
usleep. macOS rejects usleep(>= 1 s) with EINVAL and returns at once, so
every guest sleep of a second or more came back immediately; and a
signal (the runtime's thread activations, for one) ended any sleep early
through an error the stub never looked at.

The stub now builds a timespec and calls nanosleep, and when nanosleep
returns EINTR it resumes with the remaining time. A test exercises the
stub with the Win64 argument register for both a 1.2 s and a 20 ms
sleep.

(cherry picked from commit 43b770f86b5e3aa2332121f1bc2ddf2acc9d2968)

* [HLE] Query POSIX view regions from a lock-free snapshot

Every HLE import first queries the guest stack's protection, and the
view-region query took the same gate the GPU write tracker holds across
its mprotect calls, so each import waited on page-protection traffic
from other threads (the main guest thread spent ~2% of its time in that
wait at Silent Hill: The Short Message's menu).

The region list is now also published as an array snapshot whenever it
changes, and queries read the snapshot without the gate. Per-page
protections already changed in place and were read racily; nothing
changes there.

(cherry picked from commit d7e72cb4974388883696516420b2e9d462778715)

* [Cpu] Skip the scheduler gate when a host thread re-registers the same guest context

scePthreadSelf on a guest thread the scheduler does not own registers
the thread's context on every call, and that took the scheduler-wide
gate each time - tens of thousands of times a second on Silent Hill:
The Short Message's main thread, contending with the dispatcher and
every blocked-thread wake.

Each host thread remembers the handle and context it registered last;
a repeat registration only refreshes the external-handle hint and
returns. New or changed registrations still take the gate.

(cherry picked from commit d1991b2b75261cd1f1ccb4e005b10805f0da24c1)

* [Cpu] Keep each thread's guest continuation stub instead of mapping one per resume

Every resume of a blocked guest thread allocated a 512-byte executable
stub page and a host-RSP slot, and released both when the thread blocked
again: an mmap/munmap pair plus two acquisitions of the host-memory gate
per resume, which the GPU write tracker holds across its own protection
changes. Contended on that gate, resumes were 62% of all lock waiting in
Silent Hill: The Short Message's menu.

Each host thread now keeps a stub page and slot per nesting depth for its
lifetime; a resume reopens the page for writing with mprotect directly
(the page is emulator-private, so it bypasses the tracked host-memory
layer), re-emits the stub and seals it again. Lock waiting across threads
halves (13.6 s to 6.5 s per 30 s).

(cherry picked from commit 2c2293abdd5d19dd1be3d6941dcedf9b482c6420)

* [Kernel] Round host condvar waits up to whole milliseconds

Monitor.Wait truncates its timeout to milliseconds, so a timed wait with
less than 1 ms left became a zero timeout and spun until the deadline.
UE's game thread waits for the render fence in 1 ms slices, which kept a
host core busy for the whole wait and contended the condvar lock with the
signaling threads. Timed waits may wake late, so round up instead.
SH menu 4.29 -> 4.64 fps together with the readback retry change.

(cherry picked from commit 66eaa0095fe594ff8e285cbf31c32bd84274ac85)

* [Tests] Run the POSIX Sleep stub test only in x64 processes

The stub is x86-64 machine code; a native arm64 test host cannot map or run
it, so the macOS arm64 CI runner failed to allocate the stub page.
2026-10-07 10:56:03 +02:00
kuba d809f84491
[SpirvTranslator] Share one EXEC test across a run of vector ALU writes (#1040)
A vector register write is modelled as select(EXEC, result, old value), so the
old value of every register the shader writes stays live across the write. That
guard is the translator's largest per-instruction cost: dropping it altogether
(wrong results) takes Silent Hill's volumetric-fog compute from 13.43 to 10.64
ms, and the exec-full analysis can only prove 194 of its 875 instructions run
with every lane set.

A run of instructions whose only effect is their vector destination can instead
run inside one if (EXEC). The masked-off lanes skip the whole run, which is what
the hardware does to them anyway, and only the join needs the old values. A run
stops at anything the wave runs even where this lane is masked off: SALU is not
EXEC masked and every invocation keeps its own copy of the scalar registers, a
compare writes VCC or EXEC (GFX10 keeps that destination in the instruction's
control, not in its destination list), a cross-lane read needs its neighbours to
have run, and LDS, memory and the DPP encodings reach outside the register. Six
instructions is where the branch starts paying for itself.

Compute only: the same grouping made the deferred lighting pixel program 28%
slower (11.03 -> 14.11 ms per scope), where a wave already runs with helper
lanes. SHARPEMU_EXEC_RUN=0 turns it off, SHARPEMU_EXEC_RUN_MIN sets the length.

Silent Hill title menu, same build, SHARPEMU_EXEC_RUN=0 vs on:
  offline replay min ms   fog D5DF9DC46C4E35EA  13.42 -> 12.21 (-9.0%)
                          FFT 674E5A7163897BC9  16.37..17.38 -> 17.06 (flat)
                          NGG B482CD3A01A37F0F   0.494 ->  0.492 (unchanged)
  in-game ms per dispatch fog D5DF9DC46C4E35EA  16.59 -> 15.34 (-7.5%)
                          FFT 674E5A7163897BC9  13.81 -> 13.77
                              2BFB24830D8FCFAA   4.78 ->  4.58 (-4%)
  in-game ms per scope    848AFAD7/9D180B8A     11.03 -> 11.36 (unchanged SPIR-V)
                          F987C1A4/379BE850      1.90 ->  1.72 (unchanged SPIR-V)

spirv-val passes on the fog, FFT and NGG modules; the menu image is unchanged
with Anita present.
2026-10-07 10:52:01 +02:00
kuba da0953845b
[SpirvTranslator/Vk] Cheaper guest buffer loads and f16 conversion, gated on device probes (#1035)
* [SpirvTranslator] Load dword-aligned 32-bit formatted buffer components as words

A formatted untyped buffer load decodes the descriptor format at run time,
reading every component byte by byte through the generic layout select
chain. For dword-aligned 32-bit data formats (32, 32_32, 32_32_32,
32_32_32_32) with a raw-bit number format (UINT, SINT, FLOAT) a component is
one buffer word; that branch loads it directly and applies the same swizzle
and bounds rules. The descriptor is uniform, so the branch does not diverge.
UE GPU-skinning vertex shaders fetch bone data this way; Silent Hill: The
Short Message's menu goes from 1.3 to 2.0 FPS.

(cherry picked from commit 20540cd153855964d817daf065de3e243fd547bd)

* [SpirvTranslator] Convert f16 with the native pack/unpack when the host measured it exact

Every packed-f16 (VOP3P) lane widens its operands to f32 and rounds the
result back, and both conversions were the branchless integer sequences in
EmitHalfToFloat / EmitFloatToHalf: about twenty scalar ops each, because
GLSL UnpackHalf2x16 / PackHalf2x16 have no guaranteed rounding or subnormal
behaviour without float-controls execution modes. A device capability bit
does not settle it either - MoltenVK on Apple silicon reports
shaderRoundingModeRTEFloat16 and shaderSignedZeroInfNanPreserveFloat16 yet
not shaderDenormPreserveFloat16 - so only a measurement can.

ShaderCompileRequest.NativeHalfConversionExact, which the host sets only
after measuring the two ext instructions against these sequences on the
running device, swaps both helpers for the single ext instruction the
driver lowers to one hardware convert. It defaults to false, so an
unmeasured device keeps the exact emulation.

Silent Hill f2 volumetric fog dispatch: 30.7 -> 21.9 ms per dispatch in
replay, output unchanged.

(cherry picked from commit 10f76eeac0d4306172e41ecff86b737f6a3c9634)
(cherry picked from commit 7f18cc96ae58ac5fd47da6562c4ba3abe22ccb4a)
(cherry picked from commit 972f658033f8339effe42c5d305af87bcbab93f0)

* [Vk] Probe f16 conversion at device setup and report the verdict

A one-time compute pass at device setup measures the device's GLSL
UnpackHalf2x16 / PackHalf2x16 against the translator's own f16 conversion
before any guest shader is compiled, and only a clean measurement lets the
translator use them (ShaderCompileRequest.NativeHalfConversionExact). The
vectors cover all 65536 f16 bit patterns, every f16 value widened to f32,
the midpoint between adjacent f16 values and that midpoint one f32 ulp
either way, the overflow edge, signed zeroes, infinities, NaNs and f32
subnormals, and each is measured in both directions: 643200 comparisons.

Every finite value, zero and infinity must be bit-exact, sign included. A
NaN only has to stay a NaN, because neither side defines its payload: the
emulation picks sign | 0x7C00 | 0x200 narrowing and carries the f16 payload
widening, while the measured hardware convert canonicalizes to a positive
quiet NaN. On an Apple M4 that is the only disagreement - 2314 of 643200,
every one a negative NaN whose sign the hardware cleared - and f16
subnormals convert exactly despite shaderDenormPreserveFloat16 being false.

The probe never fails device setup: any error leaves the exact emulation in
place and says so on the reported line. SHARPEMU_NATIVE_HALF=0 forces the
emulation, =1 takes native without measuring. The result is one line:
  [VK][F16] native half conversion exact=True mismatches=0 tested=643200

The flag reaches the translator through IShaderPipelineHost, so it is
process-constant and cannot mix two translations of one program; the saved
driver pipeline cache keys its entries by the module it was given, and the
Metal host does not probe and keeps the emulation. Offline replays run the
same probe, so a case translates the way the game would.

(cherry picked from commit 6a343683d890b4c6d123c84fdacb41e718ec4c19)
(cherry picked from commit 65ca663aa2bfb1fcc69709c73ab6189b07ead3c1)
(cherry picked from commit 003b1216bd311e5c5fb1d48ca01a66efa3fc51de)

* [SpirvTranslator] Read an unaligned buffer word as a funnel shift of two words

The byte-wise decode of a formatted buffer load read each byte of a component
from its own bounds-checked word load: four loads, four range tests and a byte
assembly for a value that spans at most two words. One funnel shift of those
two words is the same value, including out of range, where a word reads zero on
both paths and so contributes zero bytes. Every sub-dword buffer load goes
through the same helper.

The decode is the branch a formatted load takes when its address is not dword
aligned, so its size also sets the register pressure of the aligned branch next
to it. Silent Hill: The Short Message's volumetric fog dispatch
(0xD5DF9DC46C4E35EA, 8 formatted loads in its light loop) goes from 8.68 to
7.28 ms in offline replay, and the module from 394 to 360 KiB. An experiment
that dropped the byte-wise branch entirely measured 7.35 ms, so the funnel
shift is worth all of what the branch cost.

(cherry picked from commit 0c996ffab38144d795517e68cb2ca9d8d48e8988)

* [SpirvTranslator] Load a guest buffer word unchecked where the device zeroes out-of-range reads

Every guest buffer word is read through a range test, an address clamp and a
zero select: three operations whose only purpose is to produce the zero the
hardware returns past the end of a V#. A device with robustBufferAccess2 that
honours it returns that zero itself, so the three are pure overhead there -
dropping them measured about 12% of Silent Hill's volumetric fog dispatch, 482
checked words in one program.

ShaderCompileRequest.ZeroOutOfBoundsBufferReads turns them off and defaults to
false: an out-of-range read is undefined without the promise, so only a host
that measured the device may set it. It is set from a device probe, never from
a capability bit, because MoltenVK runs on Metal, which has no hardware bounds
checking.

(cherry picked from commit 2ba626a0ac7157f93f348cfe7573ff1092762d8c)

* [Vk] Probe out-of-range buffer reads at device setup and report the verdict

A one-time compute pass binds 1 KiB of a 4 KiB allocation that carries a
sentinel everywhere and reads all 4 KiB back through the descriptor: the words
inside the range must still read their sentinel and the words past it must read
zero. Only a clean measurement lets the translator drop the range test, address
clamp and zero select it puts around every guest buffer word
(ShaderCompileRequest.ZeroOutOfBoundsBufferReads).

The probe only runs where the device enabled robustBufferAccess2, because an
out-of-range read is undefined without it and can fault the device; everywhere
else it reports the reason and the checks stay. On this Apple M4 through
MoltenVK the feature is not supported at all, so nothing changes:
  [VK][OOB] out-of-range buffer reads zero=False mismatches=unprobed tested=0
      note=robustBufferAccess2 is not enabled on this device

SHARPEMU_ZERO_OOB_READS=0 keeps the checks, =1 drops them without measuring.
The flag reaches the translator through IShaderPipelineHost, so it is
process-constant and cannot mix two translations of one program.

(cherry picked from commit e81d924349042acc1122a66d4c767268bd6ef2a4)
2026-10-07 10:51:57 +02:00
kuba db32c9d882
[SpirvTranslator] Cut emulated-wave64 and private-LDS overhead in translated shaders (#1034)
* [SpirvTranslator] Keep pure scalar lane spills in private slots under emulated wave64

A wave64 program run as a 64-thread workgroup on a 32-lane host emulates
v_readlane with an LDS exchange and a workgroup barrier. Compilers use
constant-lane v_writelane/v_readlane pairs on a VGPR as SGPR spill slots;
when that VGPR is written only by constant-lane writelanes and read only by
constant-lane readlanes, each (vgpr, lane) pair is a private scalar, so the
readlane loads the slot instead of exchanging through LDS. This halves the
heaviest compute dispatch in Silent Hill: The Short Message's menu.

(cherry picked from commit 9915b88e68b6b69d6b74ea931a22eb0589c56f1a)

* [SpirvTranslator] Zero private LDS lazily through a written-dword bitmap

Graphics stages model LDS as a per-invocation private array that must
read zero where the invocation has not written. The array carried a zero
initializer, but Metal keeps a dynamically indexed private array in stack
memory, so every invocation cleared all of it: an NGG vertex shader (the
PS5 default, with in-shader culling through LDS) cleared 8 KiB per vertex.

The array is now uninitialized and a zero-initialized bitmap of written
dwords (1/32 of the size) records which slots were addressed; LdsPointer
zeroes a slot the first time it is addressed, so loads, stores and atomics
keep the zero-read semantics. Compute LDS stays workgroup memory.
Silent Hill: The Short Message's menu, dominated by ~45M shadow-pass
vertices per frame, goes from about 2.0 to 3.1 FPS.

(cherry picked from commit 5738b8574397cf2be08cd4e1518b33057c8867b5)

* [SpirvTranslator] Test the lane's mask bit in one register under emulated wave64

VCC and EXEC keep a per-lane boolean that every mask write refreshes, and the
refresh composed the 64-bit mask from its two registers, built a 64-bit lane
bit and masked them. A host without 64-bit integer arithmetic (Apple GPUs
through MoltenVK) emulates each of those with a pair of 32-bit operations. The
bit lives in exactly one of the two registers, so selecting that register and
shifting inside it is the same test in 32-bit arithmetic.

Only the emulated-wave64 path changes; wave32 already tested one register, and
a native wave64 host keeps the existing form, which also clears the bit for
lanes outside the guest wave.

(cherry picked from commit 1b2651047fdf232304142c82168e487244dda4ef)

* [SpirvTranslator] Let s_barrier publish only what the guest waited for

Every compute s_barrier became an OpControlBarrier with AcquireRelease
over uniform, workgroup and image memory, on the assumption that the
guest had drained vector memory first. s_barrier only orders execution;
what it publishes is whatever the preceding s_waitcnt drained. A barrier
that follows an LDS-only wait (lgkmcnt(0)) publishes LDS, and ordering
device memory there costs a cache flush the guest never asked for: UE's
FFT bloom convolution barriers 48 times per group, all LDS-only.

The translator now tracks, per block, whether any wait drained vector
memory - SOPP vmcnt(0), or the GFX10 SOPK vscnt(0)/vmcnt(0) forms, since
stores are counted by vscnt there - and a barrier after an LDS-only wait
carries workgroup memory only. A drained counter stays drained for the
rest of the block, and a barrier with no wait in its block keeps the
full semantics (the wait may be in a block this one cannot be ordered
against).

(cherry picked from commit 1f81df59bc07d05f2baa6283da95f4901dada444)
2026-10-07 10:51:52 +02:00
kuba 30321142cb
[GPU/Vk] Keep guest readbacks of GPU-written memory off the command worker (#1032)
* [Vk] Run async readbacks on a second general queue family when no dedicated one exists

The async readback queue was only taken from a family without graphics,
so drivers that expose several general families (each its own hardware
queue) kept every guest readback on the main queue and the command
worker. Any other family with transfer support now qualifies after the
dedicated transfer and compute families.

(cherry picked from commit 7b04d6185dfc1711f0fbdf5c75c842a065acd1fa)

* [GPU] Let guest reads of GPU-written memory wait on the main queue off the worker

A guest thread reading memory the GPU wrote hands the download to the
command worker, which recorded the copies, submitted and then waited for
the whole GPU before handing the bytes back - unless a dedicated readback
queue existed, where the download ran asynchronously and only the guest
thread waited. MoltenVK exposes one queue family in-process, so on macOS
every such read idled the worker for tens of milliseconds, several times
per frame in Silent Hill: The Short Message's menu (a quarter of the
worker's time).

Without a readback queue the worker now copies the pieces into a buffer
of their own, submits without waiting and returns a pending download
carrying the submission tick; the guest thread waits for that tick on the
timeline (safe from any thread) and completes the read back on the
worker, as the readback-queue path does. The worker's GPU wait time drops
to zero; SHARPEMU_MAIN_QUEUE_PENDING_READS=0 restores the synchronous
path.

(cherry picked from commit 6c9ad44bcea7f9396a7564621fcbda3841834052)

* [GPU] Re-issue a stale pending readback instead of reading synchronously on the worker

A guest read of GPU-written memory records a copy and waits for its tick
off the queue. When another GPU write lands first, the completion fell
back to a synchronous read on the command worker, which stalled it until
the GPU drained (up to half of SH's readbacks). The guest thread now
re-issues the pending read and waits again; only the fourth attempt falls
back to the synchronous read.

(cherry picked from commit 17bf5bb958ebfd4dd980dbf241cbe9e77fffd938)
2026-10-07 10:51:48 +02:00
kuba 1e784a035f
[SpirvTranslator] Keep the packed-half vector registers in one variable per register (#1041)
The f16 fast path shadowed the whole vector register file with a 512-entry
private array of vec2 and zero-initialised it, even though every access uses a
constant register number. That is the array shape Metal keeps in thread memory
instead of registers - the same trap V_MOVREL's dynamically indexed register
file hit (Octopath's instanced vertex shader, 112 -> 6.4 ms/draw) - and it also
left AMD with an unused 4 KiB .bss global that broke two-stage links. One
private variable per register, declared on first use, removes it.

308 of the 1019 shaders Silent Hill's title menu translates used the array,
including the two hottest pixel programs. The generated Metal for the
volumetric-fog compute D5DF9DC46C4E35EA drops from 282 KB to 269 KB and no
longer declares a 4 KiB thread array. Measured GPU time on the menu is
unchanged within run-to-run noise (fog 18.2/19.9 -> 20.1 ms per dispatch,
pixel 934F121D8F666572 0.048/0.061 -> 0.041 ms per scope), so this is a
code-shape fix that removes the spill risk rather than a measured win.
2026-10-07 10:49:01 +02:00
kuba 7ab6de1b93
[GPU] Cut per-draw command-worker cost in resource materialization and pixel input setup (#1039)
* [ShaderCompiler] Materialize directly for plans whose cached inputs never repeat

The materialization cache records every guest word a draw reads and keys
the result on the draw's user data. Most Silent Hill draws point their
user data at per-draw constants, so only 8% of lookups hit and every miss
still paid for recording the reads and storing an entry. A plan now
counts its lookups and hits; after 64 lookups with fewer than one hit in
eight it walks its resources directly and probes the cache again on every
64th draw. On the SH title menu the command worker's busy time per draw
fell from ~125 to ~111 us (cache hit rate of the remaining lookups 70%).

* [GPU] Scan a pixel program's interpolated attributes once per decoded program

Every draw scanned all instructions of its pixel program twice, once
through a LINQ chain with Distinct/Order/ToArray, to count interpolated
attributes and the vertex outputs they read. The sorted attribute set
is a property of the decoded program, so it is computed once and kept
beside it. Pixel input resolution on the SH menu dropped from ~14 us to
a few us per draw.
2026-10-07 10:48:57 +02:00
kuba 70dcbe4e05
[GPU] Keep sampled depth attachments in one rendering scope (#1037)
Draws that sample the depth attachment need its read-only layout; every
other draw got the writable layout, even one that only tests depth. A run
of depth-sampling particles between test-only draws flipped the layout on
every draw, and each flip ended the rendering scope. Image state tracking
also recorded a barrier whenever the access changed, even from one read
to another in the same layout.

A draw that neither samples nor writes the attachment now keeps the
read-only layout it is already in, and reads after reads in one layout
record no barrier: the tracked state accumulates every reader, so the next
writer still waits for all of them. Depth-transition scope breaks in
Silent Hill: The Short Message's menu drop from ~1250 to ~820 per 5 s.

(cherry picked from commit 5563417d72fadefb115b300291b1e019b3171466)
2026-10-07 10:48:52 +02:00
kuba 0aa611e317
[GPU] Draw non-indexed indirect draws from their arguments on the GPU (#1036)
Indexed indirect draws already let vkCmdDrawIndexedIndirect read their
counts, but a non-indexed DRAW_INDIRECT read its four arguments on the
CPU. When a compute pass had just written them (UE writes rectangle-list
draw arguments this way every frame), the read downloaded the buffer and
waited for the whole GPU: about one 20-50 ms stall per frame in Silent
Hill: The Short Message's menu.

The interpreter now hands the argument address to the host when it draws
non-indexed indirect draws on the GPU (deferring the instance count read
as the indexed path does), and the executor records vkCmdDrawIndirect.
The guest layout (vertexCount, instanceCount, firstVertex, firstInstance)
is VkDrawIndirectCommand. Rectangle lists use the general rectangle-list
variant, since the count is unknown; fans, polygons, legacy lists and
targetless draws (which may be retained and replayed) still read the
arguments on the CPU. SHARPEMU_CPU_INDIRECT_DRAW=1 keeps the CPU path.

(cherry picked from commit f2b91befb7dd27d65a35546f1e57dc88f04a5c82)
2026-10-07 10:48:47 +02:00
kuba 435bbd4844
[Cpu] Copy page-sized guest memcpys on the managed path (#1055)
The native memcpy fast path copies with rep movsb straight into guest
memory. Pages the GPU reads are write-protected so CPU writes can be
tracked, so a copy into them takes one protection fault per page, and
under Rosetta each fault is a signal round trip of about 0.2 ms. The
managed memcpy marks the whole destination written before it copies and
takes no faults. Copies of at least 4 KiB now leave the stub for the
managed export; smaller ones keep the native copy, where one import
transition would cost more than the copy. SHARPEMU_MEMCPY_MANAGED_MIN
overrides the threshold.

Demon's Souls, 230 s through the PlayStation Studios intro: 536k
write-tracking faults become 71k, and the intro presents about a fifth
more frames.
2026-10-07 10:48:00 +02:00
ParantezTech 3e2ad63303 [readme] New screenshots v2 2026-10-06 19:07:24 +03:00
ParantezTech ac8608449f [readme] New screenshots 2026-10-06 19:07:06 +03:00
Berk 2df6f5b0a0
Unity fixes 1016p1 (#1043)
* [HLE] Let schedulers report guest thread stack bounds

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Core] Report the mapped stack of each guest thread

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Validate pthread stack bounds before reporting them

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Report every guest thread's stack in pthread attributes

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Publish semaphore handles as 64-bit values

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Accept zero-count semaphore signals

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Route semaphore waits through one core

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Poll host semaphore waits and deliver guest exceptions

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Add scePthreadSemTimedwait

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Remove every pending copy of a deleted event

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Block timed event queue waits in the guest scheduler

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Refresh pending user data when an event is re-added

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Keep every end-of-pipe interrupt event

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Track deadlines and POSIX results on sync waiters

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Poll host sync waits in bounded slices

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Share the sync-on-address wait and wake cores

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Kernel] Add _umtx_op wait and wake

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Np] Track WebApi2 user contexts and push filters

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Np] Add sceNpWebApi2PushEventDeleteHandle

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Np] Add sceNpWebApi2PushEventRegisterCallback

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Tests] Cover kernel sync, pthread stack bounds and NpWebApi2

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [GPU] Accept HTile-backed compressed stencil targets

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Tests] Cover HTile-backed compressed stencil targets

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [GPU] Accept stencil targets with or without HTile backing

* [Tests] Cover stencil compression with and without HTile

* [ShaderCompiler] Treat V_CMPX as an EXEC write in every encoding

* [Tests] Cover SDWA V_CMPX in the EXEC-full analysis

* [GPU] Give images first found without metadata the DCC metadata of a texture request

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Tests] Cover DCC metadata adopted from a texture request

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [GPU] Number command stream submissions in admission order

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [GPU] Queue control barriers behind accepted command streams

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [GPU] Cancel pending control barriers when the command stream fails

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [GPU] Cancel pending control barriers when the command stream is discarded

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [GPU] Run relay work after accepted command streams

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [VideoOut] Order relay work behind the presenter command stream

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [GPU] Let backends run work after pending command streams

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [VideoOut] Run state changes after pending command streams

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Metal] Run state changes after pending command streams

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [VideoOut] Close ports after pending command streams

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [VideoOut] Unregister buffers after pending command streams

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

* [Tests] Cover ordered video-out state changes

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>

---------

Co-authored-by: Sekai <140012992+igettingit@users.noreply.github.com>
2026-10-06 18:43:21 +03:00
Berk a17aaca533
Astro playroom shaders (#1030)
* [Vulkan] Enable shader clip and cull distances

* [Vulkan] Track shader clip distance support

* [ShaderCompiler] Add a clip distance support flag to compile requests

* [GPU] Expose host clip distance support to pipelines

* [GPU] Pass clip distance support to shader compilation

* [ShaderCompiler] Reserve a clip plane for position exports

* [ShaderCompiler] Cull primitives with a zero position

* [GPU] Bind vertex buffers with their guest descriptor sizes

* [GPU] Accept B10G11R11 float vertex attributes

* [GPU] Spill oversized image readbacks to a temporary buffer

* [GPU] Carry DCC metadata on compressed textures

* [GPU] Mark sampled DCC clears through the metadata fill path

* [Tests] Cover vertex descriptor bounds and oversized image readbacks

* [GPU] Revert binding vertex buffers with their guest descriptor sizes

* [GPU] Carry DCC metadata only on single-layer 2D textures

* [Tests] Cover DCC metadata on 2D, 3D and array textures
2026-10-06 01:32:13 +03:00
ParantezTech 83abe47b6e [readme] re-order 2026-10-05 16:29:03 +03:00
ParantezTech d6c4778218 [readme] added warning for not official sites, donations etc. 2026-10-05 16:27:41 +03:00
ParantezTech 5965b07c7b [Logging] Skip "tbb_thead" without LogThreadMode 2026-10-05 16:18:27 +03:00
ParantezTech 4a822aedb9 Merge branch 'main' of https://github.com/sharpemu/sharpemu 2026-10-05 15:36:18 +03:00
ParantezTech 22bbcb1415 [readme] update screenshot 2026-10-05 15:36:01 +03:00
T 9e99769ce5
[GPU] Report mip statistics for sampled textures so streamed textures reach full detail (#1029) 2026-10-05 14:39:40 +03:00
Berk 41a1912cd9
[SRT] Compile shader resource plans in the background (#1028)
Each shader resource plan is compiled once to a DynamicMethod that evaluates
its resource table graph with plan-local value slots instead of the
interpreter's hashed cache. ASTRO's PLAYROOM goes from about 20 to 60 fps.

Compilation runs on a low-priority "SRT compiler" thread; until a plan is
ready, the interpreter evaluates it, so a new shader no longer stalls the
render thread while its plan is JIT-compiled. A plan whose compilation fails
stays on the interpreter. SHARPEMU_SRT_NATIVE=0 turns the compiled path off;
it is also off when the runtime does not support dynamic code.
2026-10-05 01:19:33 +03:00
Berk 0163750e7e
Page fixes (#1023)
* [DeS] Fixes for page faults and stack bounds issues in the emulator and audio fixes

* [ShaderPrewarm] Hold the game until the prewarm finishes and resume interrupted runs

The prewarm compiled thousands of compute pipelines on background threads
while the now faster boot ran beside it, so logos, Bink movies and the first
scenes raced the compile and dropped frames.

- Hold the guest at its first splash hide, GPU submission or CPU flip until
  the prewarm is decided and done; the render thread keeps presenting the
  splash. SHARPEMU_SHADER_PREWARM_WAIT=0 opts out.
- Stamp only the shader translators, the compiler type and the driver, so a
  Libs-only build no longer triggers a full prewarm.
- Record finished pipelines in shader-prewarm.progress so an interrupted
  prewarm resumes with the remaining ones.

* [AudioOut2] Block a blocking push only until a queue slot frees

A blocking sceAudioOut2ContextPush waited only when the host stream hit its
128 KiB cap, so a title rendering faster than real time kept about 680 ms
queued and every sound played that far behind (GTA V: queue=2 context,
queued_ms avg=678). The push now waits until the device holds the cushion
plus QueueDepth-1 grains, the level GetQueueLevel already reports as full;
the host cap stays as the limit for non-blocking bursts. GTA V now holds
about 41 ms with no underruns.

* [GPU] Run bounded modulo copies of plain words on the host

Demon's Souls copies its per-frame constant and descriptor tables with a
compute kernel (dst[i] = src[i % N] for i < count). Run on the GPU, the
destinations stay GPU-dirty, and every draw whose resource tables read them
waits for a readback: about 19% of the render thread in the title scene,
for values that never changed.

The kernel is now recognized by its instruction sequence. When both
descriptors are plain 32-bit words of the same format and neither range is
GPU-dirty or GPU image memory, the copy is written to guest memory through
the host DMA path and the dispatch is skipped; anything else still runs on
the GPU. SHARPEMU_HOST_BOUNDED_COPY=0 turns it off.

Title scene with profiling: 7.4-8.6 -> 11.8-15.9 fps.

* [Diagnostics] Summarize synchronized table reads and trace several writer addresses

SHARPEMU_TRACE_GPU_READS=1 now also reports, every five seconds, the guest
words resource planning read through a GPU synchronization: read count, how
often the value changed, and the first and last value. SHARPEMU_TRACE_WRITER_ADDRESS
accepts a comma-separated list.
2026-10-04 19:07:37 +03:00
Berk 57e0da2150
Revert "[DeS] Fixes for page faults and stack bounds issues in the emulator a…" (#1022)
This reverts commit ec0198f2d0.
2026-10-04 05:57:24 +03:00
kuba cb42e849d7
[VideoOut] Keep guests on SDR output under Auto HDR on macOS (#1020)
Upstream e7a4f0a0 started reporting HDR to the guest whenever the display
has HDR enabled. On an HDR Mac, Silent Hill switches its output to HDR,
a guest pipeline then fails to compile in MoltenVK (ushort4 output to a
BGRA8 attachment) and the process crashes inside MoltenVK on every run.
Auto now stays SDR on macOS; --hdr=on still opts in.
2026-10-04 05:39:24 +03:00
Berk ec0198f2d0
[DeS] Fixes for page faults and stack bounds issues in the emulator and audio fixes (#1021) 2026-10-04 05:38:49 +03:00
ParantezTech 8903e3760b [readme] update screenshot 2026-10-04 04:28:03 +03:00
T d4891229e9
[SpirvTranslator] Shader fixes (#1019)
* [ShaderCompiler] Fix packed 10 and 11 bit buffer format layouts

* [SpirvTranslator] Pass W as the pixel position input instead of its reciprocal

* [SpirvTranslator] Keep lane spills in per invocation slots for all 64 lanes

* [SpirvTranslator] Match the hardware float conversion and NaN rules

* [SpirvTranslator] Apply VOP3 sign modifiers to conditional mask sources

* [GPU] Download guest DCC metadata before a known target is sampled

* [ShaderCompiler] End bit scan image tables at a sampler loaded from the same base

* [ShaderCompiler] Treat image table candidates with unsupported formats as null
2026-10-04 01:17:02 +03:00
Berk 6fe8b87f48
chore: bump version to 0.0.5-nexus (#1018) 2026-10-03 05:15:35 +03:00
ParantezTech 375d434353 [readme] update game order and screenshot 2026-10-03 05:13:13 +03:00
Berk 0c616d80ff
[Font] Fix font face creation (#1010) 2026-10-03 05:04:56 +03:00
Berk ddaf7ffe5b
Perf computed shaders (#1013)
* [Gpu] Recognize AGC's loaded-value buffer fill as an image/metadata clear

Octopath clears its DCC with a shader that s_buffer_load_dwords the fill value
from a one-record buffer and stores it at every thread index; only the variant
with the value in user data was recognized, so the DCC fast clear of UE's
translucency layer was lost and the layer kept stale alpha 0, which erased the
world map under it in the composite.

* [Vk] Bind descriptor sets instead of push descriptors on MoltenVK

MoltenVK returns zero from OpArrayLength for storage buffers bound through
push descriptors, so every bounds-checked buffer load read zero and every
store was dropped. In Poppy Playtime this left UE's motion blur indirect
dispatch arguments unwritten; the GPU then ran a 4294967295x1612x1490
dispatch and hung, and the game aborted on the render thread timeout.

* [ShaderCompiler] Size compute LDS to the dispatch's allocation

Every compute module declared the full 32 KiB of workgroup memory, which on
Apple GPUs leaves one workgroup resident per core. Declare the dispatch's
COMPUTE_PGM_RSRC2.LDS_SIZE (plus the wave64 bridge dwords), rounded to a power
of two so the index mask still bounds accesses. Silent Hill's heaviest per-pixel
pass (LDS_SIZE=4) replays in 11 ms instead of 46 ms.

* [Loader] Guard frame-pointer spill slots that live in the guest red zone

Host signal frames are pushed onto the guest stack, so GuestRedZonePatcher
protects functions that keep live data in the 128-byte red zone. It only
recognized RSP-relative accesses. Clang spills just as often through the frame
pointer: after 'push rbp; mov rbp, rsp' and N callee-saved pushes, RSP sits at
RBP-N and every [rbp-d] with d > N is below RSP. A fault taken there (lazy
commit, write tracking) overwrote those slots, which showed up as garbage
pointers on random threads in Poppy Playtime's level load.

Derive RBP-to-RSP from the standard prologue (leading pushes and the first
'sub rsp, imm') and treat [rbp-d] in (delta, delta + 128] as red-zone use. An
under-estimated delta only widens the guarded window. In Poppy's eboot this
protects 787 functions instead of 485.

* [AvPlayer] Report a loaded source as active before playback starts

sceAvPlayerIsActive answered true only after sceAvPlayerStart. Titles poll it
after adding a source and before starting playback; Superliminal waits for it
to turn true before it calls Start, so New Game never left the loading screen.
Report a player as active from the moment its source is loaded until it is
stopped or reaches its end, and track Stop explicitly so a stopped player
stays inactive.

* [Gpu] Copy the mips of a GFX10 mip tail block written through its own view into the chain

GFX10 packs every mip smaller than half a swizzle block into one block at the
start of the chain. A title can render into that block through a single-level
view exactly the size of the block: Unity's screen-space reflection blur writes
mips 3 and up of its 1024x512 color pyramid through one 128x64 view. The cache
keeps that view as its own image, so the chain's tail mips kept stale data and
sampling them produced rainbow blobs over Superliminal's menu.

When a chain is acquired with data, look for a GPU-written single-level image
at the chain's address with the tail block's extent, format and tile mode, and
copy each tail mip out of it at its placement inside the block. A per-image GPU
write sequence makes the copy happen once per new write.

* [SpirvTranslator] Give each used register its own private variable

The translator kept the scalar and vector register files as two private arrays
(128 and 512 uints) in every module. MoltenVK lowers a private array that size
to per-thread memory, so every register read and write in every invocation went
through memory. Declare one private variable per register the program touches,
on first use; the SPIR-V is otherwise unchanged.

* [ShaderCompiler] Resolve V_MOVREL* with selects instead of a spilled VGPR array

A program with any V_MOVREL* instruction declared its whole VGPR file as a
512-entry private array indexed at run time, which Metal spills to thread
memory, so every register access became a memory access. Keep one variable
per register and resolve relative accesses with a select over the registers
the program names. Octopath's instanced vertex shader (1536 x 8124): 112 ms ->
6.4 ms per draw in replay, output byte-identical.

* [AvPlayer] Start a player on its own when the title registers no event callback

Reporting a loaded source as active before Start regressed Quake, which
waits for its player to go inactive before starting its queued intro
videos. A player left loaded but unstarted stays active forever. Only a
title that registers an event callback can see the ready state and call
sceAvPlayerStart itself, so a player whose title asks for autoStart or
registers no callback now plays by itself once its source is ready,
reaches the end of the stream and reports inactive. Titles that start
playback themselves keep seeing a loaded source as active.

* [Gpu] Recognize AGC's constant buffer fill as an image/metadata clear

Octopath clears the DCC of its 1920x2200 DOF/bokeh accumulation target with a
third fill kernel: v_mov_b32 v1, 0 and buffer_store_format_x v1 at the thread
index, with only the destination descriptor in user data. It was not
recognized, the DCC fast clear was lost, and the additive bokeh pass kept
blending onto the previous frames until the target held thousands and inf,
which showed as noise and banded garbage over every 3D scene.

Program info now carries the constant a program stores when it is only index
arithmetic, one v_mov of a constant and one buffer store; a one-buffer 32-bit
formatted fill over every record with that value becomes a clear.

* [Vk] On macOS, retire a GPU tick only once its submission's fence signals

MoltenVK records Metal command buffers with unretained resource references
and signals a Vulkan timeline semaphore from a Metal event scheduled at the
end of the command buffer. That event can fire before Metal has finished
executing the command buffer, so a host thread that treats the timeline
value as authoritative can free resources the GPU is still reading. Metal
API validation reports this as "destroyed while still required by a command
buffer", and the resulting use-after-free showed up as intermittent GPU
tick stalls.

Every submission that signals the timeline now also signals a VkFence, which
MoltenVK signals from the Metal command buffer's completion handler. The
timeline value a host thread is allowed to act on is clamped to just below
the oldest tick whose fence has not signaled yet, and a timeline wait is
followed by a blocking wait on the last fence covering that tick. Fences are
pooled, tracked before the submit so no reader can observe a signaled tick
without its fence, and reset only once no thread is blocked on them. Fences
are polled at all only when the timeline already claims the tick is done, so
the common path costs one queue peek. The behaviour is macOS-only and can be
turned off with SHARPEMU_FENCE_RETIREMENT=0.

* [Vk] Refuse push descriptors on macOS for every device through one policy

The guard that refuses push descriptors on MoltenVK keyed on the
VK_KHR_portability_subset device extension, but that extension is not
enumerated for every device: the headless device the draw replay and the
device tests create took the raw driver limit, so replays on macOS read
guest storage buffers past their first bytes as zero (MoltenVK reports a
zero OpArrayLength for buffers bound through push descriptors).

The limit now comes from one policy both the presenter and the headless
device use, and macOS, whose only driver is MoltenVK, pushes none
regardless of which extensions are listed.

* [SpirvTranslator] Order emulated wave64 LDS phases in every block

A guest wave64 spans two host subgroups. The guest orders one lane's LDS
write before another lane's read by executing in lockstep, so the translator
puts a workgroup barrier between the halves wherever the wave switches
between writing and reading LDS. It only did so for programs of a single
block, which left every program with branches -- an NGG geometry stage
compute program has seventeen exec-skip branches around its ES to GS LDS
exchange -- reading the other half's LDS words with no ordering at all, so
the result depended on timing.

Every guest branch condition is wave-uniform in this emulation (EXEC and VCC
tests go through the wave mask and SCC is scalar), so every block is entered
by the whole wave and may hold a barrier, exactly as the wave64 ballot
exchanges already do. A block can be reached from either phase, so its first
LDS access always starts with a barrier.

* [Vk] Keep the rendering scope open across barriers and guest cache flushes

Tile-based GPUs pay for every rendering scope: ending one stores the
attachments out of tile memory and beginning the next one loads them back.
The presenter ended the scope for three things that do not actually need it.

An image barrier that repeats an attachment access in the same layout only
matters to work that runs after the scope, because accesses to an attachment
inside one scope are already ordered by rasterization order. Such barriers
are now queued and recorded as the scope ends instead of ending it.

A guest cache flush (a global memory barrier) inside a scope whose draws only
wrote attachments is deferred the same way, and every scope now opens with
that global barrier so anything recorded before the scope stays ordered
before its draws. A draw whose vertex or pixel stage stores to buffers,
storage images or device addresses ends a scope with a deferred flush first,
so a store can never move ahead of it. Finally, the DMA fill and copy entry
points no longer end the scope up front: the buffer cache completes most
transfers in guest memory, and the paths that do record GPU work end the
scope themselves.

SHARPEMU_DEFER_GLOBAL_BARRIERS=0 restores ending the scope at every flush.

* [SpirvTranslator] Take wave32 EXEC/VCC lane state from the low register

In wave32 a lane mask is its low register alone: EXEC_HI and VCC_HI are
ordinary SGPRs. Writing one of them therefore must not recompute the wave
lane state, and the lane state has to be tested against the low dword rather
than against a 64-bit mask composed from the low and high registers.

The lane-bit test that guards this covered the wave64 constant width only, so
it now accepts a mask of either width.

* [AudioOut2] Pace one grain per Advance+Push cycle and report the queue level

sceAudioOut2ContextAdvance and sceAudioOut2ContextPush each submitted the
context's pending PCM and, whenever nothing reached a host device, each slept
a whole grain in the software pacing fallback. A title that calls Advance then
Push once per grain therefore spent two grain periods per grain, so its audio
thread ran at half real time and held up every thread waiting on it.

Advance now claims the next grain on the context's clock and returns without
blocking (or, when its own submit already blocked in the backend, marks the
grain served), and Push waits for the grain Advance claimed; a Push with no
preceding Advance claims one itself. Neither call skips pending PCM. Because
the output holds QueueDepth grains, a grain may be queued as soon as the grain
QueueDepth-1 ahead of it starts playing, so a context can run that far ahead
of real time before a push blocks.

sceAudioOut2ContextGetQueueLevel reported an empty queue with the full depth
available on every call. Titles size their rendering by those two values --
Wwise renders exactly puiAvailableQueues grains per update and pushes them
non-blocking -- so a queue that never filled made it render a full queue every
update and then block inside Push for a grain per push while holding its audio
lock, which starved the game thread. The level now comes from the context's
grain clock, or, once a grain has reached a host device, from that device's
queued milliseconds beyond a short cushion.

* [Vk] Keep one depth attachment layout for draws that do not sample it

The depth attachment layout followed each draw's depth/stencil write mask, so
a draw sequence that toggles depth writes — opaque geometry, then decals and
translucency — changed the layout every few draws, and every layout change
ended the rendering scope.

The read-only attachment layouts are only required when the draw also samples
the attachment. Every other draw now uses the writable layout for the
aspects the format has, and the write mask alone selects the access, so the
layout stays constant and the scope survives. The presenter answers whether a
texture bound for the draw being prepared views the depth attachment's
subresources, defaulting to yes for hosts that do not track it.

* [Audio] Sleep for the SDL queue's drain time instead of polling every ms

Submit's backpressure wait is what paces every audio grain on the SDL
output, and it polled the queued byte count with Thread.Sleep(1), costing
the audio thread several wakeups per grain.

It now sleeps for the time the device needs to drain back under the queue
cap, still bounded by the existing deadline and the maximum wait.

* [CLI] Shut down cooperatively on SIGTERM

SIGTERM (launchd, process managers, kill) used the runtime default and
exited with GPU work in flight. It now takes the same host-interrupt path
as Ctrl+C: the presenter closes and tears Vulkan down before exit.

* [SpirvTranslator] Fold constant integer and boolean instructions

The translator materializes a good deal of translation-time knowledge as
SPIR-V instructions: a specialized descriptor's buffer format, swizzles,
strides, and the conditions that guard host-guaranteed ranges all end up as
arithmetic, comparisons and selections over operands the builder itself just
emitted as constants. That reaches the driver as runtime work, and on Metal
every one of those instructions costs pipeline-creation time.

The module builder now remembers the value of each 32-bit integer and boolean
constant it emits and folds pure instructions over them while it builds:
integer arithmetic, bit operations, shifts, bitfield extraction, the integer
and logical comparisons, the logical connectives, and a selection on a known
condition. Folding happens at emission, so a folded result feeds further
folding without a separate pass. Anything whose operands are not all known
constants is emitted exactly as before.

The buffer format decoder uses this directly: a specialized descriptor makes
the unified format a constant, so it decodes on the host instead of emitting
a module-level lookup table and an indexed read.
The test that covered that path asserted the table's presence and now asserts
its absence.

* [SpirvTranslator] Skip host-guaranteed bounds checks and unused LDS

Two sources of instructions the driver has to compile but can never need.

A read of a planned slot in the host-flattened resource table is always
inside the table the host binds, because the table holds every planned read
and written range. Those loads nevertheless carried a runtime array-length
compare and two selects each; a large pixel shader made about 376 of them.
A slot below the planned reserved word count now loads directly, and only a
slot computed at run time keeps the check.

ds_swizzle and ds_bpermute exchange data between lanes and GDS instructions
address the global data share, so none of them touches LDS. A graphics stage
that uses only those was still given the 8 KiB zero-initialized private LDS
array, which Metal pays for in compile time and in private memory per
invocation. The array is now declared only for instructions that really
address LDS.

* [Cpu] Answer pthread_self and pthread_getspecific in native stubs

The managed import gateway costs a guest-to-host stack switch, a full guest
register pack, a GCHandle dereference and the whole dispatch bookkeeping
before the HLE export even runs. For the two hottest trivial imports that is
three orders of magnitude more work than the answer itself: one UE5 title's
load measured 12.9M scePthreadGetspecific calls (38 s of wall time) and 4.3M
scePthreadSelf calls, at roughly 2.9 us each.

The few values those two imports need are now published into a per-host-thread
native block whose address lives in a native TLS slot, rebound whenever a
thread enters or is restored as a guest thread, and the two imports are
patched to emitted x86-64 stubs that read the block directly. pthread-specific
values live in native per-thread tables so a stub and the managed export read
exactly the same storage and cannot drift apart. The managed path stays the
source of truth for every case a stub declines -- no block published, no guest
thread bound, no value table, a key outside the table -- which it reaches by
jumping to the import's normal trampoline with every register intact,
including the incoming RAX variadic vector count.

The value-table slot follows whichever handle the current thread reports, the
bound guest thread or the host thread's own synthetic handle, so
pthread_getspecific is answered on unbound host threads as well; the primary
execution thread never enters through the guest-thread path and is a major
caller in UE5 titles. The self-handle slot stays guest-only, because
pthread_self on an unbound thread registers that thread's CPU context with the
guest scheduler and must keep reaching the managed export. Each stub also
bumps a per-thread hit counter, reported next to the existing HLE cost
histogram, so a call that misses the stub is visible rather than invisible.

The stubs are gated to macOS x64, where the emitted TLS read matches the
platform's pthread key array, and switched off whenever import or pthread
tracing needs every call to pass through the gateway.

* [CPU] Run libc memcpy/memmove as a native guest-side stub

Guest memcpy went through the managed gateway on every call: two write
tracking notifications, the memory reader lock and two region lookups, about
0.8 us per call under Rosetta even for a copy of a few bytes. Dreaming Sarah
makes roughly 45k of those calls a second.

Guest memory is identity-mapped, and every fault a copy can take -- write
tracking, lazy commit -- is resolved from the fault address alone, so the copy
can run as a forward rep movsb in a fast-path stub. A destination that sits
inside the source, where a forward copy would read bytes it has already
written, and the write-watch diagnostic both keep the managed export.

* [SpirvTranslator] Fill the FRONT_FACE and ANCILLARY pixel inputs

A pixel shader that enables FRONT_FACE or ANCILLARY reads those VGPRs, but the
translator only reserved their compact slots and left them reading zero.

Unreal Engine bakes its colour grading LUT one volume slice per instance and
has the pixel shader read the slice back out of ANCILLARY[28:16], the render
target array index. With that register zero every slice was baked from the
blue = 0 slice, so the tonemapped image lost its blue channel.

ANCILLARY is now the Layer built-in placed at bit 16, and FRONT_FACE the
FrontFacing built-in mapped to 1.0f for a front face and 0 for a back face,
which satisfies both the float greater-than and the integer not-equal tests
compilers emit for it.

* [SpirvTranslator] Fill the SAMPLE_COVERAGE and POS_FIXED_PT pixel inputs

The two remaining reserved pixel system inputs were also left reading zero.
SAMPLE_COVERAGE now reads the first element of the SampleMask built-in, and
POS_FIXED_PT packs the integer pixel position taken from FragCoord, x in bits
[15:0] and y in bits [31:16], so no enabled pixel system input is left unset.

* [CPU] Run the POSIX signal handler on the host stack for guest faults

Guest faults are delivered on the guest stack, and the managed handler ran
there. The runtime only knows the thread's own stack; Windows swaps the TEB
stack bounds while guest code runs, but POSIX has no equivalent. A garbage
collection during a guest fault then misread the thread's frames and
reported garbage object references, and the process failed with "attempted
to call a UnmanagedCallersOnly method from managed code". A small native
trampoline now moves to the host stack below the frames that entered guest
code before it calls the managed handler.

* [VideoOut] Give every swapchain image its own sRGB-encode intermediate

A float guest flip is blitted through an sRGB image before it is copied into
the UNORM swapchain image, and one such intermediate served every frame. Each
frame discards its contents by transitioning it Undefined -> TransferDst
before blitting, so with two frames in flight the next frame's blit raced the
previous frame's copy-out and the presented image came out fully zeroed. The
guest's own display buffers held the correct picture the whole time, so the
window simply went black for long stretches.

Each swapchain image now owns its own intermediate, created and destroyed with
the swapchain, so a frame can only ever discard the intermediate belonging to
the image it is about to present.

On Superliminal the same run went from 60 of 84 presented-swapchain readbacks
black to 10 of 187, all of them from before the menu appears, and the menu no
longer turns black after a couple of minutes.

* [VideoOut] Present a flip only after the tick that copied it

CaptureFlip recorded the display-surface copy into the guest batch, flushed
it, and then read the shared _submitTimeline field for the presentation's
RequiredTick. The presenter thread writes that same field from its own
submission, so the flip could be tagged with an older tick that was already
complete. The render loop then blitted the freshly created (never written)
flip snapshot, which presents as fully zeroed RGBA - a black frame with an
alpha of 0. Take the tick that this flush returned instead.

* [VideoOut] Pool the per-flip display-surface snapshot images

CaptureFlip created a 1920x1080 RGBA16F VkImage with its own VkDeviceMemory
for every flip and destroyed both when the flip retired: around twenty
vkCreateImage plus vkAllocateMemory pairs a second for the same extent, and on
MoltenVK as many Metal texture allocations.

Retired snapshots are now kept in a small per-format, per-extent pool and
handed back instead. A snapshot is fully overwritten by the copy that follows
it and its barrier takes it from Undefined, so neither its previous contents
nor its layout matter. The pool holds at most eight images, is guarded because
the command-stream thread takes snapshots while the render thread retires
them, and is drained at teardown. SHARPEMU_FLIP_SNAPSHOT_POOL=0 turns it off.

* [CPU] Bridge SIGFPE into the guest fault handler so #DE crashes are reported

The POSIX bridge installed handlers for SIGSEGV, SIGBUS, SIGILL, SIGTRAP and
SIGABRT, but not SIGFPE. A guest integer divide by zero therefore took the
runtime default and killed the process with no exception record and no guest
context, so the faulting instruction was invisible.

SIGFPE now routes to the same handler and is reported as an integer divide by
zero. Like SIGILL, it skips the image write tracker, which only ever resolves
data-access faults.

* [Cpu] Resolve GPU-tracked page faults before building the exception context

A CPU access to a page the GPU memory tracker protects faults on every first
touch after an upload or a GPU write, thousands of times a second in a frame.
The POSIX bridge converted each one into a full Win64 CONTEXT (general and
vector registers), then walked the interrupt, image-tracker, lazy-commit and
allocator-hole handlers before the tracker ever saw it.

The tracker needs only the fault address and the access kind, so it now runs
right after the image tracker, straight off the raw signal. An env var
restores the old handler order.

* [CPU] Serialize import stub and lazy TLS site patches

Patching a guest code site unprotects the page it lives on, writes, and
restores the old protection. Two threads patching sites that share a page
therefore race: one restores the page to read-execute while the other is
still writing into it, and the second write faults or is lost. Both happen in
practice -- a runtime module load sets up import stubs while guest threads are
running, and guest threads patch their own lazy TLS access sites as they first
reach them, several of which land on one page.

Every import stub patch and every lazy TLS site patch now runs under a single
gate, so the unprotect/write/restore sequence is never interleaved.

* [SpirvTranslator] Follow guest NaN and saturation rules for float ops

v_min_f32 and v_max_f32 return the other operand when one of them is NaN, and
v_cvt_u32_f32 and v_cvt_i32_f32 saturate an out-of-range value to the target
type's limit and turn NaN into zero. The GLSL FMin and FMax instructions and
SPIR-V's ConvertFToU and ConvertFToS all leave those cases undefined, so a
host driver is free to hand back the NaN or an arbitrary integer, and Metal's
fast math does return the NaN.

The minimum and maximum now select the other operand when either side is NaN,
and the two conversions clamp at the type's limits and return zero for NaN.
NaN is tested on the value's bits rather than with an ordered comparison, so
fast math cannot assume the test away.

Device tests cover this and the wave-wide maximum that Unreal's motion blur
computes with DPP row_shr, v_permlanex16_b32 and v_readlane_b32 across a
64-lane wave.

* [ShaderCompiler] Fix the SOPK immediate width and S_ADDK_I32's SCC

Two things were wrong in the SOPK forms, in both the SPIR-V and the MSL
backend.

S_CMPK_*_U32 compares against SIMM16 zero-extended; only the signed compares
sign-extend it. Both translators sign-extended it for every compare, so
"s_cmpk_le_u32 s4, 0xffff" was true for any value at all. NGG geometry
programs use exactly that test on the packed vertex and primitive count, so
they took their no-primitive path and exported a single null primitive, which
left Unreal's volume passes empty. The register S_CMPK names is also only
read, yet user-data liveness recorded it as a definition, so a compare on a
user-data register left that register unloaded; it now counts as a use, as
does the register of the SOPK accumulations.

S_ADDK_I32 is defined as D.i = D.i + signext(SIMM16) with SCC set on signed
overflow, the same SCC rule as S_ADD_I32, which both backends already
implement. Neither wrote SCC for the SOPK form, so a following
S_CBRANCH_SCC0/1 or S_CSELECT read whatever the previous instruction had left
there. S_MOVK_I32 and S_MULK_I32 correctly leave SCC alone.

* [ShaderCompiler] End the decode at S_CODE_END

S_CODE_END pads the space after a shader's last instruction. When the code
after the final S_ENDPGM ends in a backward branch, the linear decoder walks
straight into that padding and failed with an unknown SOPP opcode 0x1F, which
stopped the session.

The decoder now names 0x1F and ends the program there, unless a forward branch
still targets code beyond it -- in which case the padding really would be
inside the program and the decode still fails.

* [ShaderCompiler] Widen host-evaluable scalar operations for descriptors

A resource descriptor the host materializes is built by replaying the shader's
scalar operations on the host. The validator that decides whether a value can
be replayed listed only part of what the scalar evaluator implements, so a
descriptor computed through any of the others was rejected even though the
host can compute it exactly.

The list now covers every pure scalar function the evaluator supports: the
remaining integer maximum, absolute value and high multiplies, the signed and
unsigned comparisons, 64-bit equality, the bit-counting and bit-scanning
operations, the quad mask, the integer/float conversions, and the float
arithmetic and comparisons. Anything with side effects or a lane-varying
result is still rejected.

* [Gpu] Stop draining the GPU for predication that asked not to wait

PredicationZPassWaitOp is 0 for kWaitForQueryResults and 1 for
kDoNotPredicateIfQueryResultsNotReady, so the wait bit was being read
inverted. Every SET_PREDICATION packet that asked the command processor not to
stall drained the whole host queue instead, and the packets that did ask to
wait read the predication label without waiting for the results at all, so
they could act on a stale value.

The drain now happens only for kWaitForQueryResults, and only on the branch
that actually reads the label. A packet tagged
kDoNotPredicateIfQueryResultsNotReady leaves its work unpredicated, which is
what the hardware does while the results are still outstanding, and clearing
predication reads no memory at all so it never drains.

* [Agc] Queue the suspend point instead of draining the command stream

A suspend point inserts GPU commands that drain the graphics pipe. The call
that inserts it does not wait for those commands, and the hardware allows one
suspend point to be in flight, but the queue implemented it by blocking the
calling thread until every accepted submission had been processed.

The frame boundary is now a queued submission of its own, ordered behind the
graphics work it ends, and processing it flushes what the frame recorded. The
caller returns immediately for the first boundary and only waits when a second
one arrives while the first is still pending. Shutdown still drains, because no
slice will run a queued boundary once the queue stops accepting work, and a
cancelled queue drops pending boundaries along with the rest.

Measured on Octopath Traveler II: the call went from 223 ms to 72 ms, and the
guest render thread spent 38 s instead of 107 s of its 170 s inside it.

* [Cpu] Give guest execution threads a 64 MiB host stack for nested continuations

Every guest call that blocks in an HLE import resumes through a managed
continuation frame, so a guest thread that blocks deep inside its own call
tree nests host frames instead of unwinding them. Demon's Souls' render
thread overflowed the default thread stack within a minute of reaching its
main loop.

Guest execution threads now reserve 64 MiB. The reservation is virtual, so a
generous stack costs nothing until it is touched.

* [Gpu] Report pipeline creations that take longer than 250 ms

A host shader compiler can spend seconds inside vkCreateGraphicsPipelines or
vkCreateComputePipelines for one translated program, and the guest's command
stream cannot advance while it does. There was nothing in the log to say that
had happened, so the stall looked like the game hanging.

Pipeline creation is now timed, and any creation of 250 ms or more is reported
with the stage hashes, the SPIR-V byte count of each module that went into it,
and the running total spent creating pipelines, which is what identifies the
program to look at.

* [Gpu] Compile compute pipelines off the command-stream thread

A host shader compiler can spend tens of seconds on a single large translated
compute program, and the command stream could not advance while it did, because
the pipeline cache holds its lock across the creation call. Every other queue
that wanted any pipeline waited behind it too.

The pipeline host can now report that a compute pipeline is not ready yet. The
Vulkan presenter answers that way the first time it sees a program: it prepares
everything the command stream owns (descriptor set layout, pipeline layout,
shader module) up front and runs only vkCreateComputePipelines on a worker
thread, so the task touches nothing else. The number of compiles in flight is
bounded, since each one blocks its thread inside the host compiler and a frame
can touch many programs at once. Asking again returns the finished pipeline
without starting a second compile.

The dispatch waits for its pipeline rather than being dropped: a guest that
does not replay a one-shot dispatch deadlocks, because the dispatch can feed a
label a later packet waits on. Asking again from the executor keeps that wait
outside the pipeline cache's lock. Hosts that compile cheaply keep the blocking
behaviour through the interface default, and
SHARPEMU_ASYNC_COMPUTE_PIPELINES=0 compiles inline.

* [GpuMemory] Stop allocating on every tracked CPU write and shader identity hash

MarkCpuWrite runs on every guest CPU write into tracked memory — HLE register
patching, memcpy, index buffer setup. Its flush lambda and the region visitor
inside InvalidateRegion were both capturing closures, so every such write
allocated two closure objects and two delegates even when nothing needed
flushing at all.

InvalidateRegion now has a state-passing overload built on a ref-state
VisitRegions, and both call sites use static lambdas. The public
Action-based overload is kept for the callers that are not on a hot path.

ShaderIdentity.Compute is the other one: it allocated a fresh XxHash3 and a
byte[] per code range on every draw of a shader that carries no declared hash.
It now resets a per-thread hasher and reads each range into a pooled buffer.

* [Gpu] Allocate nothing per host DMA fill or copy

A DMA fill the buffer cache completes on the host allocated a 16 KiB array for
every call, even though almost all of them are label-sized, and a host copy
allocated a 64 KiB array per call. WriteHostMemory then scanned every
registered buffer for each write it made.

The fill builds its repeating chunk in a stack buffer sized to the fill, the
copy rents its staging buffer from the shared pool, and WriteHostMemory walks
only the registered buffers the write actually overlaps, which it can do
because the registry is ordered and disjoint.

* [Audio] Fix out-struct sizes and layouts in AudioOut, Ajm and AudioPropagation

sceAudioOutGetPortState wrote 16 bytes and put the volume in a single byte at
+7, which is inside rerouteCounter. SceAudioOutPortState is 0x20 bytes with
volume as an int16 at +4; the export now writes the whole struct with
volume = -1, which is what the library reports for anything but the pad
speaker. Its guest-stack bail is gone for the same reason it is going in
libSceAudioOut2: the call names one out pointer and its host address range is
not a signal.

ClearAjmBatchError zeroed 24 bytes, the naive sum of the field widths.
SceAjmBatchError is 0x20 bytes because its two pointers force 8-byte
alignment, so pJobOriginRa lands at +0x18 and kept stale guest bytes across
every batch start and wait.

sceAudioPropagationSystemQueryMemory wrote a {size, alignment} pair at +0x00
and +0x08, which is not what SceAudioPropagationSystemMemory holds there: that
is the struct descriptor the caller has already initialized. The two size
fields live at +0x18 (CPU) and +0x28 (GPU), and the descriptor is now left
alone.

* [SpirvTranslator] Emit forward, nested control flow as structured selections

Every program with more than one basic block ran as a loop around a switch on
the guest PC. That shape keeps every guest register live across iterations and
stops the host compiler from optimizing across blocks, so on Metal the register
files spill.

A program whose branches only skip forward over properly nested ranges -- an
s_cbranch_* skip, an if/else with an s_branch over the else range, a skip to
the end of the program -- is exactly a nest of SPIR-V selections, and is now
emitted as one. The structuring is attempted first without emitting: if any
branch goes backwards, leaves a range it entered, or targets an instruction
that is not a block boundary, nothing is emitted and the program keeps the PC
dispatcher, so no program loses a shape the dispatcher handled. Setting
SHARPEMU_STRUCTURED_CF=0 forces the dispatcher for every program.

Measured on two replayed programs, with identical output: an instanced vertex
shader 8.1 -> 4.8 ms and a per-pixel compute program 10.7 -> 6.6 ms.

* [Agc] Batch the interpolant mapping's guest reads and register writes

sceAgcCreateInterpolantMapping and its version 2 run once per draw. Each one
read every semantic word with its own guest read — the output semantic table
was re-read from the start for every pixel input — and then wrote the 32
SPI_PS_INPUT_CNTL (register, value) pairs as 64 separate four-byte guest
writes, each paying the full guest write path.

Both semantic tables are now read once, the register block is built on the
stack (the identity pairs first, then the computed values over them), and the
whole block goes out in a single guest write. The output table is rented from
the pool when it is larger than a kilobyte. The bit arithmetic is unchanged,
and a new test pins version 1's output to the values the per-word
implementation produced.

Measured on Dreaming Sarah in-game: 8.6 us to 1.7 us per call, which is 5.0 s
down to 1.0 s of the main thread over about two minutes.

* [AudioOut2] Take every out pointer and struct size from the ABI

Out-parameter handling in libSceAudioOut2 was inferred at runtime: a helper
picked whichever of two argument registers looked like a writable pointer, and
another decided from the host address range whether a pointer was a guest
stack slot. Neither is a signal. The ABI names exactly one out pointer per
argument slot, and guest stacks sit at 0x7FFF_xxxx_xxxx on Windows but
0x6FFF_xxxx_xxxx on macOS, so the heuristic silently changed behaviour per
host. Every export now reads its out pointer from the register the prototype
gives it: ContextQueryMemory from rsi, PortCreate from rdx, PortGetState from
rsi, GetSpeakerInfo and GetSystemState from rdi.

The written sizes are now the real sizeof() of each struct.
SceAudioOut2PortState is 0x40 bytes, not 0x20. SceAudioOut2SpeakerInfo is
0x50, not 0x20, and its contents were invented; it now reports a TV speaker
type with front-left and front-right and the two stereo speaker angles.

sceAudioOut2SpeakerArrayCreate follows its real signature (pHandle,
pVbapParams, pAmbiParams): the handle goes to rdi, the speaker count and the
caller's buffer and size come out of the param block, and the handle is that
buffer when the title supplied one. The previous reading wrote the handle over
the VBAP parameters' speaker-position pointer and left the caller's handle
uninitialized. sceAudioOut2GetSpeakerArrayCoefficients writes
uiNumCoefficients floats to rsi and the ambisonics form writes them to rdx;
both used to zero a fixed 0x400-byte slab, and the ambisonics form could pick
its own count register as the destination address.

Errors are returned in the libSceAudioOut2 error space (0x80268xxx) rather
than as generic codes a title never compares against, and
sceAudioOut2UserGetSupportedAttributes is added.

sceAudioOut2ContextQueryMemory and sceAudioOut2ContextGetQueueLevel are left
exactly as they are, so that the two changes already in flight for them do not
collide with this one; the two address-range predicates survive for those two
callers alone.

* [Kernel] Memoize guest path resolution against the mount configuration

Resolving a guest path to a host path is pure text work plus a reparse-point
check per path component, but a resource streamer calls
sceKernelAprResolveFilepathsToIdsAndFileSizes with the same paths hundreds of
thousands of times over a load, so the work dominates.

Successful resolutions are now memoized. The only inputs that can change an
already-computed answer are the mount table and the host mount roots, so each
entry records the root configuration it was computed under and is ignored if
that has changed, and registering or removing a guest mount clears the cache
outright. A denial is not memoized: it is a containment decision about the
host filesystem's current shape, so it stays live. The cache is bounded, and
its size is tracked in a plain counter because ConcurrentDictionary.Count
takes every bucket lock, which on a path a dozen guest threads resolve at
once costs more than the resolution it guards.

* [Gpu] Route packed pixel color exports to the slots CB_SHADER_MASK enables

Shader compilers drop the MRTs a pixel program does not write, and the
hardware then delivers the Nth color export to the Nth target that
CB_SHADER_MASK enables — the export number and the target slot are not the
same thing. Unreal's base pass is the clear case: it exports MRT0 and MRT1 but
means targets 0 and 4 (SPI_SHADER_COL_FORMAT=0x54, CB_SHADER_MASK=0x000F000F).

Mapping each export to the target of the same number therefore wrote the
fourth G-buffer's data into the first, which showed up as a scene rendered in
world normals. Exports are now paired with the enabled slots in order, the
compiled pixel program keys its outputs by the export that feeds the slot
rather than by the slot, and a bound slot that no export reaches gets a zero
color write mask so it keeps its contents. When the two registers do not pair
up — different counts, or no formats at all — each export keeps going to the
slot of the same number, as before.

* [GPU] Clamp a volume render-target view to the volume depth

Unreal binds a whole volume as a render target with the last slice set to
its depth, one past the end, and the resolver stopped the session on it.
The color block writes only the slices the volume has, so the view is now
clamped to them. A view that starts past the last slice is still fatal.

* [Kernel] Remember APR paths absent from the read-only game image

Looking up a resolved path's size stats the host filesystem, and a path that
is not there costs two stats (a file probe and a directory probe) every single
time it is asked for. A resource streamer asks for far more paths than the
image contains, so misses are the common case and they are repeated.

/app0 is the read-only game image: a path that is not in it now will not
appear later. Absent paths under that root are therefore remembered, which
turns the repeated pair of host stats on what is often a slow external volume
into a dictionary probe. Paths outside /app0 can be created at run time and
are not remembered.

* [Kernel] Verify mount containment per component, not by listing directories

Containment verification stats each component of a resolved path once to reject
reparse points. It was changed to verify a whole parent directory in one
listing instead, on the theory that one listing is cheaper than N stats.

It is not, for a game image: .NET fills the attributes of every entry it
enumerates, so listing a directory stats all of it. A title whose asset
directories hold thousands of entries paid for every file it never opens --
directories of up to 7,390 entries, about 9 s of a load spent in those stats.

Each component is verified with its own stat again, and the per-component
memo that made the listing look attractive stays, so a component is still
verified only once.

* [Ngs2] Implement sceNgs2ParseWaveformData/File/User

The three waveform-parse entry points returned NOT_FOUND, and Void Terrarium
calls ParseWaveformData on its audio thread for every sound it loads.

They now parse the container and publish the description SceNgs2WaveformInfo
lays out (0xE8 bytes, with the block table at 0x48). A RIFF/WAVE fmt chunk
resolves to PCM by bit depth, to IEEE float, through the SubFormat GUID for
WAVE_FORMAT_EXTENSIBLE, or to ATRAC9, whose configuration is decoded so that
the audio unit size, samples per unit, units per frame and delay samples
describe the real frame and superframe. A smpl chunk supplies the loop points
and a fact chunk the decoded sample count. The "VAGp" container maps onto the
VAG waveform type at 28 samples per 16-byte HE-VAG unit.

ParseWaveformFile resolves the guest path and reads the header at the given
offset. ParseWaveformUser would have to drive a guest read callback, so it
reports INVALID_WAVEFORM_DATA rather than fabricating a description.

* [NpWebApi2] Add sceNpWebApi2CheckTimeout

Poppy Playtime calls it every frame, about 6k times a session, and each
call went through the unresolved-import path. No web API request is ever
in flight, so there is nothing to expire and it returns success.

* [Ime] Fill in sceImeKeyboardGetInfo and sceImeKeyboardGetResourceId

sceImeKeyboardGetInfo zeroed 0x88 bytes at its out pointer and returned
success. SceImeKeyboardInfo is 0x24 bytes -- six 4-byte members plus
int8_t reserved[12] -- so the call scribbled 0x64 bytes past the caller's
struct, and a title that reads the device type or the status got a zero for
both, which is not a state the library ever reports.

The export now writes exactly sizeof(SceImeKeyboardInfo) and reports an OSK
device in the DISCONNECTED state, which is what the host can honestly claim
since no physical keyboard is bridged. sceImeKeyboardGetResourceId returned
success without touching the caller's array, leaving the storage that
sceImeKeyboardGetInfo is called with uninitialized; it now publishes the
caller's user id with an empty id array. Both use the real error values.

Also adds a test for the existing scePadResetOrientation, which is a
handle-only call with no out parameter, so a bad handle is its only failure.

* [Gpu] Run an attachment-less draw whose vertex stage stores to memory

A draw with no color target, no depth target and no pixel stage was treated as
having no observable effect and dropped. That is wrong when the vertex or
emulated geometry stage stores to a buffer or a storage image: a GPU-culling or
stream-out pass whose only output is the buffer a later indirect draw reads
looks exactly like that, and dropping it leaves the indirect draw reading
stale data.

Such a draw now runs for its stores alone. The check only resolves the shader
programs when there is an export address to resolve, so a genuinely empty draw
still costs nothing.

* [AudioOut2] Write a single size_t from sceAudioOut2ContextQueryMemory

The export returns the context memory size through one size_t. For heap
output it also wrote a second qword (an alignment) after it, and titles keep
other locals there: Octopath Traveler II's allocator reads the dword at +12 of
that block as a divisor, so it faulted with #DE during boot. Write only the
size, wherever the output lives.

* [CPU] Reuse import trampolines and leave patched stubs alone on module reloads

Every runtime module load sets up the import stubs of all loaded modules
again, and each setup built a fresh trampoline and rewrote the stub's 16 bytes
-- under guest threads that may be executing exactly those bytes.

A trampoline only encodes its import index, so the trampolines are now cached
by index and reused, and a stub that already jumps to the trampoline it would
be given is left untouched, with no unprotect/write/restore cycle at all. A
reload of a module whose stubs are already patched therefore writes no guest
code.

The trampoline ABI test builds its backend with GetUninitializedObject, which
skips field initializers, so it has to supply the new cache; both fields now
go through one helper that names a field it cannot find.

* [Cpu] Read import stack arguments from the thread's own stack without a region query

Every import copies six stack argument slots (twice, with the per-thread
diagnostics). The per-thread readable-range cache is keyed by the host mapping
generation, which every tracker protection change bumps, so on macOS nearly
each slot fell through to a locked host region query: about half of the busy
managed samples at Octopath Traveler II's title were in that query or waiting
for its lock. Addresses inside the calling guest thread's stack, which the
backend maps read-write for the thread's lifetime, are now read directly.

* [HLE] Do not log expected wait timeouts, mutex relocks and closed pad reads

The import gateway logs an error result unless the NID is on a list of calls
whose failure is a normal outcome. Four common ones were missing:
scePthreadCondTimedwait and sceKernelWaitEventFlag time out exactly as the
caller asked, scePthreadMutexLock reports EDEADLK to the owner of an
error-checking mutex (titles use that to build their own recursive locks), and
scePadReadState reports a handle that is not open because titles poll every pad
slot.

Poppy Playtime logs roughly 260k of these lines per session, which buries the
failures that do matter.

* [Kernel] Run one mapping transaction per sceKernelBatchMap

A mapping change has to be handed off to the GPU worker before any lock the
image and buffer reads need, and the handoff waits for the GPU to drain.
sceKernelBatchMap walked its entries calling the same per-mapping entry points
the single-mapping exports use, so every entry in the batch took its own
handoff and its own drain.

The batch now opens one transaction and runs all of its entries inside it. A
thread-local flag marks that a transaction is already open so a nested entry
point runs directly instead of handing off again, and the flag is cleared in a
finally so an entry that throws cannot leave it set.

* [Loader] Rewrite SHA instructions in spans that touch no guest memory

The red-zone patcher computes a shifted core for every span: the run of
faultable guest memory accesses that executes with RSP moved below the
red zone. A span over register-only instructions has no such access, so
the core could not be computed and the span was refused as too short.
SHA rewrite sites are exactly that shape (sha256msg1/msg2/rnds2 on
registers), so no SHA instruction was rewritten any more: on hosts
without the SHA extensions every one trapped and was emulated one at a
time. Poppy Playtime logged sha_rewrites=0 with 144 too-short refusals,
hit hundreds of thousands of illegal-instruction traps and stopped making
progress within a minute.

A span with no guest memory access has nothing that must run outside the
shift, and the span builder has already refused any instruction that
touches RSP, so the whole span is the core.

* [Cpu] Keep fiber switches from writing stack bounds into the macOS TSD

The guest context-transfer stub that fiber switches go through keeps the
Windows TEB stack bounds in sync by storing the target stack top and
bottom to gs:[8] and gs:[16]. Unlike the guest-entry and exception stubs,
it did so on every host. On macOS gs addresses the pthread TSD, where
those slots hold errno's address and another key: a fiber switch wrote
its stack bounds over them, and the next failed libc call on the thread
(a timed-out condition wait) crashed reading errno through the pointer.
Demon's Souls died this way within 10-50 s of boot; a hardware
watchpoint on the thread's errno slot stopped in this stub.

The fiber bounds sync now lives next to the other TEB-bound helpers and,
like them, emits nothing off Windows.

* [Kernel] Take APR file sizes from the mount check's stat instead of listing directories

Resolving an APR path already stats every component to rule out links out of the mount.
The same stat now records the file's size, so size lookup no longer lists the file's whole
directory. Listing stats every entry: Demon's Souls keeps up to 7,390 files per asset
directory on a slow disk, and each first lookup in one paid for all of them.

FileInfo reports a missing component as -1 instead of throwing; it still ends the check.

* [Kernel] Place mappings in untouched address space without waiting for the GPU worker

Every mapping change queued behind the GPU worker so it could not race a pending GPU read.
During loading that worker is busy translating shaders, and Demon's Souls' reservations and
batch maps waited seconds for a queue slot (23.6 s over 132 calls, up to 7.1 s each) while
the change itself took about a millisecond.

A change that only places memory where the GPU was never given anything - a kernel-chosen
address, or a fixed range that is empty or reserved and overlaps no registered span - cannot
race a GPU read and has nothing to unregister, so it now runs on the calling thread. Those
paths also skip the unregister request for free and reserved ranges, which are never
registered; waiting on the worker while holding the mapping lock that GPU reads take would
deadlock, so reaching a registered span there throws instead.

* [HLE] Resolve single-view backing reads and writes without the views lock

Every HLE access to aliased guest memory took one lock shared by all guest threads, and
Demon's Souls' job threads queued on it for 8-byte mutex reads. Readers now binary-search a
copy of the view list; mutations drop the copy under the lock and the next reader rebuilds
it. A reader that races a mutation uses the list from before it, as if it ran first.

* [ShaderCompiler] Find each instruction's block by search and reuse register files

Several passes found a block's instructions by scanning the whole program once per block,
and the value graph did it again on every block visit. Blocks are sorted and contiguous, so
IrControlFlowGraph.BlockOf finds an instruction's block by binary search and each pass
groups the program once.

The value graph also allocated two 256-entry register files per block visit. Large Demon's
Souls compute shaders visit thousands of blocks, and the collections that caused stopped the
renderer for about 2.6 s of its first-frame shader work. Replaced entry and exit states are
now reused; a reused state is reset exactly as the constructor would, at the same point, so
the undefined values it starts with and the instructions they are charged to are unchanged.
The resource graphs and SPIR-V of the game's first 134 programs are identical.

* [Cpu] Deliver guest access faults on macOS as signals, not runtime Mach exceptions

The runtime receives EXC_BAD_ACCESS for its threads through a Mach exception port before
the kernel raises SIGSEGV or SIGBUS. It treats a fault in the page holding RSP, or the page
below, as a stack overflow and aborts - assuming RSP is on a stack it owns. Guest code runs on
guest stacks whose pages fault whenever GPU or image write tracking protects them, so Demon's
Souls died with a bare "Stack overflow." about every other minute of play.

Threads now clear that port the first time they enter guest code. Their access faults reach
the signal handler, which resolves guest faults and chains the rest to the runtime's own
signal handlers, as on Linux. Each tracking fault also skips the Mach round trip, which
roughly doubled Demon's Souls' frame rate (about 2 to 4-5 fps).

* [ShaderCompiler] Solve user-data liveness on bit sets and hash graph nodes by id

The liveness fixpoint that picks a program's user-data registers walked 256 booleans per
block per pass; it now works on four 64-bit words per block with the same equations.

ScalarValue keys many graph dictionaries. It keeps reference equality but hashes its unique
id, which skips the runtime's identity-hash path.

* [SpirvTranslator] Mask a wave32 saveexec result at 64 bits

The low-half mask for s_*_saveexec_b64 on a 32-lane wave used the 32-bit AND helper on the
64-bit exec value, emitting OpBitwiseAnd %uint with a %ulong operand. spirv-val rejects the
module and the driver's behaviour for it is undefined; Octopath has four such programs.

* [SpirvTranslator] Structure loops, early exits and single-block programs

Only forward, nested branches were emitted as structured control flow; every program with a
loop, and every single-block program, went through the loop around a switch on the guest PC,
which keeps all registers live and stops the host compiler from optimizing across blocks.

A block that a later block branches back to - on a wave-uniform condition or with s_branch -
now opens a SPIR-V loop whose body is structured like any range, whose continue evaluates
that branch's condition, and whose iterations share the dispatcher's step guard so a guest
loop that never ends still cannot wedge the GPU queue. A conditional branch from the body to
the block after the latch is a break out of the innermost loop. Any other backward edge, or a
break past an outer loop, keeps the dispatcher as before.

Across eight games, programs left on the dispatcher went from 760 to 104 (Poppy, Silent Hill,
Oregon Trail and Dreaming Sarah to none); every emitted module passes spirv-val and the
frames match. Octopath's 12.5M-invocation instanced draw went from 12.2 to 10.8 ms.

* [Gpu] Age cached images by frame, not by guest submission

The image collector runs after every completed guest submission as well as at each flip, and
every run advanced the age it compares against. A title submits dozens of command buffers per
frame, so "unused for 16 ticks" meant a fraction of a frame: every texture sampled once a frame
was deleted and recreated, and Superliminal uploaded 3,591 textures (2.2 GB) every 5 seconds.
Deleting a GPU-written image the cache cannot read back also dropped its contents.

Only a flip now advances the age. Collection after a submission still runs, so memory pressure is
relieved during long stretches without a flip. Superliminal's per-frame uploads went from ~55 to
none, and its intro scene from 12 to 45 fps.

* [Gpu] Submit on a release-memory label write only when it raises an interrupt

A RELEASE_MEM that writes a 32-bit label submitted the open batch at once. Without an interrupt
nothing inside the slice waits for the label, and every slice that makes progress already ends
with a submit - including one that stops at a WAIT_REG_MEM on that label - so the label is
written when that batch completes either way.

The per-label submits split Superliminal's frames into ~35 extra command buffers; each ended the
render pass, which stores and reloads every attachment on a tile-based GPU. The title menu went
from 15.6 to 21 fps and Octopath from 13-21 to 18-29 fps, with no hang over 150 s runs.

* [VideoOut] Reinterpret the sRGB-encoded frame on a presenter image, not the swapchain

A floating-point display buffer is sRGB-encoded by blitting it into an sRGB image and then
copied byte-for-byte into the UNORM swapchain image. MoltenVK does that copy through a
reinterpreting texture view it caches on the destination; on a presentable image the drawable
behind it changes every frame, and destroying the swapchain then walked those views through a
freed device. Superliminal, whose display buffers are RGBA16F, crashed at every exit (SIGSEGV
in MVKImagePlane::releaseMTLTexture from vkDestroySwapchainKHR) and each crash kept two crash
reporter processes busy for minutes.

The encoded bytes are now copied into a presenter-owned image in the swapchain's format first,
so the swapchain image only ever receives a same-format copy. Frames are unchanged; the exit is
clean.

* [Cpu] Stop guest threads at their next import once the host shuts down

Host shutdown only kept guest threads from starting another slice; a thread already inside
guest code kept calling HLE while the services behind it were torn down. Superliminal's job
threads then got a null command buffer from the GPU library, wrote through it, and the process
died with SIGSEGV on its way out.

After shutdown is requested an import now returns to the host entry stub instead of running,
ending that thread's slice; the scheduler already refuses to enter guest code again.

(cherry picked from commit 9e0e8ec0aa8b0bfaa030e5e97d51dbc0223f93e1)

* [Gpu] Declare every parameter the pixel program reads in the paired vertex program

A vertex program declares outputs for the parameters it exports plus as many locations as the
pixel program has inputs. SPI_PS_INPUT_CNTL can map a pixel input to any parameter number,
though, so a pixel program with two inputs may read parameters 4 and 5. Metal requires every
fragment input to be written by the vertex stage, so the pipeline failed to link ("fragment
inputs user(locn4), user(locn5) ... not written by vertex shader"), the command stream slice
failed and Silent Hill stopped rendering at a scene change.

The required output count now covers the highest location the pixel program reads, resolved
through the same input mapping its translator uses; missing exports are zero-filled as before.
The pipeline creation error also names the vertex and pixel programs.

(cherry picked from commit 80ec9fb53cfae922e458e2dd43d4b1010dbd2701)

* [Kernel] sceKernelAprGetFileSize writes the resolved file size

The export was a success stub that never wrote its out parameter, so callers
sized buffers from uninitialized memory. The signature is (id, uint64_t* size);
it now answers from the APR file registry like sceKernelAprGetFileStat.

* [Cpu] sceKernelWaitSema is an import-loop guard boundary

A thread that blocks on a semaphore between iterations is waiting, like the
condition-variable waits already on the list. A job-wait loop that runs queued
work between timed semaphore waits was otherwise taken for a hang once loading
outlasted the guard window.

* [Fiber] Avoid formatting unused names and disabled traces

* [Shader] Build materialization cache ranges without temporary copies

* [ShaderCompiler] Intern scalar graphs without string keys or hit allocations

* [Media] Vectorize movie YUV conversion without changing pixels

* [Shader] Reuse scratch storage for resource read recording

* [Tests] Initialize presenter state for GPU binding validation

* [Vulkan] Keep temporary descriptor writes off the managed heap

* [GPU] Reuse bounded descriptor preparation scratch arrays

* [ShaderCompiler] Persist versioned scalar analysis across launches

* [GPU] Remove duplicated image layouts from texture bindings

* [ShaderCompiler] Bind cached graph payloads to their input keys

* [Vulkan] Load content-addressed driver cache shards on demand

* [Tests] Keep presenter allocation fixture independent of diagnostics

* [Vulkan] Forget driver cache identities of destroyed shader modules

The remaining change of the updated #1006 head (e6659681) that the integration branch did not carry.

* [Vk] Measure GPU busy time from per-submission timestamps

Per-draw timestamps are written only with SHARPEMU_PROFILE_GPU_DRAWS=1; by
default each submission gets two timestamps and the overlapping intervals
are merged into a [PERF][GPU_BUSY] line with the busy share and the longest
submission.

* [HLE] Reserve a host core for the render thread

On Windows hosts with 8 to 64 logical processors the render thread gets the
last physical core of the fastest efficiency class at the highest priority,
and guest threads are kept off that core so spinning job workers cannot take
the renderer's SMT sibling. SHARPEMU_RENDER_CORE=0 turns it off.

* [Vk] Spin briefly before blocking on a GPU wait

A GPU wait polls the timeline for SHARPEMU_GPU_WAIT_SPIN_US microseconds
(150 by default) before it sleeps, which saves the wake-up latency when the
work completes soon.

* [GpuMemory] Answer CPU-dirty range queries without the tracker lock

MayHaveCpuDirtyPages reads the per-block summary and the region masks without
taking the region lock; a true answer still goes through the locked path.
Regions also bump a CPU-dirty epoch whenever a page becomes CPU-dirty.

* [Gpu] Skip the device-address sweep while no page became CPU-dirty

PrepareBda remembers the CPU-dirty epoch and the mapping it swept last and
skips the full sweep of mapped memory until either changes.

* [GpuMemory] Scan possibly CPU-dirty blocks 64 at a time

ForEachPossiblyCpuDirtyRange combined a region lookup and a summary read for
every 4 MiB block of mapped memory on each device-address sweep. A bitset of
blocks that have a region and the dirty summary words are now scanned a word
at a time, with the same runs as before.

* [Gpu] Skip syncing a written buffer range that is already GPU-owned

SynchronizeBuffer returns early when a written range lies entirely in the
GPU-modified ranges, uses the lock-free CPU-dirty query for reads, and the
render profile splits buffer sync time into written, texel and upload parts.

* [Gpu] Version the GPU-modified buffer ranges

A written range bumps GpuModifiedVersion only when it adds bytes to the
GPU-modified set, so readers can cache a clean verdict for a page until the
GPU writes something new.

* [Gpu] Track the extent of DCC metadata and answer overlap queries

A color target records the size of its DCC metadata, and the image cache can
tell whether a guest range overlaps registered DCC metadata.

* [Diagnostics] Trace the GPU writers of DCC metadata and of a chosen address

SHARPEMU_TRACE_DCC_WRITERS=1 writes the shader program and a call stack for
GPU buffer writes that overlap DCC metadata; SHARPEMU_TRACE_WRITER_ADDRESS
does the same for writes that cover one guest address.

* [Gpu] Fill DCC metadata on the GPU and in guest memory at once

A fill that targets DCC metadata which no GPU write owns is recorded as a GPU
fill and also written to the guest backing directly, so the range never
becomes GPU-dirty and later CPU reads need no readback. Ranges the GPU still
owns or that overlap an image keep the plain buffer fill.

* [Gpu] Turn AGC fill compute shaders into buffer fills

BoundedFillDetector recognizes the AGC range-fill and pattern-fill compute
programs, including thread-dimension dispatches, and a dispatch of one that
targets DCC metadata becomes a DCC fill instead of a compute dispatch.
Refused conversions are reported by the DCC writer trace.

* [HLE] Resolve backing aliases for readers that hold an access window

SharedBackingViews publishes its view snapshot as an alias identity, and a
reader can enter an alias access window once and resolve guest pages to their
backing alias without the views lock until the snapshot changes.

* [Diagnostics] Trace shader resource reads that wait for GPU-written memory

SHARPEMU_TRACE_GPU_READS=1 writes the shader, stage, address and table phase
of a resource read that has to synchronize GPU-written memory, once per shader
and page.

* [ShaderCompiler] Keep recent materialization variants per draw key

A cache key now holds up to four materializations in most-recently-used order,
so alternating descriptor contents reuse their earlier result instead of
re-materializing. The cache report counts stale and unreadable entries and
table refreshes, and ReadingTable tells a reader when the flattened table is
being evaluated.

* [VideoOut] Read resource words through cached clean pages and the backing alias

TryReadGuestWord and TryReadResidentGuestBytes remember the last 64 pages
found free of GPU-dirty bytes, keyed by the GPU-modified version, and read
them through the backing alias inside the alias access window that each
command stream slice now holds. GPU-dirty checks are byte-precise, and a
resident read refuses unreadable ranges before it asks the page tracker.

* [Gpu] Read GPU-written guest memory back asynchronously and ahead of time

VulkanAsyncReadback keeps eight slots: slot zero serves synchronous reads and
the others carry asynchronous downloads. A guest thread that reads GPU-written
memory lets the render thread start the copy and waits for it on its own
thread, then the render thread applies it if the range is unchanged; a
foreign read keeps the source buffer alive until it completes.

Windows the render thread had to read synchronously become hot. When a batch
writes into a hot window, the window is downloaded right after the batch is
submitted, and the next read uses that copy if it is still current. A pending
write to a hot window ends the open batch early. SHARPEMU_ASYNC_GUEST_READBACK=0
keeps the synchronous path; [PERF][ASYNC_READBACK] reports the counts.

* [ShaderCompiler] Prefetch contiguous resource words in one resident read

Resource materialization read every descriptor and flattened-table word
through its own guarded TryReadGuestWord call, about 2.6 million per five
seconds in Demon's Souls. The raw reads of each plan are grouped once; at run
time their addresses are resolved, sorted and read as contiguous runs through
the resident guest reader, and the values seed the evaluator cache. A run the
GPU may still own, or one out of bounds or through a null base, falls back to
the word reads. The cache recorder records the prefetched words, and
SHARPEMU_RESOURCE_PREFETCH=0 turns the prefetch off.

* [Gpu] Reuse image lookups that found the same backing

FindImage remembers, in a 256-slot table, each request that resolved to an
image with the same backing, keyed by the request bytes and a generation
that every index registration or removal advances. A repeated request
touches the image and returns the remembered result without walking the page
owner index. SHARPEMU_IMAGE_LOOKUP_MEMO=0 turns it off; [PERF][IMAGE_LOOKUP]
reports hits and misses.

* [ShaderCompiler] Look up fused programs without the registry lock

Every shader header lookup and decode asked the fused-program registry under
its lock. The registry now publishes an immutable dictionary that readers
take with a volatile read, and a registration copies it under the lock.

* [ShaderCompiler] Decode fused programs whose halves are far apart

The fused decoder joined the continuation at its address distance from the
entry and refused halves more than 4 GiB apart or in the wrong order, which
Poppy Playtime now hits for a vertex shader (invalid-fused-layout). Such a
continuation is placed right after the entry instead, and each of its
instructions keeps its real offset from the program address in AddressOffset;
S_GETPC_B64 in the scalar graph, the SPIR-V and the MSL translators uses that
offset, and the persisted scalar analysis key includes it.

* [Gpu] Collect an image only once it is old enough in frames

Since images age by frame, clamping the age to the current frame made every
image of the first frame collectable during the first 16 frames, and a
submission that ended no frame could delete a texture the frame still used.
Without memory pressure nothing is collected until the full age has passed;
under pressure the clamp still lets collection relieve memory early.

* [Tests] Cover the deferred global barrier of an attachment-only scope

A global barrier still ends a scope whose draws store to memory. In a scope
whose draws only write attachments it waits for the scope to end, and a later
memory-writing draw ends the scope and records it.

* [Agc] Make the number of suspend points in flight configurable

CommandStreamQueue takes the number of frame boundaries that may wait behind
the graphics commands. The default stays one, so the guest CPU can run one
frame ahead, and SHARPEMU_SUSPEND_POINTS_IN_FLIGHT=0 makes a suspend point
wait until its own boundary is processed, for diagnosis. Tests cover both
modes, including a label that the CPU rewrites after the suspend point.

* [Gpu] Throttle the device-address fault and 3D slice range logs

Little Nightmares wrote 195 thousand "Accessed non-GPU cached memory" lines in
one session and Silent Hill 6800 slice range lines. Both now print the first
occurrences and then only at power-of-two counts, with the count.

* [Docs] Document the new environment variables

Adds SHARPEMU_ASYNC_GUEST_READBACK, SHARPEMU_GPU_WAIT_SPIN_US,
SHARPEMU_IMAGE_LOOKUP_MEMO, SHARPEMU_RESOURCE_PREFETCH,
SHARPEMU_SUSPEND_POINTS_IN_FLIGHT, SHARPEMU_RENDER_CORE,
SHARPEMU_TRACE_DCC_WRITERS, SHARPEMU_TRACE_GPU_READS,
SHARPEMU_TRACE_WRITER_ADDRESS and SHARPEMU_PROFILE_GPU_DRAWS, and corrects the
group counts to the rows the tables hold.

* [GpuCommands] Write the 64-bit label of a compute-done release with an interrupt

RELEASE_MEM with event 0x2F and a 64-bit data selection was accepted only
without an interrupt. It is now handled like the other end-of-pipe events:
the label is written and the interrupt is raised after it.

* [GpuCommands] Let a wait see its own queue's preceding label store

A memory wait that directly follows a 4- or 8-byte store of its queue to the
same address, with only waits or inert NOPs and markers between them, compares
against the stored value when guest memory no longer holds it. Dead Cells
writes a label and waits for it while the CPU already rewrites it for the next
frame. The forwarded value lives only within one slice, and any draw, dispatch
or transfer in between drops it.

* [ShaderCompiler] Key fused programs by the underlying guest memory

Each native guest thread wraps the guest memory in its own tracking wrapper,
so a fused program registered while one thread created the shader was not
found by the render context. The registry now unwraps ICpuMemoryWrapper
before it looks up the per-memory table.

* [Agc] Implement the legacy sceAgcFuseShaderHalves export

The older NID (nApJjpKNBl4) shares the fusing path but keeps the front half's
user data pointer, takes the larger value of the resource allocation fields
of both halves, takes the user SGPR count and the ES component flag from the
front half, and copies the checksum register of the matching stage.

* [ShaderCompiler] Decode S_CMOV_B64 as a conditional select

S_CMOV_B64 is decoded as S_CSELECT_B64 with the old destination pair as the
false source, so it reuses the SCC, register-pair and EXEC handling of the
translators and of the scalar value graph.

* [Gpu] Merge the stencil replacement references of a face

Vulkan has one stencil reference per face for the comparison and every
replace operation. Replacements with the test value and with the operation
value are now merged into that reference over the written bits, and refused
only when they conflict on bits the comparison also reads. A replacement
with an operation value of zero over the written bits becomes a zero
operation, which ASTRO's PLAYROOM uses to clear one stencil bit.

* [Json] Add the sce::Json::Value copy constructor and keep shadow values on copies

The copy constructor (fSb2oQTNrgA) shares the assignment path. Assignment
and reads now carry the value a constructor or setter recorded in the object
heap, so a copy keeps it after the source is destroyed.

* [Font] Implement sceFontTextSourceInit

Fills the 0x60-byte text source with the text range, the cursor, the parser
callback and its object, and the text size, and leaves the memory after it
untouched.

* [AudioIn] Answer sceAudioInGetSilentState without an input backend

No AudioIn port can be opened until an input backend exists, so the export
returns an AudioIn error code (0x80260101) instead of being an unresolved
import.

* [PhtreadSpecificSemanticsTests] Fixes for computed shaders

* [ShaderCompiler] Return the entry header address of a registered fused program

An overload of TryGetFusedProgramParts also gives the entry header, so a
caller can register the same fused program on another memory.

* [Vulkan] Keep saving a pipeline cache that outgrows its limit

A driver cache over 256 MiB was refused on every save, so it was never
written again and every launch compiled the same pipelines. Such a cache
is now written as an empty file once, and the next launch starts a fresh
one. A save is skipped when the driver reports the size that was loaded
or last saved, and a failed or deferred save waits for the next interval
instead of running again on every new pipeline.

* [Vulkan] Write only the cache shards that gained pipelines

A shard remembers the driver size it was loaded or saved with and is not
written again while that size holds. Shard saves are logged as one
summary line, and a shard load only when it is slow.

* [Vulkan] Remove the single-file cache once the shard mode takes over

The shard mode never reads the single cache file of the default path, so
it is deleted instead of staying on disk.

* [Gpu] Record compiled compute programs in a shader prewarm list

When the pipeline host gives a list, each compute permutation the game
compiles is appended at once to shader-prewarm.bin: the guest code the
decoder read (once per program), the fused parts, the compute input, the
system registers, the resource specialization and the push data start.
Records carry a checksum, so a cut tail is dropped at the next open.
TryCompilePrewarm rebuilds the compile request from a record through the
same layout and request code as the runtime, so it produces the same
SPIR-V.

* [Vulkan] Prewarm the recorded compute pipelines after a translator or driver change

A first run after a translator change compiled every compute pipeline
one at a time while the render thread waited (Demon's Souls: 282
pipelines, 144 s). When the translator assemblies or the driver differ
from shader-prewarm.stamp, background threads at below-normal priority
translate the listed programs again and create their pipelines, so the
driver has them before the game asks. With the same build and driver the
launch does no prewarm work. The log reports the prewarm result and how
many runtime compute modules it produced.

* [Vulkan] Keep the pipeline cache in memory on Windows

The GPU driver keeps its own shader cache on disk, and the shader prewarm
list rebuilds it after an update, so Windows no longer writes the driver
cache file by default. SHARPEMU_VK_PIPELINE_CACHE=file keeps one; Linux
keeps the file and macOS keeps its shards.

* [Docs] Document the pipeline cache modes and the shader prewarm list

Adds SHARPEMU_SHADER_PREWARM, SHARPEMU_VK_PIPELINE_CACHE_SHARDS and
SHARPEMU_VK_PIPELINE_CACHE_LEGACY_SHARDS, and describes the new cache
file default.

---------

Co-authored-by: kuba <xnetcat.dev@gmail.com>
2026-10-03 04:49:43 +03:00
Mathias e01198a147
Fix/gen5 shader resource handling (GTA 5) (#1015)
* fix(gpu): upload complete dirty pages for cached image sources

* perf(shader): hoist single-block loop entry guards

* fix(shader): support scalar quad masks in resource analysis

* fix(gpu): use native Vulkan fill for three-vertex rectangles

* test(stdio): wait directly for concurrency workers
2026-10-03 04:00:32 +03:00
Mathias 6cac2b5f47
fix(agc): patch indirect-buffer continuation links (#1012) 2026-10-03 00:51:09 +03:00
Mathias 6cebf7ec56
Fix/vulkan wave64 ballot masks (#1011)
* fix(shader): preserve wave64 ballot masks on Vulkan

* test(shader): cover subgroup lane bits in wave mask predicates
2026-10-02 15:10:39 +03:00
ParantezTech e7a4f0a0ce [Gpu] Expose HDR output support and handle PQ buffer attribute changes 2026-10-02 03:37:17 +03:00
ParantezTech f199acd42f [SharedBackingViewsTests] using await Task.WhenAll instead Task.WaitAll 2026-10-02 02:57:25 +03:00
Berk 5a3755fbdc
[GUI] revision of default settings (#1009)
* [GUI] revision of default settings

* [test] fix performance profile settings tests to use the new default values
2026-10-02 02:51:50 +03:00
Thegamerx808 8e1c6255b8
libSceFont: scale layout metrics with font size and accept layout pointer in rdx (#1008)
sceFontGetHorizontalLayout returned fixed 16px-era values regardless of
the scale set with sceFontSetScalePixel. Games that size their render
surfaces from the reported baseline (Astro Bot) ended up with surfaces too
short for the requested glyph size, so DrawGlyph cropped the bottom half of
every character. The layout values now derive from the font's current
pixel height, matching the ascent convention already used by glyph
metrics and the rasterizer.

Some callers pass the layout output pointer in rdx instead of rsi, which
the stub read exclusively, causing an INVALID_ARGUMENT return and Astro
Bot's own assert in FontSystem.cpp followed by the string being skipped
entirely. Fall back to rdx before failing.
2026-10-02 02:07:06 +03:00
ParantezTech e007d43181 [readme] update readme with new image 2026-10-01 21:44:07 +03:00
Berk 36acf4a40e
Des dcc fixes (#1000)
* [SpirvTranslator] Take high products from extended multiplies

Co-authored-by: TarkusR <90155059+TarkusR@users.noreply.github.com>

* [SpirvTranslator] Keep shader flags in uint variables

Co-authored-by: TarkusR <90155059+TarkusR@users.noreply.github.com>

* [SpirvTranslator] Synchronize shared memory phases in multi block shaders

Co-authored-by: TarkusR <90155059+TarkusR@users.noreply.github.com>

* [GPU] Merge device address ranges before buffers are bound

Co-authored-by: TarkusR <90155059+TarkusR@users.noreply.github.com>

* [GPU] Track per-slice guest DCC clear metadata

* [GPU] Materialize guest DCC clears in color and sampled images

* [GPU] Preserve color targets without pixel shader exports

* [ShaderCompiler] Track wave-indexed image descriptor waterfalls

* [ShaderCompiler] Analyze wave64 half masks and shared-memory hazards

* [ShaderCompiler] Pass guest LDS size to SPIR-V compilation

* [SpirvTranslator] Synchronize wave64 half masks through LDS exchange

---------

Co-authored-by: TarkusR <90155059+TarkusR@users.noreply.github.com>
2026-10-01 21:15:55 +03:00
ParantezTech 658e839933 [readme] hi from astro-bot 2026-10-01 15:46:36 +03:00
ParantezTech 5d640293c2 [readme] added new screenshot 2026-10-01 14:23:10 +03:00
T a424a67c1c
[GPU] Refresh the stencil plane through the depth image (#989) 2026-09-30 20:14:49 +03:00
ParantezTech b480391a7b [SpirvTranslator] perf: Clear vertex shader interpolation parameters before translating to SPIR-V
Co-authored-by: Astellou <mathieu.richardeau@gmail.com>
2026-09-30 19:01:44 +03:00
Astell c076513bd6
Astrobot / Demon's soul performance improvement (#955)
* [Gpu] Astro Bot performance: 14 -> ~60 fps at the title scene

- Defer RELEASE_MEM command-buffer flushes instead of submitting per packet
- Resolve indexed indirect draws and dispatches on the GPU
- Async buffer readbacks on a dedicated transfer/compute queue
- Pool image backings, flip snapshots and tiler resources
- One-frame command-stream run-ahead
- Lock-free backed-range and image page-owner checks on CPU writes
- Scalar value cache with generation-stamped reset; memoized invariant phis
- Shader compiler: structured forward-only CFGs with function-scope
  registers, EXEC-full guard elision, bounded V_MOVREL offsets
- Trivial-leaf import dispatch and strchr/strrchr fast paths
- SHARPEMU_GUEST_REFRESH_RATE option

* [Tests] Fix full-suite flaky tests

- Red-zone patcher: when every fixed trampoline candidate is taken, look
  for any free host range within relative-jump reach
  (PhysicalVirtualMemory.EnumerateFreeHostRanges); the test places its
  image in a probed free hole
- Kernel memory tests: probe a free PRT-area address instead of the fixed
  0x1026C00000; the reservation-extent test only rejects picks inside
  the reservation
- Release checks tolerate another thread reusing the freed range
- Allocation tests measure on a dedicated thread and keep the least
  allocating of several rounds (AllocationMeasurement.SteadyState)

* [Gpu] Astro Bot: structured loops, buffer slabs, pad motion

- Shader: emit natural loops as structured SPIR-V loops (OpLoopMerge)
  instead of the block dispatcher, and merge SCC-uniform if/else joins
  as selects so descriptors picked on a branch stay evaluable per draw.
- Shader: decode V_FFBH_I32 and V_MIN3_F16; read the DS GDS bit from
  bit 17 (GFX10) and route GDS append/consume through shared atomics.
- Gpu: sub-allocate small device-local buffers from shared memory slabs.
- Pad: report motion data by default (Astro Bot never imports
  scePadSetMotionSensorState), convert acceleration from m/s^2 to G,
  and add scePadResetOrientation / scePadSetAngularVelocityDeadbandState.

* [CPU] Stop pinning guest threads to host cores by default

Guest affinity packed Demon's Souls' 13 spinning BPE job workers onto 10 host lanes and put its renderer next to them at equal priority, so the renderer and lock holders waited out whole scheduler quanta. Intro 10.6 -> 44.7 fps, menus 11.6 -> 47.5 fps. SHARPEMU_GUEST_AFFINITY=1 restores pinning.

* [Media] Convert host movie frames to YUV on the decoder thread

The render thread converted every decoded Bink frame from BGRA back to YUV 4:2:0 (6.7 ms per frame). The decoder thread now produces the planes and the presenter uploads them as-is. PS Studios logo 33 -> ~50 fps, attract movie 44 -> 60 fps.

* [Agc] Accept a zero destination in sceAgcDcbWriteData

Demon's Souls emits WRITE_DATA with address 0 into a template and patches it after copying. Returning null made the title patch a relocated null pointer, which wrote a stack-derived value into its label and was then parsed as an unknown NOP marker (GPU FATAL).

* [Loader] Patch short red-zone faulting accesses next to branches

A faulting access shorter than a jump could not be relocated when followed by a branch or a branch target, so a handled fault let Windows overwrite the red zone. Spans may now end with a direct branch (run after the stack pointer is restored) or start up to three instructions earlier. Fixes a Demon's Souls crash after the new-game intro movie.

* [GPU] Astro Bot rendering fixes: compute subgroups, DCC clears, PS inputs, texture MIN_LOD

- Compute: pin non-emulated compute pipelines to a 32-lane subgroup
  (VK_EXT_subgroup_size_control) so wave64 groups other than 64
  invocations keep lanes 32..63 active on AMD (striped froxel volume).
- Shader translator: 64-bit AND for the wave32 S_*_SAVEEXEC_B64 mask.
- PS inputs: slots that read one VS parameter share one host input;
  a smooth/flat slot next to a V_INTERP_MOV slot is rebuilt from the
  per-vertex values (black save cards, NaN normals, black props).
- Render targets: drop unwritten color targets of depth passes from both
  the attachments and the pixel outputs, keeping locations aligned.
- DCC: materialize pending zero fast clears before shader access so a
  later attachment bind does not wipe compute writes.
- Textures: honour the T# MIN_LOD with VK_EXT_image_view_min_lod so
  streamed textures never sample unloaded mips (blown-out stadium sky).
- Device: enable VK_KHR_maintenance5 for point lists without PointSize.

* [Audio][GPU] ATRAC9 independent mono streams, BC storage fallback, 32-bit bit counts

- LibAtrac9: decode config header 0x30 streams (PS5 ambisonics), where every
  channel is an independent mono stream interleaved frame by frame.
- Images: when the driver refuses storage usage on a block-compressed image,
  compute writes go through an uncompressed replacement of the same memory.
- SPIR-V: count 64-bit masks with two 32-bit OpBitCount, which Vulkan requires
  without maintenance9.
- Agc: drop the indirect-draw emit debug log; report the draw modifier in the
  unsupported indirect draw error.

* [GPU] Partial texture-pool uploads and cheaper per-draw preparation

- A CPU write into a large layered texture (Demon's Souls' 256-layer BC3
  pools) re-uploaded the whole image, ~750 ms of GPU detiling in one frame.
  Each (mip, layer) piece's guest bytes are hashed at upload; a refresh
  uploads only the pieces that changed.
- Device-address preparation touched and checked every registered buffer for
  each dispatch. Recency touches now run once per retirement tick, and uploads
  only visit buffers in blocks the tracker does not know to be clean; hot
  pages stay dirty, so they are still re-uploaded every time.
- Resource materialization cache: when the only changed words are ones the
  flattened table alone reads (per-frame inline constants), only the table is
  evaluated again.
- A full batch no longer splits an open render pass before 512 draws.
- Lock-free lookups for shader headers, device format properties and the idle
  host movie bridge; the SPIR-V dump toggle is read once per program cache.

* Demon soul - fix texture

* Delete runahead tests

* Screcrow fix for nvidia GPU
2026-09-30 18:14:21 +03:00
Berk d0dd3fe102
Gran Turismo 7 Fixes, AvPlayer, Razor CPU stubs, Net stubs (#927)
* [AvPlayer] improved video playback sync and stats

* [AvPlayer] enabled threaded video decoding

* [Fiber] fixed current fiber address query

* [Font] added bitmap glyph rendering

* [GPU] added unsigned scaled texture sampling

* [Network] added socket receive and resolver support

* [NpManager] added offline signup status

* [NpSessionSignaling] added signed-out request stubs

* [SigninDialog] added dialog stubs

* [TextToSpeech] added initialization stubs

* [SystemService] added music player disable stub

* [VideoOut] added VRR stubs

* [RazorCpu] added marker stubs

* [Fiber] updated current fiber address test

* [Pad] Added scePadGetFeatureReport stub

* [Pad] For now, extra controller definitions should remain as stubs

* [Runtime] Module loader correct order: libc > libSceNpCppWebApi > libScePfs > logiWheel

* [SystemService] Added sceSystemServiceReenableMusicPlayer stub

* [VideoOut] Add sceVideoOutVrrUnpegFromFixedRate stub

* [Cpu] Reuse exited guest thread stack and TLS regions

* [Kernel] Isolate temp0 storage per game process
2026-09-30 01:37:56 +03:00
ParantezTech 50198a694b [Kernel] Return packed directory entries for Quake II 2026-09-30 00:56:20 +03:00
ParantezTech 4d65ff6d51 [Test] Prevented other tests from running the same alignment test in parallel 2026-09-29 19:32:08 +03:00
Berk a9e1c25968
[Ampr] Fix PakDirectoryTracker to handle multiple directories with the same name (#925) 2026-09-28 04:43:05 +03:00
ParantezTech b4c065a0b0 [GPU] performance: negative interval filter 2026-09-28 04:37:46 +03:00
okidooh 4799a1f82f
test(shader): cover SAshrI64 translation (#914) 2026-09-28 02:59:51 +03:00
Foued Attar e17fb6fed1
Fix for DS,Little Nightmares, Little Nightmares III and Astro Bot shader (#913)
* shader(decode): fill MIMG float-atomic and SOP1 opcode gaps

Gen5ShaderTranslator's opcode name tables were missing several real
RDNA2 opcodes that Ghost of Yotei's shaders use: MIMG 0x1D-0x1F
(ImageAtomicFcmpswap/Fmin/Fmax) and SOP1 0x15/0x1B/0x1C/0x1E/0x34/0x3B
(SFlbitI32B32, SBitset0/1 B32/B64, SAbsI32, SBitreplicateB64B32).
Without names these decoded as unknown instructions and aborted
translation before the pipeline ever reached GPU submission.

* shader(resources): accept float-numeric-class atomic image descriptors

ResourceMaterializer rejected any atomic image descriptor whose guest
format wasn't exactly Format32Uint, but real atomic-float images
(ImageAtomicFmin/Fmax) arrive with a float-numeric-class format. Relax
the check to also accept formats whose SampledNumericClass is Float,
and default the null-descriptor's NumericClass to the image's own
class for atomics instead of always Float.

Also surface the actual materialization failure reason in
ShaderProgramCache's error message instead of a generic string, so
each subsequent blocking bug could be identified from the log without
extra instrumentation.

* shader(bindings): add a dedicated atomic-float image binding range

BindingLayout only had a single Atomic-Uint binding range (32-36), so
any atomic image with a float numeric class had nowhere to bind and
ForImage() returned null for it. Add AtomicFloatBinding (37-40),
bump ImageBindingCount 36->41, and split Describe()'s atomic range
into its Uint and Float sub-ranges to match.

Also add an Atomic flag to ShaderImageShape (TargetRegisterWords.cs)
so downstream view-format resolution can distinguish atomic images
from ordinary sampled/storage ones.

* spirv: emit ImageAtomicFmin/Fmax and DsSwizzleB32, fill SOP1 ALU gaps

Vulkan has no native float image-atomic-min/max instruction, so emit
them the way KytyPS5 does: OpImageTexelPointer (typed uint) ->
OpAtomicLoad -> compare-and-swap loop (OpPhi/OpFOrdLessThan/OpSelect
on the bitcast float value) -> OpAtomicCompareExchange, returning the
pre-exchange value from EmitAtomicWordUpdate (now non-void, valid SSA
at the loop's merge block).

Add DsSwizzleB32 (bit-mode/quad-mode lane shuffle via
GroupNonUniformShuffle) to the LDS-opcode switch and to
UsesSubgroupShuffle(), and add the SFlbitI32B32/SAbsI32/SBitset0B32/
SBitset1B32 SOP1 cases so the newly-decoded opcodes from
Gen5ShaderTranslator actually translate to SPIR-V instead of hitting
the generic "unsupported instruction" fallback.

* spirv: declare atomic image OpTypeImage with a Uint sampled type

DeclareImageClass previously derived the SPIR-V OpTypeImage's sampled
type purely from the binding's logical NumericClass, so an
atomic-float image (logical class Float, backed by our new
compare-and-swap emission on a uint texel pointer) still declared its
OpTypeImage as float-sampled. That violates
VUID-StandaloneSpirv-Image-04965: the R32ui image format and a float
sampled type disagree on numeric class. Force the sampled type (and
format, already correct as R32ui) to Uint whenever the image is
atomic, regardless of its logical numeric class.

* gpu: back atomic-float images with a real Uint VkImageView

The SPIR-V side now declares atomic images as Uint (OpTypeImage), but
the actual VkImageView bound at dispatch time was still created with
the descriptor's own float-numeric-class format, tripping
VUID-vkCmdDispatch-format-07753 (the shader's declared sampled type
must match the bound view's format numeric class).

Fix both resolution paths:
- VulkanVideoPresenter.Descriptors.cs: NumericClassOf() now forces
  Uint for any atomic image (this covers the null-descriptor
  fallback path), and ShapeOf() forwards the Atomic flag.
- ImageRequestBuilders.Texture.cs: the real, non-null descriptor path
  resolves its view format independently from the descriptor's raw
  GuestPixelFormat and was untouched by the above. Extend the
  existing Sint-storage override (which already forces such images to
  Format.R32Uint) to also cover shape.Atomic.

Confirmed via a fresh Vulkan-validation-layer run against Ghost of
Yotei that VUID-vkCmdDispatch-format-07753 no longer appears.

* fix(shader): reconcile atomic SPIR-V emitters with upstream

* spirv: fix ImageGetResinfo querying a non-existent sampled-image combo

ImageGetResinfo never goes through UsesSampler() (it only matches
ImageSample*/ImageGather* opcodes), so TryResolveLayoutImage always
resolves imageObject as the raw OpTypeImage value for this
instruction, never a sampled-image combo. The size-query code still
called OpImage on it whenever resource.IsStorage was false, expecting
to extract a combo that was never built, which spirv-val rejects:
"Expected Sample Image to be of type OpTypeSampleImage". Just use
imageObject directly in both branches.

This surfaced only after the earlier atomic-image fixes let Ghost of
Yotei's shader compilation get this far; it is an unrelated,
pre-existing bug in the base PR.

* gpu: remap compute workgroup axes past the Z-dimension device limit

Ghost of Yotei dispatches a compute shader with NUM_THREAD (1,1,256).
Vulkan only guarantees maxComputeWorkGroupSize >= (1024, 1024, 64):
the Z axis has a much tighter guaranteed limit than X/Y, and this ran
into it for real: "LocalSize Z (256) exceeds device limit
maxComputeWorkGroupSize[2] (64)" at vkCreateComputePipelines, even
though the NVIDIA RTX 5070 here allows up to 1024 total invocations.

Fix by remapping logical GNM axes onto physical SPIR-V/dispatch axes
at shader-compile time: the largest NUM_THREAD axis always lands on
physical X (most headroom), the smallest on physical Z (tightest
limit). Gen5SpirvTranslator.ComputeWorkgroupAxisOrder computes this
permutation purely from the three thread-group sizes, so:

- The translator uses it to declare the physical LocalSize execution
  mode and to read gl_LocalInvocationId/gl_WorkGroupId's components
  back into the correct logical VGPR/system-register slots, so guest
  shader code (which only ever sees logical TID/WorkGroupId values)
  is unaffected by the remap.
- RenderExecutor.Compute.cs recomputes the identical permutation from
  the same thread-group sizes (input.ThreadsX/Y/Z) to reorder
  vkCmdDispatch's group counts the same way, with no new metadata
  plumbing between the shader compiler and the dispatch path.

* shader(resources): drop redundant ImageHandle ScalarBufferWord guard; add opt-in resource-tracker diagnostics

GetHandleSource had a blanket rejection of any ImageHandle whose dword
was a ScalarBufferWord, predating ValidateSource/RuntimeValueValidator
being able to generically resolve such dwords at bind time (the same
mechanism MaterializationSources already uses for buffer descriptors).
Drop the redundant, narrower guard and let ValidateSource be the
single source of truth, so legitimately resolvable image descriptors
loaded from a scalar buffer (not just the two hardcoded heap/direct
shapes TryMakeIndirectImage/TryMakeDirectImage recognize) aren't
rejected outright.

This did not resolve Ghost of Yotei's current blocker: its compute
shader at hash=0xCECFBCC210CB2372 pc=0x59C resolves an image
descriptor's byte offset through a loop-carried Phi (a hash-table/
linear-probe style bindless material lookup) that is not loop-
invariant, so it genuinely cannot be pre-evaluated as a single
per-dispatch runtime value the way MakeRuntimeBufferSource/
RuntimeValueEvaluator expect. That needs real GPU-side dynamic
descriptor indexing support, which doesn't exist yet in this
resource-tracking pass.

Also add SHARPEMU_RESOURCE_TRACKER_DIAG=1, gating verbose stderr
tracing in TryMakeIndirectImage's rejection paths and
RuntimeValueValidator's node-validation failures. This is what
localized the loop-carried Phi as the actual root cause; keeping it
opt-in and off by default costs nothing at runtime.

* shader(resources): degrade control-dependent bindless image/sampler descriptors to null instead of failing compilation

Ghost of Yotei's material lookup resolves an image descriptor's
offset through a loop-carried phi (a hash-table/linear-probe bindless
scheme) that RuntimeValueValidator correctly can't treat as a single
compile-time source - this resource tracker has no GPU-side dynamic
descriptor indexing to fall back on. Track *why* validation failed
(RuntimeValueValidator.ControlDependent, set when the failure traces
to a non-invariant phi) and, for image/sampler handles specifically,
degrade to a null descriptor instead of hard-failing shader
recompilation. Buffer handles and any other failure reason still fail
as before.

This ports KytyPS5's fix for the identical case
(feat/shader-control-dependent-descriptor, "shader: fall back to a
null image when its descriptor is control-dependent") rather than
attempting full dynamic descriptor indexing support, which doesn't
exist in either codebase.

Also add SHARPEMU_RESOURCE_TRACKER_DIAG tracing in
RuntimeValueEvaluator's evaluation failure paths (per-source-dword,
flattened table reads, and raw memory reads), used to localize the
next blocker down this same path.

* gpu: keep image shader types compatible with Vulkan views

* shader: decode and emit VOP1 0x39 (VFfbhU32, find-first-bit-high)

Another decode gap in the same family as the earlier MIMG/SOP1 fixes:
VOP1 0x39 was missing from the opcode name table, aborting
translation of Ghost of Yotei's next compute shader before it reached
GPU submission. Ported the fix straight from our own tree's
Gen5ShaderTranslator.cs/Gen5SpirvTranslator.Alu.cs, which already
decodes and emits it: reverse the bits then use the GLSL FindILsb
extended instruction, since RDNA reports the leading-zero count with
the architecturally required 0xFFFFFFFF sentinel for a zero source.

* shader(resources): treat a null-based SRT raw read as zero instead of faulting

A bindless pointer the guest leaves unbound for a dispatch resolves
its address-handle dwords to zero; EvaluateRawRead's raw-address
branch (as opposed to the buffer-descriptor branch) then dereferenced
that near-null address directly, and the guest memory reader
correctly refused it as unmapped, failing the whole materialization
with a generic "reason=Other". Ghost of Yotei's next compute shader
hit exactly this: a flattened SRT table read through a null-resolved
48-bit pointer plus a small immediate offset landed at address 0x14.

Treat any raw-address read below a small guest null-page threshold
(0x10000) as reading zero, so the value (and whatever descriptor it
feeds) collapses to null instead of faulting materialization. Ported
directly from KytyPS5's identical fix for the identical symptom
("shader: treat a null-based SRT constant read as zero").

* shader: decode/emit VOPC 64-bit compares and the 64-bit LDS atomics

Ghost of Yotei's cs 0x80003AF600 emits V_CMPX_NE_U64 (VOPC 0xF5) and DS_ADD_U64/DS_OR_B64 (0x40/0x4A), which the decoder refused. The VOPC 0xE0-0xE7 / 0xF0-0xF7 forms are the same V_CMP/V_CMPX U64 operations the VOP3 table already names and the emitter already lowers, so mirror them into the two-operand VOPC table.

The 64-bit LDS atomics sit just ahead of DS_WRITE_B64. LDS is modelled as an array of dwords, so each is lowered to the two 32-bit atomics that make it up. Neither opcode returns the old value, which makes the split exact: only the final memory state is observable. OR is bitwise-independent, so two 32-bit ORs are the 64-bit OR; ADD folds exactly one carry per low-dword wrap into the high dword, so the high word converges on the true wrap count regardless of lane interleaving. The RTN variants cannot be split without losing the returned pair's atomicity, so they stay undecoded and keep failing loudly.

* shader: decode and emit VOP3-encoded 64-bit VOPC compares (0xE0-E7, 0xF0-F7)

Ghost of Yotei's material-lookup shader issues V_CMP_*_U64/V_CMPX_*_U64
through the VOP3 encoding (opcode < 0x100, the same numbering VOPC
uses), which DecodeVop3's own opcode table never listed - it only
handled VOP3-native opcodes >= 0x101. Ported the fix straight from our
own tree, which already special-cases this exact sub-range (see the
"VOP3-encoded 64-bit VOPC" comment there): decode 0xE0-E7/0xF0-F7 to
the VCmp*U64/VCmpx*U64 names, and emit them as a 64-bit integer
compare (or a constant true/false for the F/T pseudo-compares),
reusing the existing generic VCmpx exec-mask handling.

* shader: implement GFX10 double-precision conversions and arithmetic

Decode VOP1 0x04/0x0F/0x16/0x2F/0x30/0x34 and their VOP3-encoded forms (V_CVT_F64_I32, V_CVT_F64_U32, V_CVT_F32_F64, V_RCP_F64, V_RSQ_F64, V_SQRT_F64) and emit them as real SPIR-V doubles. A 64-bit result is split across the destination register pair: the low word goes through the common destination store and the high word is stored alongside it, both under the EXEC guard. V_MUL_F64 (VOP3 0x165) and V_FMA_F64 (VOP3 0x14C) are added as well.

Source absolute/negate modifiers and the VOP3 output modifier and clamp are honoured. FP64 inline constants and literals are rejected explicitly rather than read as the wrong bits, following the gfx10 expansion rules. The shaderFloat64 device feature is enabled only when the physical device offers it, and the Float64 capability is declared only for modules that use a double.

Ghost of Yotei compute shader 0x8000393600 (hash 0xB90E2024732C6111) now translates and compiles instead of stopping at decode.

* shader: decode and emit VOP2 V_PK_FMAC_F16

V_PK_FMAC_F16 (VOP2 0x3C) reads the destination register as the addend for both packed f16 lanes and writes D = S0 * S1 + D back as a packed pair. Reuse the existing single-rounding fused-f16 path per lane and merge the two halves, so the result matches the hardware's f16 fused behaviour.

Ghost of Yotei compute shader 0x8000393600 reaches this opcode after the decoder is past the f64 gap.

* shader: decode and emit VOP3 V_MAD_I32_I24

V_MAD_I32_I24 (VOP3 0x142) differs from the already-handled V_MAD_U32_U24 only in that both multiplicands are signed 24-bit values. Sign-extend each operand from bit 23, multiply with 32-bit wrap and add the third source.

Ghost of Yotei compute shader 0x8000393600 reaches this opcode after the decoder is past the f64 gap.

* shader(resources): address runtime-fetched buffer descriptors via the device-address page table

A V# whose four dwords are all read from a runtime scalar-memory address (a descriptor-array entry indexed by a loop counter, as in Ghost of Yotei's cs 0x80003F2A00 at pc=0x1B4) cannot be bound ahead of time, so resource tracking used to fail the whole shader. Its dword reads are not control-dependent in any other way: they are real guest memory loads emitted at runtime through the device-address page table.

Detect that case in ResourceTracker and mark the access RuntimeBufferDescriptor instead of building a dense buffer resource. The SPIR-V backend then builds the guest address from the descriptor's runtime base (SGPR pair), the MUBUF offset/soffset/vector offset and, when indexed, the descriptor's stride, and stores/loads through the existing physical-storage-buffer path. num_records/stride bound the access so a null or empty descriptor binds nothing, matching the static path's range check. Formatted, typed and atomic accesses keep failing loudly because their descriptor format cannot be reconstructed from the raw words.

This completes the former d8959888aafd2552 dispatch.

* shader(resources): recognize runtime V# descriptors built from SRT reads and loop phis

The first runtime-descriptor detection only accepted four direct raw ScalarAddressWord dwords. Ghost of Yotei's cs 0x8000289300 at pc=0x78C builds its V# from a loop-carried phi that merges two SBufferLoadDwordx4 reads of the SRT buffer at a dynamic offset, so the access was still rejected even though the descriptor is a real runtime value.

Make the detection generic: a BufferHandle is a runtime descriptor when each dword is a plan-time-known value (constant, user data, shader base, flattened table word) or a guest-memory read, recursing through phis, selects and operations, and at least one leaf is a raw read. A cycle through a phi is accepted the same way the read planner treats cyclic phis. Anything else keeps hard-failing.

The raw BufferLoadUshort at pc=0x78C now lowers through the device-address page table. The formatted BufferLoadFormatX at pc=0x18F4 correctly escalates to BoundedCandidateTable, which is still unimplemented and is now the next required feature.

* shader(resources): plan bounded runtime V# candidate tables

* shader(resources): materialize bounded runtime V# candidate tables

* shader(resources): address a runtime scalar buffer V# through the device page table

* shader(resources): model the guest V# separately from its Vulkan lowering; real 64-bit LDS atomics

Replace MemoryAccessInfo.RuntimeBufferDescriptor with a GuestBufferDescriptor that carries the V#'s provenance (Static or Runtime) and, for static descriptors, the dense-buffer index. The backend now chooses an explicit BufferLoweringStrategy from the descriptor and the access shape via GuestBufferDescriptor.ChooseStrategy, instead of hard-coding a single path: NativeBinding for compile-time descriptors, PhysicalStorageBuffer for a runtime descriptor used by a raw dword/subword access, and BoundedCandidateTable for a runtime descriptor whose format/type cannot be reconstructed from the raw words. The last one is not implemented and fails loudly with a precise reason, which is now a single, discoverable place rather than a boolean scattered through the translator.

DS_ADD_U64/DS_OR_B64 are now emitted as true 64-bit workgroup atomics when the device exposes shaderSharedInt64Atomics (queried through VkPhysicalDeviceShaderAtomicInt64Features, declared with the Int64Atomics capability, addressed through a 64-bit LDS view). The previous two-32-bit split is kept only as a documented fallback for devices without the feature; it is not atomic as a pair. Device setup reports the capability once.

* shader/image: accept 11_11_10 UNORM sampling and 8-bit sRGB render targets

The unified image format 30 is 11_11_10_UNORM, a packed 11/11/10 normalized float format the rendering side already knows as GuestPixelFormat.Bits11_11_10UNorm (mapped to VK_FORMAT_B10G11R11_UFLOAT_PACK32). It was missing from the shader-side sampled-format table, so cs 0x8000265D00 failed specialization with 'sampled image descriptor 7 uses unsupported format 30'.

Bits8Srgb and Bits8_8Srgb also had RenderTargetBytesPerElement = 0 while their element sizes are 1 and 2 bytes, which made TryGetTextureSize reject a RenderTarget-tiled 8-bit sRGB surface (tile mode 27) that the guest creates. Their render-target footprint is the same as the matching UNORM formats, and the host view already substitutes R8/R8G8 UNORM, so give them the real byte counts.

* shader: allow inline constants and literals as VOP3P packed-f16 operands

The VOP3P packed-f16 path only accepted register operands and refused constants, so cs 0x80003AF600 stopped at V_PK_FMA_F16 with src[128]. An inline constant or literal is a single f32 value rather than a packed register pair, so there is no half to select: both lanes take the exactly-converted f16 of that constant, with the lane's negate modifier still applied. Keeps the register path unchanged.

* shader: support Yotei f16 shifts bitsets and safe LDS atomics

* shader(vulkan): dispatch bounded runtime V# candidate tables

* shader: support additional LDS operations

* shader: support RDNA LDS store encoding

* shader: handle LDS operands and RDNA buffer cache op

* shader: extend strict GFX9 buffer and ALU decoding

* [VideoOut] added output mode events and profiling fixes

* [Gpu] fixed image, render, and dispatch handling

* shader: track SDWA and runtime buffer values

* [Shader] expanded Gen5 resource and interpolation support

* gpu: reconcile PR893 metadata and indirect dispatch

* gpu(commands): consume unknown NOP marker ids instead of failing the submission

Demon's Souls emits markers 0x6EC and 0x80C, which the interpreter treated as
fatal. The packets carry no work for the ids without a known side effect
(flips, user-data marker context), so they are consumed and each id is
reported once (command_stream.marker_ignored). voidborn, which gets much
further on this title, likewise ignores NOP marker payloads it has no
handler for.

* kernel: implement sceKernelGetOperationMode (PS5 base console, no submode)

The import was unresolved, so callers polling it (Demon's Souls does it at very
high frequency) got an unresolved-import result instead of the mode. Behaviour
matches KytyPS5 KernelGetOperationMode: *mode = 2, *submode = 0.

* loader: protect red-zone functions with enclosing patch spans

The red-zone patcher only built a trampoline span forward from the faulting
instruction, so it gave up whenever the next instruction was a branch target,
touched RSP, or changed control flow. Those refusals never reached FailedSites,
and the summary line reported sites=11472/11472 failed=0 while 777 faultable
instructions inside red-zone functions stayed unprotected.

Demon's Souls died on one of them. A GPU write-tracking fault at 0x800AD91F8
dispatched on the guest stack with RSP unshifted, which let Windows overwrite
the 128 bytes below RSP; the function reloaded its pointer from [rsp-0x10],
got 0, and faulted writing through it. The fault census confirms the same RSP
in the faulting frame as in the crash report.

Add TryBuildEnclosingPatchSpan: when the forward search refuses, take the
span's bytes from before the faulting instruction so the span ends at that
instruction and the one after it is never rewritten, which keeps every existing
branch into it valid. Two invariants make this safe and both are exercised by
the new tests:

  - the RSP shift brackets only the faultable core, so a relocated
    RSP-relative access still reads the slot it meant to;
  - a span may not start before the end of the previous site, because a
    backward span can otherwise reach instructions an earlier site already
    replaced with its own jump.

Patch() also refuses overlapping sites outright rather than corrupting guest
code, and the scan now reports unrelocatable sites by category.

Unprotected sites drop from 777 to 270. The title runs past the main menu into
its opening cinematic and stops on an unrelated shader resource error.

The fiber tests move into a shared collection: they mutate the same static
FiberExports state as FiberExportsTests, and xUnit runs separate classes in
parallel, which made them fail intermittently.

* cpu: count guest faults and make access-violation reports readable

Guest code is commonly mapped PAGE_EXECUTE, which no host read can satisfy, so
IsReadableHostRange refused it and the whole call-site section of the access
violation report was skipped: every crash in such a title printed "Could not
read code at RIP" and nothing else. Read those windows through the guest memory
view instead, and print the faulting region so a future refusal explains
itself.

Add a census of the recoverable faults taken on guest threads, by guest RIP and
by the handler that resolved them. Windows has no alternate exception-dispatch
stack, so each of those faults overwrites the 128 bytes below RSP; knowing which
guest instructions fault is what turns a red-zone corruption from a hypothesis
into a measurement. Counters are allocated once per RIP and are inert unless
SHARPEMU_FAULT_CENSUS is set, since the handler is hot.

Also fix the guest context transfer frame: it published the stack bounds in the
wrong order, storing the low address in NT_TIB.StackBase and the high one in
StackLimit on every fiber switch.

Note: these two files also carried uncommitted work from earlier in this
worktree (guest stack sizing, the 22-qword transfer frame, import and exception
paths); that work is included here rather than separated, because the hunks
interleave.

* gpu: stop handing image ownership to a buffer that was only bound

Binding a formatted writable buffer over an image's memory invalidated every
image covering that range. The shader only declares that it may write there, so
this fired on every bind of every draw, and when the range belonged to a render
target the image was refreshed from guest memory the GPU never wrote - its
tiled backing read as linear, over the frame that had just been rendered.

A Demon's Souls capture showed 412 buffer-dirty refreshes and 207 GB of image
traffic. Suppressing the two bind-time call sites removed the banding entirely
and took the frame time from 7471 ms to 4272 ms, with GPU occupancy going from
29% to 45%; buffer-dirty refreshes dropped to zero.

Writes that actually happened still invalidate, from GuestBufferCache's fill and
DMA-copy paths, which is what the image ownership tests cover. The declared-write
case needs a reconciliation after the submission, once the GPU-modified ranges
are known; that does not exist yet, so
SHARPEMU_SPECULATIVE_IMAGE_INVALIDATION=1 restores the old behaviour meanwhile.

Note: these two files also carried uncommitted work from earlier in this
worktree.

* shader: name the instruction behind an undefined descriptor dword

A descriptor dword that resolves to an undefined value fails the resource plan
with nothing to act on. Undefined nodes are interned per type, so one shared
node stands for every instruction the builder could not model, and the builder
has about twenty places that produce one: the message names a category where it
should name an opcode.

Record the instruction the builder is translating, and under
SHARPEMU_RESOURCE_TRACKER_DIAG intern undefined nodes per instruction as well.
Revisiting a block reaches the same instruction, so values stay as stable across
visits as before, and the rejection can walk the failing dword back to the
undefined leaves it rests on and name them. Without the diagnostic the interning
and the message are unchanged.

A first attempt recorded every undefined created while an instruction was in
flight, which listed 1410 instructions for one shader because many are created
legitimately, for SCC among others. Only the leaves the failing value actually
depends on are reported.

Test counts are unchanged at 614 passed and 95 pre-existing failures.

* shader: carry over the Gen5 translation work from the port

Pre-existing work in this worktree, recorded here rather than authored in this
session: VOP3B carry/borrow paths for VSubCoCiU32 and VSubrevCoCiU32, the high
opcode bit for MIMG and the ray-tracing names it selects, DS/GDS flags, the
wave32 EXEC mask, and the scalar value graph and runtime evaluator changes that
go with them, across the Vulkan and Metal back ends.

* gpu: carry over the AGC and VideoOut work from the port

Pre-existing work in this worktree, recorded here rather than authored in this
session: tracked image caching in place of unused snapshots, depth/stencil
handling, snapshot reuse for feedback loops, read-only depth, the Vulkan
feedback target resolver, the GuestDepth and draw recording paths, GPU barriers
and image cache tracking, the storage resolver, BDA and GPU memory tracking in
VideoOut, and the submission and indirect dispatch corrections.

* hle: carry over the library export work from the port

Pre-existing work in this worktree, recorded here rather than authored in this
session: an HTTP connect timeout, an offline MAC, NP signalling context, the
asynchronous NP stubs and entitlement key, NP WebAPI context deletion, a CFW
stub, and the added aliases for sceImeKeyboardGetInfo, unlink, sched_yield,
scePadResetOrientation, scePadIsRemoteController and the solo scheduling
attributes, along with the Ampr file registry and kernel compatibility exports.

* cpu: carry over the guest execution and memory work from the port

Pre-existing work in this worktree, recorded here rather than authored in this
session: the guest stack size the pthread attribute asks for and its cap, the
PthreadYield fallback, the multi-thread stall watchdog, import and handoff
instrumentation, the host view and guest space owner changes, and the physical
virtual memory and runtime adjustments that go with them.

* base: verified pre-rebase gameplay baseline

* fix: materialize indirect shader image resources

* gpu,shader: Yotei bring-up changes (GPT session, unreviewed)

Work from a separate GPT session aimed at Ghost of Yotei, committed as-is so
it can be reviewed block by block. It clearly improves Demon's Souls rendering
(the scene becomes visible), but review found problems to fix before a PR:

- S_BITREPLICATE_B64_B32 copies the dword into both halves; the ISA doubles
  each bit (bit i -> bits 2i, 2i+1). Wrong in the translator and the graph.
- FindMatchingMipLevel no longer matches 2D array views of 3D volumes; the
  test expectation was flipped from 0 to -1 instead of keeping the case.
- VectorCompareResourceTests lost five passing tests, replaced by
  compile-only smoke tests.
- 11_11_10 UNorm is mapped to a packed-float Vulkan format.
- Unknown BufferLoadFormat descriptors now read zeros / null images silently.
- Undefined scalar values are interned per instruction for every title.

Also adds 16-bit integer VOPC compares, V_RSQ_F16, IMAGE_GATHER4_C_LZ,
IMAGE_SAMPLE_A decode, Yotei base entitlements, richer image-creation
failure messages and flip-source diagnostics.

* diagnostics(renderdoc): capture automatically after N guest flips

SHARPEMU_RENDERDOC_CAPTURE_AFTER_FLIPS=N requests one capture once N guest
flip boundaries have been seen, so an unattended run can capture without the
F12 key. The count is kept here because the flip image version is per display
buffer, not a global frame index.

* fix(import-guard): recognize polling progress boundaries (#895)

* port shader GDS byte reads and float atomics

* port kernel debug exception exports

* Revert "port kernel debug exception exports"

This reverts commit 71d605e82c82a4a921862bfc865a104305cf746d.

* Revert "port shader GDS byte reads and float atomics"

This reverts commit 8f4e7664d7fc81a28ba5c47c86cbc03dba61d47b.

* [usbd] implement event timeout

* [usbd] add initialization stubs

* [share] add feature and overlay exports

* [kernel] implement pthread cond signalto

* [font] add renderer and kerning exports

* [agc] implement ACB rewind exports

* [share] add running status export

* [stubs] add game service exports

* [kernel] implement high resolution timer events

* [gpu] support polygon primitive rendering

* chore: bump version to 0.0.4

* [kernel] implement debug exception exports

* [vulkan] adopt synchronization2 submission and barriers

* docs: update upstream README

* Revert "[vulkan] adopt synchronization2 submission and barriers"

This reverts commit 7e44f90d624c05ccac3ba8d14f94bfa63c225b1d.

* Reapply "[vulkan] adopt synchronization2 submission and barriers"

This reverts commit 66b456dd2a4562b081970404034ab3cc3135f66a.

* Revert "[kernel] implement high resolution timer events"

This reverts commit faf105d9a0f93120f9954ff30890701683d79a67.

* Reapply "[kernel] implement high resolution timer events"

This reverts commit 9858506307cfdc43e875180f21727bea936ac888.

* Revert "[kernel] implement pthread cond signalto"

This reverts commit 83ad3b0ac313b01c7d1493e685423a5a419b8766.

* Reapply "[kernel] implement pthread cond signalto"

This reverts commit 4bc31dabf7885687dc3777aefede6b4122f936a4.

* [ampr] fall back to executable-local index cache

* [ampr] probe cache directory writability

* [media] keep host movies progressing when Vulkan bindings lag

* [media] release stalled host movies independently of rendering

* [vulkan] reuse pipeline caches across emulator builds

* [media] propagate stalled movie EOF to the guest

* [ampr] return guest path ids from resolve exports

* Revert "[vulkan] reuse pipeline caches across emulator builds"

This reverts commit f0f8e21382b402b510645acb66cf122ad765cd4a.

* [vulkan] reuse pipeline caches across emulator builds

* Revert "[vulkan] reuse pipeline caches across emulator builds"

This reverts commit fd667b6320b74be1e5d71b37a3c29fc11af713bb.

* fix(media): honor full Bink movie duration

* fix(vulkan): schedule wave64 barriers uniformly

* fix(vulkan): emit wave64 loop merge before branch

* fix(vulkan): predicate wave64 image stores

* Revert "fix(vulkan): predicate wave64 image stores"

This reverts commit 5842298743822d1b128e8b4dcb3f9d0a8fd03c35.

* Revert "fix(vulkan): emit wave64 loop merge before branch"

This reverts commit d2b98e0a8439e41c8faf04335320f202740fe7f9.

* Revert "fix(vulkan): schedule wave64 barriers uniformly"

This reverts commit 976d5a5957080de6568417c9d12235570b1503ef.

* baseline: preserve good Demon's Souls rendering

* Revert "[vulkan] adopt synchronization2 submission and barriers"

This reverts commit 7e44f90d624c05ccac3ba8d14f94bfa63c225b1d.

* fix(ampr): keep APR resolve ids unique on path-hash collisions

The APR resolve exports now return the 31-bit FNV hash of the guest path
(upstream 5c347d2c). Demon's Souls ships 13 same-hash file pairs under
/app0/ alone, and it resolves 32 colliding paths before the main menu.
Colliding files shared one id and read each other's bytes, and the
first level load after the menu never finished.

Hand out the hash only while it names one host file, keep it in an
authoritative table that later alias publishes cannot poison, and fall
back to a collision-safe Register() handle otherwise. Log both the
fallback and reads of unknown ids, which previously failed silently.

* fix(media): size the Bink watchdog to the movie, restore Gen4 export

c77746bd reverted the duration-based Bink watchdog to a fixed 90 s, and
MediaFramePlayback also capped every movie (AvPlayer too) at 120 s.
Demon's Souls ships a 171 s intro, 117-138 s attract movies and 756 s
credits, which both bounds cut short. Arm the watchdog for the movie
length plus 60 s (minimum 90 s, no upper bound) and drop the fixed
playback cap, as in the pre-rebase baseline.

Also keep cfwBSQyr5Ys exported for Gen4 as the baseline did.

* chore: trim added comments, keep upstream's cfwBSQyr5Ys target

Restore upstream's APR ABI comment verbatim, keep each added comment to
one line, and leave sceKernelDebugWriteCppExceptionInfo Gen5-only as
upstream declares it.

* fix(vulkan): query image levels only when the result is read

ImageGetResinfo emitted OpImageQueryLevels for every sampled image even
when the dmask never reads component 3. On a 3D image, the unused query
crashes NVIDIA's pipeline compiler inside vkCreateComputePipelines:
Demon's Souls compute shader 0xC553F37E46481A6B (dmask 3) takes the
process down right after the first frame.

* fix(shader): reconcile branch resource planning with upstream contracts

- Keep upstream's 42-binding image ABI. A float min/max image atomic
  binds as the uint atomic image its compare-exchange loop already uses.
- A null-page read turns into zero only after a null base pointer fails
  to read, not for every address below 0x10000.
- An empty V# reads as zero; overrunning a bound buffer stays a failure.
- A wave-indexed image table reads its mask through the clean reader,
  like every other slot its descriptor depends on.
- A buffer load through a lane-varying address stays undefined, and a
  constant exec_lo write keeps s_cbranch_execz decidable.
- An image descriptor from a scalar buffer must be one contiguous
  record, and a vector access through a draw-evaluable V# keeps its
  explicit binding instead of the runtime-descriptor path.

* fix(shader): drop the null-page read rule, encode GDS at bit 17 in tests

KytyPS5 does not turn a null SRT pointer read into zero: an unmapped
read fails materialization, which FailedResourceReadsRemainFatal pins.
Remove the rule.

GFX10 DS encodes GDS at bit 17 with OP at 25:18; bit 16 is GFX9's
position (LLVM DS_Real_gfx10). The decoder already reads bit 17, so the
DataShare tests set GDS there, and the LDS wave-counter tests clear it.

* fix(vulkan): keep bind-time image invalidation coherent with GPU contents

Binding a formatted writable buffer over an image marks the image
buffer-modified, so its next use refreshes it from the buffer. The
buffer was only synchronized from the image for read-only views, so
over a render target a writable view refreshed the image from stale
guest bytes. That is the Demon's Souls banding 48cd723a worked around
by skipping the invalidation altogether.

Synchronize writable formatted views from the image as well, and at bind
time invalidate every covered image except one whose only current copy
is still on the GPU and could not be copied into the buffer. Drop the
SHARPEMU_SPECULATIVE_IMAGE_INVALIDATION switch.

* test(shader): expect the header immediate in the indirect selector offset

The materializer reads the selector word at selector * stride +
SelectorOffset, so the offset must include the material read's
immediate. With the header immediate 4 and the +4 in the address
arithmetic it is 8; the test expected 4 since it was written.

* chore: remove session diagnostics

Drop the ad-hoc instrumentation added while bringing Demon's Souls and
Ghost of Yotei up:
- SHARPEMU_RESOURCE_TRACKER_DIAG traces in the resource tracker, dense
  image tables, runtime evaluator and validator
- unconditional materialize_snapshot_probe warnings and the
  SHARPEMU_TRACE_INDIRECT_IMAGES wave-table trace
- the guest fault census (SHARPEMU_FAULT_CENSUS / SHARPEMU_FAULT_WATCH)
- red-zone patcher address trace, unprotected-site file and short-jump
  estimate (SHARPEMU_REDZONE_TRACE / SHARPEMU_REDZONE_SITES_FILE)
- RenderDoc capture after N flips and the flip-source refresh traces

The red-zone coverage counters and the readable access-violation report
stay.

* fix(kernel): sparse virtual reservations and HLE compatibility fixes

- sceKernelReserveVirtualRange: keep very large non-fixed reservations
  in the guest mapping table without one contiguous host placeholder.
- Event flags: read names and write handles through the kernel
  compatibility helpers so read-only image pages work.
- sceAjmInitialize: only validate the 32-bit reserved argument.
- Pthread: only free opaque objects the compatibility layer allocated.
- Route libc memset to the HLE implementation (int 45h path in LLE).

* fix(gpu): tiling, color targets, VideoOut pipelines and primitive stage masks

- Tile geometry and tiler shader fixes for detiling.
- Color target request fixes for 3D slice ranges.
- VideoOut descriptor, draw recording and pipeline fixes.
- Accept the compact primitive-shader stage masks (0x2000, 0x2030)
  and report the rejected state when draws are skipped.

* fix(shader): Little Nightmares III decoding and translation

- Decode S_SCRATCH_STORE_DWORDX2 (SMRD op 0x16) as a scalar scratch
  store instead of a load.
- Raise the decoder instruction limit to the maximum shader size.
- Stop linear decoding at S_SETPC_B64 and treat the tail as
  unreachable; find fused GS pairs across the full 1 MiB object and
  resolve continuation image descriptors from its entry SGPRs.
- Translate S_SETPC_B64 in linearised fused programs as a marker.
- Implement S_MUL_HI_I32 and V_DOT2C_F32_F16.

* fix(shader): Astro Bot debugger stubs and wave32 primitive pipelines

- S_CBRANCH_CDBG* only fire under an attached GPU debugger; do not let
  them extend the decoded program past S_ENDPGM into debugger stubs
  that load descriptors from devkit-only addresses.
- Stop decoding after an unconditional backward S_BRANCH with no
  pending forward target instead of reading the "sl00" metadata
  trailer as instructions.
- Ignore the GS_W32_EN/VS_W32_EN wave-size bits when matching
  primitive-shader stage masks; graphics stages always compile as
  wave32, so these draws were skipped for no reason.

* fix(kernel): keep sparse reservations out of the macOS graphics window

macOS reserves 0x10_0000_0000..0x70_0000_0000 for graphics memory, so a
sparse reservation starting at 0x40_0000_0000 could not be mapped there
(LargeVirtualReservationFallsBackToSparseGuestRange failed on osx-x64).
Start the sparse aperture at 0x70_0000_0000 on macOS.

* fix(shader): declare guest 1D textures as 1D images

Since #903 the image request builder binds guest 1D textures through
1D Vulkan views, but the SPIR-V translator still declared them as 2D.
Sampling a 1D view through a 2D image type is invalid, and on NVIDIA
Astro Bot's final composite pass wrote black to the flip buffer.
Declare Dim1D/Dim1DArray as SPIR-V 1D images with Sampled1D/Image1D,
matching upstream.

* revert(gpu): fail on written device-address ranges that cannot be planned

The Little Nightmares III work skipped unplannable written ranges on the
host and allowed those stores unconditionally in the shader. Stores whose
pages were not already in the BDA page table were then dropped silently.
Restore upstream's fatal and remove UnplannableWrittenMemoryIndices, so
each such shader is reported and fixed on its own.

* fix(shader): route FLAT accesses through the LDS and scratch apertures

GFX10 sends a FLAT access to LDS or scratch when bits 63:48 of its
address match the shared or private aperture (SH_MEM_BASES), and to
global memory otherwise. src_shared_base/src_private_base decoded to 0,
so an LDS pointer became a device address the host could not plan
(Little Nightmares traversal stack, shader 0x9669394374AAE5BC).

- Give the apertures distinct high dwords no guest address can reach,
  consistent for 32-bit (high dword) and 64-bit (address) reads.
- Keep aperture identity in the scalar value graph (MemoryAperture).
- The device-address planner classifies a FLAT access whose high dword
  derives from an aperture and gives it no device range; plain device
  pointers keep the range and the fatal.
- The SPIR-V backend routes each lane by bits 63:48 to LDS, scratch or
  the page-table global path.

* fix(shader): use the PS5 apertures and prove clamped FLAT addresses local

Little Nightmares builds its BVH traversal-stack addresses without
src_shared_base: v_med3_u32(0x80000000, 0x70000000, sp) clamps the high
dword, and the pop path reads the 0x7xxxxxxx entries with ds_read and
the 0x8xxxxxxx ones with scratch_load. These are the PS5 shared and
private apertures, so:

- SharedApertureHigh = 0x70000000, PrivateApertureHigh = 0x80000000;
  the backend routes a FLAT lane by the top nibble of its high dword.
- Model v_med3_u32/v_med3_i32 in the scalar value graph.
- The planner bounds the high dword (Phi/Select/UMin/UMax, cycles
  ignored since each yields one of its inputs) and classifies it local
  when the bounds stay inside the apertures.
- Record the EXEC mask of each global/flat access: a select on that
  same mask is a lane-masked register write whose inactive arm never
  reaches a lane that accesses memory.

Little Nightmares shaders 0x9669..., 0xE9F1... and 0x392B... now plan
without the removed unplanned-write skip.

* feat(shader): emulate the GFX10 BVH node test for image_bvh_intersect_ray

Follows AMD GPURT's RT IP 1.1 fallback: triangle nodes 0/1 with barycentric
swizzle, FP16/FP32 box slab test with grow, NaN rule and sorting network.
Nodes are read through the device-address page table.

* fix(gpu): move a depth surface sampled as color into a color image

A texture whose format reads neither the depth nor the stencil plane
reinterprets the memory; Little Nightmares samples a D32S8 surface as BGRA8.

* fix(shader): make s_barrier order buffer and image memory within the workgroup

Guest code publishes stores to its workgroup with s_waitcnt vmcnt(0) +
s_barrier; the host barrier only ordered shared memory.

* fix(gpu): match sampler border color and filtering to the sampled view

Border colors were always integer, which Vulkan leaves undefined on float
views (an opaque-white border can read back as black). The numeric class
now comes from the bound view formats; integer views also drop filtering.

* fix(shader): decode the VOP1 and VOP3 half-precision transcendentals

Silent Hill's pixel shaders use v_log_f16; add rcp, sqrt, log, exp,
floor, ceil, trunc, rndne, fract, sin and cos for f16 in both encodings.

* fix(shader): decode v_min3_f16, v_max3_f16 and v_med3_f16

Silent Hill's pixel shaders use v_min3_f16; the draw was skipped.

* fix(agc): give a fused shader the larger user SGPR count of its halves

sceAgcFuseShaderHalves kept the back half's SPI_SHADER_PGM_RSRC2_GS, so a
front half reading its vertex tables from user SGPRs 26-29 saw only 12
(Silent Hill: front 30, back 12) and the draw faulted on null tables.
A vertex program without input semantics no longer needs the tables, and
the null-table and PRT-mapping messages name the registers involved.

* fix(shader): read buffers past the binding budget through their device descriptors

A Silent Hill compute shader binds more than 32 distinct buffers. Past the
budget an untyped access reads its V# from its SGPRs at run time, as the
hardware does, instead of failing the whole shader.

* fix(gpu): treat a compute constant fill of DCC metadata as a fast clear

Unreal fast-clears DCC render targets with a compute shader that stores one
dword from a constant buffer into every metadata record (0x40404040 clears
to 0,0,0,1). The fill only reached plain memory, so Silent Hill's
translucency layer stayed 0 and its composite multiplied the scene by 0.
The exact instruction sequence is recognised and routed to the existing
DCC clear tracking.

* fix(gpu): invalidate every image under a bound writable formatted buffer

The bound-write path skipped images whose only current copy was on the GPU.
Such an image kept stale contents after the guest rewrote it through the
buffer: Dead Cells lost its background and Demon's Souls showed wrong
surfaces. Bisected to ed3de27b; upstream invalidates unconditionally, and
52fbe275 already synchronizes writable views from the image, which fixed the
banding the exception was meant to hide.

---------

Co-authored-by: ParantezTech <12572227+par274@users.noreply.github.com>
Co-authored-by: Mathias <06mathias6@gmail.com>
2026-09-27 20:43:07 +03:00
kuba ab0e77e5d6
[CPU] Emulate and rewrite SHA extension instructions on hosts without SHA (#909)
* [CPU] Emulate the SHA extensions when the host raises #UD

The PS5 CPU supports SHA1RNDS4/SHA1NEXTE/SHA1MSG1/SHA1MSG2 and
SHA256RNDS2/SHA256MSG1/SHA256MSG2, and titles use them without a CPUID
check. Rosetta 2 does not translate them, so Poppy Playtime (UE4) aborted
with SIGILL on sha1msg1 on Apple Silicon. Decode the faulting instruction
and compute it from the saved XMM registers, like the SSE4a fallback.

* [Loader] Rewrite SHA extension instructions at load time on hosts without SHA

A #UD trap per SHA instruction is far too slow for titles that hash large
files: Poppy Playtime took 300k traps and the stall watchdog stopped it.
The red-zone patcher now replaces each SHA instruction with general-register
code in its trampoline, keeping all other registers and the flags. The trap
handler stays as the fallback for sites that cannot be patched.
2026-09-26 14:20:08 +03:00
kuba 9073974ba8
[SpirvTranslator] Use a flat input for v_interp_mov P0 on MoltenVK (#910)
MoltenVK advertises VK_KHR_fragment_shader_barycentric, but SPIRV-Cross
cannot translate PerVertexKHR inputs to MSL, so every pixel shader with
v_interp_mov_f32 failed pipeline creation on macOS (Poppy Playtime).
Reading only P0 is the provoking vertex value, which a flat input gives.
Attributes that read P10 or P20 still use per-vertex inputs.
2026-09-26 14:19:57 +03:00
kuba 155add2c10
[CPU] Cache readable stack ranges per thread for import arguments on POSIX (#911)
Every import reads its stack arguments through a region query, which on
POSIX takes the global region lock and scans the view regions. With many
guest threads this was the most contended lock in the process (about 36k
contentions per second in Poppy Playtime) and HLE calls averaged ~59 us.
Keep the last readable range per thread for the import path and reuse it
until any POSIX mapping or protection changes. The signal handlers keep the
uncached query, since they must not touch thread-static storage.
2026-09-26 14:19:48 +03:00
kuba ac8ffc3f3a
[Pad] Reject pad handle 0 after the title opens a pad (#912)
Unreal Engine polls scePadReadState for all four user slots and passes
handle 0 for slots it never opened. Accepting handle 0 as the primary pad
reported three phantom controllers that repeated the primary pad's buttons,
and Poppy Playtime ignored all menu input. Handle 0 stays valid until the
title opens a pad, for Monster Truck Championship's early polling.
2026-09-26 14:19:39 +03:00
kuba 1759e18b77
[Memory] Pre-reserve the guest address space on POSIX hosts (#905)
* [Memory] Reserve the guest address space again after an image reset

SelfLoader clears virtual memory before loading the eboot, which released the
startup pre-reservation of 0x10_0000_0000..0xFC_0000_0000. Every later large
reservation then had to probe the host address space candidate by candidate.

* [Memory] Pre-reserve the guest address space on POSIX hosts

POSIX had no ReserveFreeAddressRanges and cannot query foreign host mappings,
so TryHoldRangeAtOrAbove stepped through the address space one alignment unit
at a time. On macOS a 512 GiB reservation (all UE titles) took ~196k failed
mmap/munmap pairs before reaching free space, which the stall watchdog reported
as a hang in sceKernelReserveVirtualRange.

Find the free ranges by bisection, and use the first pre-reserved candidate
once a lower candidate fails.
2026-09-26 03:06:25 +03:00
Berk f98ff990ec
chore: bump version to 0.0.4-release.2 (#908) 2026-09-26 01:14:40 +03:00
Mathias 8feea60d31
fix(gen5): handle runtime and shader buffer paths (#904)
* Implement AMPR wait-on-address commands

* Accept Gen5 AJM initialization options

* Preserve opaque context register table values

* Support shader-loaded buffer descriptors

* Support V_CVT_U16_F16 shader instruction
2026-09-26 00:37:09 +03:00
Berk 7dbdd8b2cc
[GUI/CLI] Add support for console settings and language selection (#907) 2026-09-26 00:17:25 +03:00
Astell d4ff32a1d2
Amd fix opcode nid filecache wave32 (#903)
* AMD RDNA2 FIX, WAVE32 FIX, GatherResourceFileInfo imporove with cache, sop 0x0F

* multiple vulkan fixes

* Astrobot video black square fix

* Fix tests: supported depth/stencil formats on AMD, non-null unmapped pointer, robust allocation check
2026-09-24 15:00:42 +03:00
Astell 22e73fbea1
[SpirvTranslator] Declare vgprPackedHalf only when a shader uses it (#901)
Every translated module declared the 4 KiB private vgprPackedHalf array,
even when no packed-half instruction reads it. AMD's Windows compiler
keeps the unused array as a named .bss global; linking two stages then
fails and the driver copies the NOBITS section as file data, reading
past the end of the ELF. The result is a 0xC0000005 access violation
inside amdvlk64.dll during vkCreateGraphicsPipelines /
vkCreateComputePipelines.

The array is now created on first use by PackedHalfRegisters().
2026-09-24 02:00:23 +03:00
Berk f70e333c08
[GUI/CLI] Add option to disable Vulkan implicit layers (#900) 2026-09-23 16:08:19 +03:00
ParantezTech d9433ff185 [Vk] Implement VK_EXT_shader_viewport_index_layer extension support 2026-09-23 14:47:08 +03:00
ParantezTech a036bc1a48 [CPU] Host RBX is restored from the saved host frame below 2026-09-23 14:46:17 +03:00
Berk 6d6a5a1a0d
[Spirvtranslator] Added many instruction set for SPIR-V translator (#894) 2026-09-23 14:05:19 +03:00
Berk 6fe8555f8f
Vk version update (#897)
* [Gpu] Vk version update

* [Gpu] Add Vk synchronization2 helper

* [Gpu] Migrate Vk queue submission to synchronization2

* [Gpu] Migrate Vk timestamps to synchronization2

* [Gpu] Migrate Vk buffer barriers to synchronization2

* [Gpu] Migrate Vk image barriers to synchronization2

* [Gpu] Migrate Vk GDS barrier to synchronization2

* [Gpu] Migrate Vk cached image barriers to synchronization2

* [Gpu] Migrate Vk command stream barriers to synchronization2

* [Gpu] Migrate Vk present barriers to synchronization2

* [Gpu] Migrate Vk rendering barriers to synchronization2

* [Gpu] Update Vk image tests for synchronization2

* [Gpu] Update Vk compute tests for synchronization2
2026-09-22 17:45:51 +03:00
Berk db382cf32f
chore: bump version to 0.0.4 (#896) 2026-09-22 04:33:19 +03:00
ParantezTech 0b2ee449a7 [readme] update 2026-09-22 04:29:46 +03:00
ParantezTech 54ab51e117 [SpirvTranslator] Fix SPIR-V viewport/layer capability handling 2026-09-22 04:20:21 +03:00
ParantezTech 5c347d2cb2 [Ampr] Separate APR guest file IDs from local handles 2026-09-22 04:17:44 +03:00
ParantezTech f1be35aa35 [GameService] more service stubs for the game service 2026-09-22 04:17:08 +03:00
ParantezTech 7d299c6b87 [Share] Added sceShareGetRunningStatus 2026-09-22 04:16:44 +03:00
ParantezTech a86f67393b [GPU] added polygon rendering support for VK 2026-09-22 04:15:50 +03:00
ParantezTech 082b5fa246 [AGC] sceAgcAcbRewind, sceAgcRewindPatchSetRewindState implements 2026-09-22 04:13:11 +03:00
ParantezTech b4584f8040 [Font] sceFontRebindRenderer, sceFontGetCharGlyphMetrics, sceFontGetKerning 2026-09-22 04:12:19 +03:00
ParantezTech 2a27cfe8be [Pthread] implement scePthreadCondSignalto 2026-09-22 03:07:29 +03:00
ParantezTech acbf28c784 [Share] added more share implementation 2026-09-22 03:06:54 +03:00
ParantezTech 0409f3aab1 [Usbd] more stubs 2026-09-22 03:06:22 +03:00
ParantezTech 3e760035b1 [Kernel] implement sceKernelAddHRTimerEvent & sceKernelDeleteHRTimerEvent for scheduler 2026-09-22 02:27:08 +03:00
ParantezTech acb0d4afb5 [usbd] implement sceUsbdHandleEventsTimeout, just return signal not connect and timeout 2026-09-22 02:26:15 +03:00
ParantezTech f4e6c7355c [Kernel] added sceKernelDebugWriteCppExceptionInfo implements for cpp exception info 2026-09-22 01:54:27 +03:00
Mathias ea943c1281
fix(import-guard): recognize polling progress boundaries (#895) 2026-09-21 19:56:05 +03:00
Berk cff4a167a6
Alpha rebuild part2 (#893)
* [Libc] routed strcasecmp through HLE

* [Ampr] implemented command buffer streaming contract

* [Agc] completed command buffer operations

* [Kernel] added wide string formatting coverage

* [VideoOut] added output mode events and profiling fixes

* [Shader] expanded Gen5 resource and interpolation support

* [Gpu] fixed image, render, and dispatch handling

* [Memory] added guest address space pre-reservation

* [AMPR] deferred read file execution to command submission

* [SpirvTranslator] Add DsMinF32 and DsMaxF32 instructions

* [spirvtranslator] Added V_CMPX_NE_U64 instruction

* [spirvtranslator] added DsReadI8 instruction

* [test] fixed
2026-09-21 00:29:16 +03:00
ParantezTech 9db40fcec8 [readme] new screenshots 2026-09-20 05:00:39 +03:00
voidborn_astra 37694dfbb7
Alpha Rebuild Progress (#887)
* refactor(videoout): isolate host buffer code

Move pooled host-visible Vulkan buffer management into a dedicated presenter partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(videoout): isolate draw state code

Move guest draw-state conversion into a dedicated presenter partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(videoout): isolate texture format support code

Move Vulkan image-format capability and render-target mapping into the texture-format presenter partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(videoout): isolate presentation image code

Move presentation-target selection and offscreen presentation-image creation into a dedicated presenter partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(videoout): isolate HDR presentation code

Move HDR presentation resources, pipelines, and command recording into a dedicated presenter partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(videoout): isolate draw execution code

Move offscreen draw, clear, guest-image write, and render-pass execution into a dedicated presenter partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(videoout): isolate guest submission code

Move guest GPU batching, submission, retirement, and visibility waits into a dedicated presenter partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(videoout): isolate render loop code

Move presenter work waiting, queue draining, frame acquisition, and work dispatch into a dedicated partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(videoout): isolate draw recording code

Move translated draw recording, image transitions, draw tracing, and resource retirement into a dedicated presenter partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(videoout): isolate guest work queue code

Move guest GPU work submission, scheduling, backpressure, and completion into a dedicated partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(videoout): place storage image contracts code with its owner

Move SPIR-V storage image contract parsing and validation into the compute storage-image partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(videoout): place ordered guest flips code with its owner

Move ordered guest flip capture and completion into the presentation-frame partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(videoout): place frame lifetime code with its owner

Move presentation frame waits and flip-version retirement into the presentation-frame partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(videoout): place texture cache resolution code with its owner

Move cached texture resolution and CPU refresh into the texture-cache partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(videoout): place barycentric pipeline code with its owner

Move the fullscreen barycentric shaders and pipeline creation into the pipeline partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(videoout): place swapchain draw resources code with its owner

Move presentation render-pass, image-view, and framebuffer setup into the swapchain partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(videoout): place feedback snapshot creation code with its owner

Move color feedback snapshot creation beside its pool lifecycle and telemetry.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(videoout): place texture format conversion code with its owner

Move real-format conversion and its transfer scratch images into the texture-format partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(videoout): place guest image diagnostics code with its owner

Move guest image tracing and readback diagnostics beside the guest image registry.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(videoout): place guest submission execution code with its owner

Move ordered actions, cache operations, pooled-data returns, and guest-work descriptions beside guest submission lifetime.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): isolate initialization exports code

Move AGC initialization and versioned register-default materialization into a focused partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): isolate driver resource exports code

Move AGC driver resource-registration exports and their private machinery into a focused partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): isolate event exports code

Move AGC event signaling exports and their private machinery into a focused partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): isolate shader state exports code

Move AGC shader-pipeline state exports and their private machinery into a focused partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): isolate packet patching exports code

Move AGC command-packet patching exports and their private machinery into a focused partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): isolate control flow exports code

Move AGC command-buffer control-flow exports into a focused partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): isolate register packet exports code

Move AGC register-programming packet exports and their private machinery into a focused partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): isolate draw packet exports code

Move AGC draw-command packet exports and their private machinery into a focused partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): isolate DMA packet exports code

Move AGC DMA packet exports into a focused partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): isolate compute fast paths code

Move CPU semantic replacements for recognized compute kernels into a focused partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): isolate compute dispatch code

Move translated compute dispatch handling into a focused partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): isolate draw translation code

Move translated guest draw construction into a focused partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): isolate draw state code

Move AGC render-state decoding into a focused partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): isolate submitted geometry code

Move submitted index and vertex retention into a focused partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): isolate texture transport code

Move guest texture transport into a focused partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): isolate PM4 decoding code

Move submitted PM4 stream decoding into a focused partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): isolate GPU wait lifecycle code

Move submitted GPU wait suspension and resumption into a focused partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): isolate orphan preamble recovery code

Move orphan-preamble recovery into a focused partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): isolate submission runtime code

Move submitted command execution into a focused partial.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): isolate draw diagnostics code

Move translated-draw diagnostics into a focused partial.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): isolate command packet code

Move general command-buffer packet builders into a focused partial.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): place multi-DCB export in submissions

Place the multi-DCB submission handler beside the queue parser and submission lifecycle.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): place driver configuration exports with its owner

Place tessellation driver configuration handlers beside the other driver resources.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): place software present path in events

Place software presentation beside the AGC flip and event handlers.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): place color-control decode in draw state

Place the CB color-mode and hardware-resolve helpers with the other draw-state code.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): place translated draw buffers in draw translation

Place the translated draw buffer builders with the draw translation pipeline.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): place wait packet helpers in command packets

Place WAIT_REG_MEM encoders and command-packet trace counters with their only consumers.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): place draw translation helpers with its owner

Place pixel-input translation helpers and their trace state with the draw translation pipeline.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): place frame packet trace setting in pm4

Place the frame-packet trace setting with its PM4 consumers.
The new location makes ownership clear.
Execution behavior does not change.

* refactor(agc): place guest memory transfer helpers in submissions

Place guest memory copy and fill helpers with their submitted-command consumers.
This change only improves code organization.
Keep runtime behavior unchanged.

* refactor(agc): place packet identity helper in packet patching

Place packet identity decoding with its packet-patching consumers.
This move changes code ownership only.
Runtime behavior stays the same.

* refactor(agc): place duplicate target trace state in draw state

Place the duplicate-target trace counter with its draw-state consumer.
The new location makes ownership clear.
Execution behavior does not change.

* style(renderer): remove extra spacing from split files

Remove excess blank lines left by the AGC and Vulkan partial extractions.
This cleanup changes formatting only.
Runtime behavior stays the same.

* refactor(agc): keep rebased state in the split renderer

Keep color-metadata state with draw translation after the file split. Restore the complete PM4 helper set. Retain frame publication and metadata tracking that arrived during the rebase.

* fix(gpu): keep acquire memory cache ranges

Keep acquire memory operations separate when their ranges or cache semantics differ.
Merge only matching operations that overlap or touch.
Submit the exact operations as one backend work item to limit queue growth without widening invalidation ranges.
Add focused tests for the changed behavior.

* fix(vulkan): scope guest cache barriers to tracked resources

Use exact buffer and depth-image ranges for ranged guest cache operations. Use a global barrier when the range or image layout is unknown, when no resource matches, or when the operation covers all memory. Record cached texture spans and test each fallback.

* fix(cpu): handle guest image write faults in native code

Keep tracked-page write faults inside the Windows VEH path.
Restore page access, queue dirty pages, and defer managed tracker work until safe execution.
This prevents CLR fail-fast crashes and avoids full tracked-page scans.
Add focused tests for the changed behavior.

* fix(agc): track constant fills as managed writes

Call the managed-writer tracker after the constant-fill fast path writes guest memory. Do not protect a range that the managed memory layer already reports. This prevents an immediate write fault on the same pages.

* fix(cpu): make exception memory reads safe

Probe diagnostic addresses before reading them.
Use ReadProcessMemory for unstable data and keep direct reads only for executable bytes that are active during dispatch.
This prevents the crash reporter from causing a second access violation while it reports a guest fault.
Add focused tests for the changed behavior.

* fix(loader): protect the guest red zone on Windows

Patch faultable memory instructions in System V red-zone functions to run through nearby trampolines.
The trampoline moves RSP while Windows handles a write fault, so the handler cannot overwrite live guest values below the stack pointer.
Add focused tests for the changed behavior.

* fix(audio): correct AJM job argument and size handling

Read the gapless result pointer from the fifth ABI argument and keep reset as the fourth argument.
Use the split-job base size before adding buffer descriptors.
This keeps batch cursors and result writes aligned with AJM.
Add focused tests for the changed behavior.

* fix(vulkan): use all attachments for framebuffer identity

Clamp the framebuffer extent to every color and depth attachment.
Include the color format and view in the depth-framebuffer cache key.
This prevents invalid reuse when one guest image changes attachment views or formats.
Add focused tests for the changed behavior.

* fix(videoout): align captures with complete guest frames

Start a capture at a guest-frame boundary and keep it active across presenter ticks.
End the capture after the next guest flip completes.
Discard interrupted or timed-out captures so later requests can continue.

* fix(shader): keep forward branches after program end markers

Allow a forward scalar branch to target reachable code after S_ENDPGM.
Preserve instruction boundaries and reaching definitions across the program-end marker.
Add a regression test for this control-flow form.

* fix(shader): apply fragment export validity

Apply the EXP.VM flag when a fragment export is conditionally valid.
Suppress invalid color and depth exports in both shader backends.
Add translation and output-mask tests.

* fix(vulkan): declare graphics subgroup operations

Enable subgroup capabilities only for graphics modules that use subgroup instructions.
Declare the required SPIR-V capabilities and execution modes.
Add interface validation tests for vertex and fragment stages.

* fix(vulkan): use flat fragment subgroup inputs

Decorate integer fragment subgroup built-ins with Flat so generated SPIR-V satisfies Vulkan interface validation.
Add focused tests for the changed behavior.

* fix(storage): return usable storage capacity

Add the temporary-data capacity export and saved-data search alias, then report total and free blocks using the platform ABI.
Add focused tests for the changed behavior.

* fix(videoout): send capture requests to guest-frame control

Send the capture hotkey request to the guest-frame capture state machine.
Do not start a host-present capture directly from the window event.
This keeps one request aligned with one complete guest frame.

* fix(videoout): apply metadata-backed image clear patterns

Track metadata fills and constant-buffer fills with their full 16-byte pattern and physical image size.
Decode clear values by target format, including repeated half-float texels.
Apply pending clears when the guest image becomes available.
Add tests for tiled allocation sizes, metadata state, and clear patterns.

* chore(videoout): trace selected image synchronization paths

Add address-filtered traces for compute destinations, guest-image overlaps, and sampled-image binds.
Keep the traces off unless the related environment option is set.
Use the output to compare image synchronization without broad logging.

* fix(vulkan): require a compatible wave size for graphics subgroups

Enable graphics subgroup lowering only when the device reports a compatible native wave size.
Pass the selected policy into shader translation and shader cache keys.
Add policy and interface tests.

* fix(agc): preserve composite depth target extents

Decode attachment width and height from composite depth binding packets and carry them separately from independent context-register state.

Detach irreconcilably mismatched depth attachments instead of shrinking color passes, with focused coverage for packet parsing and fallback behavior.

* fix(cpu): preserve full-width guest callback results

Return the complete 64-bit value from direct and worker-backed guest calls instead of truncating it to 32 bits.

Add fourth-argument support for guest callbacks and cover both ABI behaviors in the native return smoke test.

* fix(shader): key translated programs by checksum

Include programmed shader checksums in decode and compiled-pipeline cache identities so replaced code at a reused guest address cannot resolve to stale translations.

Propagate checksums through compute, graphics, and submitted-geometry translation and cover address reuse with a regression test.

* fix(agc): skip rectangle draws without parameter exports

Track enabled shader parameter exports and reject only non-indexed rectangle draws that have no vertex inputs or parameter exports but still request interpolated pixel inputs.

Treat the rejection as intentional and cover the guarded and preserved draw shapes with focused tests.

* diagnostics(agc): trace video draw chains

Track decoded video planes through recent render-target dependencies and report texture payload summaries, generations, blend state, and viewport data for targeted analysis.

Keep the trace flag, rectangle-skip visibility, and flip-bounded diagnostic state together in this commit.

* fix(agc): snapshot rotating video buffers per draw

Recognize registered decoded-video buffer addresses independently of tracing and bypass address-only upload and texture-content cache shortcuts for those ranges.

This preserves the texels associated with queued draws when the decoder later reuses a rotating guest buffer.

* fix(video): queue decoded frames and honor seek state

Start video decoding with playback, buffer decoded frames on a bounded worker queue, and distinguish pending data from end of stream instead of blocking guest polling.

Preserve pause state across jumps, seek audio and video decoders to the requested timestamp, offset reported times, and deliver the completion warning through the event callback ABI.

* fix(video): load replacement-backed sources through callbacks

Read replacement-backed sources through the guest open, size, offset-read, and close callbacks, materializing a temporary host file for decoding.

Fall back to the ordinary resolved path when the optional replacement interface declines a source, and clean up owned temporary files when sources or players are released.

* fix(video): report decoded stream metadata

Preserve display aspect ratio, color range, primaries, transfer characteristics, frame pitch, and crop information when reporting decoded video streams and frames.

Use the stream's fallback frame rate when its average rate is unavailable, and cover the extended ABI field layout with focused tests.

* fix(video): report extended audio stream format

Populate channel count and sample rate in extended stream details so clients configure decoded PCM output correctly. Add ABI coverage for the audio union layout and duration.

* fix(shader): preserve full LDS single-address offsets

Combine both encoded offset bytes for single-address data-share operations while retaining independent scaled offsets for paired forms. Apply the correction to both shader backends and add decoder and generated-module regression coverage.

* fix(shader): preserve depth comparison filter semantics

Decode the sampler comparison function instead of assuming one operation, and evaluate linear LOD-zero depth filtering per texel before interpolation. Add focused generated-module coverage for comparison functions and the four-tap filtering path.

* fix(agc): create native interpolant mappings

Implement dbOlWdppb4o as sceAgcCreateInterpolantMapping2 and emit packed SPI_PS_INPUT_CNTL register pairs from matched shader semantics. Fill unused entries with identity mappings and cover identity, matched, default, flat, custom, and f16 modes.

* fix(agc): construct primitive state registers

Build context and user-config primitive state independently, merge optional hull-stage state, and select the correct output primitive source. Cover hull handling and derived versus shader-owned output primitives.

* fix(agc): emit native indirect register packets

Emit and parse native indirect context, shader, and user-config register packets. Patch native address and count fields without clobbering control bits, retain legacy packet compatibility, and apply flagged depth-size entries to the correct state.

* fix(agc,video): support guest depth-stencil state

Decode guest stencil attachment, clear, mask, reference, compare, and operation state. Create compatible Vulkan depth-stencil resources and preserve their aspects through render passes, pipeline caching, feedback copies, and barriers.

* chore(diagnostics): add focused guest graphics tracing

Add canonical draw-state summaries with optional descriptor detail filters. Improve guest instruction sampling with thread selection and per-window accounting, and trace register-default handoff provenance.

* fix(video): publish cache identity for depth aliases

Reuse the exact sampled-texture content identity when a depth-backed image resolves successfully. Publish its sampler and write generation so later submissions avoid redundant guest-memory snapshots that the presenter would discard.

* chore(diagnostics): profile guest thread execution

* chore(diagnostics): profile graphics command processing

* chore(diagnostics): profile texture preparation

* chore(diagnostics): profile shader evaluation

* chore(diagnostics): profile address synchronization

* chore(diagnostics): unify performance profiling switches

* fix(video): disable face culling for rectangle primitives

Keep expanded rectangle draws visible regardless of triangle winding. Preserve culling and all other raster settings for other primitive types.

Add regression coverage for each culling combination and unchanged non-rectangle raster state.

* fix(agc): keep depth clears separate from color targets

Remove the color-clear request triggered by the depth-clear control bit. Keep the existing depth, stencil, and color-metadata clear paths unchanged.

* fix(shader): honor pixel input counts and mapped locations

Read only the programmed pixel input controls and use identity mappings beyond that range. Share duplicate-location resolution between both shader backends.

Test input-count bounds, zero-input state, identity fallback, duplicate mappings, and location limits.

* chore(diagnostics): trace interpolant packet and draw mappings

Add opt-in packet fingerprints, register pairs, and consumed draw mappings through SHARPEMU_TRACE_AGC_INTERPOLANT_FLOW.

Limit packet output and unique draw records to 1024 entries. Stop packet scans after the trace limit and keep diagnostic state bounded.

* feat(memory): add GPU memory tracking foundations

Track mapped ranges and page access state.
Keep unresolved faults on the existing error path.
Report memory counters once when shutdown starts.

* feat(memory): map direct memory through shared backing

Create portable host views over a persistent guest backing object.

Route direct and flexible mappings, protection, queries, unmaps, and aliases through one owner.

Preserve private-memory behavior and cover lifecycle, overlap, and failure paths.

* feat(gpu): schedule submissions with timeline completion

Route graphics and compute work through one queue and scheduler. Replace per-submit fences with timeline ticks and preserve batch accounting on every submission path.

Add synchronous worker relay, ordered completion callbacks, and shutdown tests. Keep optional video frame polling nonblocking to prevent a relay deadlock.

* fix(gui): drain pending logs before closing the file

Save all queued output at shutdown without the display batch limit.

Keep console output when file logging is disabled. Add tests for complete and batched drains.

* fix(cpu): bypass managed handlers for debug output

* fix(vulkan): correct image barriers and release texture resources

Include attachment reads and select the source stage for presentation uploads.

Release cached textures at shutdown and retain ownership of sampled image views.

* fix(kernel): service guest exceptions during mutex waits

Deliver pending exceptions outside the mutex lock and resume the wait.

Test delivery without granting ownership and isolate the shared scheduler state.

* feat(gpu): replace buffer snapshots with a tracked buffer cache

Keep guest buffers in device memory and upload changed ranges on use.

Recover CPU access through GPU downloads and preserve guest page permissions.

Manage buffer lifetime through submission completion and remove the old writeback path.

Add buffer, tracking, fault recovery, and descriptor synchronization tests.

* fix(vulkan): use cached buffers for GPU-written geometry

Read current vertex and index data from the buffer cache instead of stale CPU snapshots.

Preserve buffer offsets and complete allocation merges before resolving draw handles.

Add tests for index routing, vertex bindings, and overlapping geometry allocations.

* feat(diagnostics): trace GPU submissions and guest memory reads

Keep a bounded submission history and report memory mappings when buffer reads fail.

Trace scalar loads for the selected shader and reset operation details for each recording.

Add tests for submission history and memory-range reports.

* feat(gpu): replace image snapshots with a tracked image cache

* fix(shaders): bound scalar load memory ranges

* fix(shaders): preserve image bindings across branch paths

* fix(videoout): use the active presentation target

* feat(diagnostics): report image cache and presenter costs

* perf(agc): skip unused context updates during geometry capture

* perf(gpu): skip image uploads before full color clears

* perf(gpu): wake the presenter when submissions complete

* perf(agc): reuse retained vertex data before capture

* feat(diagnostics): report image upload preparation costs

* feat(diagnostics): report vertex snapshot and prepass costs

* feat(gpu): replace command processing and label handling

* test(gpu): use asynchronous waits in the image lock test

* fix(memory): finish GPU work before taking mapping locks

* perf(memory): avoid temporary segments for single-mapping reads

* perf(shader): cache scalar analysis query results

* perf(shader): cache scalar load bounds and batch component reads

* feat(diagnostics): report draw and scalar evaluation costs

* fix(videoout): widen the performance overlay and align it right

* test(videoout): isolate flip state and limit port cleanup

* refactor: remove unused fields and unreachable branches

* fix(kernel): use a private lock for event waiter completion

* feat(gpu): replace draw and compute translation

* fix(memory): select the first available guest address

* fix(gpu): invalidate tracked memory across partial ranges

* perf(gpu): stream repeatedly written buffer pages

* fix(shader): preserve image array coordinates

* fix(shader): order shared memory access across wave lanes

* perf(gpu): cache decoded shader instruction metadata

* fix(gpu): preserve command failures when error logging fails

* feat(profiling): add detailed render and submission timings

* feat(profiling): add optional presentation frame history

* fix(gpu): use storage images for stencil writes

* fix(gpu): support compressed image sampling on macOS

* fix(gpu): check graphics subgroup stage support

* fix(cpu): preserve pending waits during signal delivery

* fix(memory): avoid reserved graphics addresses on macOS

* fix(cpu): preserve vector registers during macOS signal recovery

* fix(loader): protect guest memory accesses on macOS

* fix(avplayer): preserve the playback clock during video catch-up

* fix(kernel): write the complete local time conversion result

* feat(http): parse URI components into guest memory

* feat(profiling): separate GPU wait and submission cleanup time

* feat(profiling): add GPU command completion intervals

* fix(profiling): use the configured shader dump directory

* feat(diagnostics): add Windows crash dump capture

* fix(gui): save console placement and hide it on Options

* fix(renderdoc): use F12 for capture and show the shortcut

* feat(gui): add crash dump controls and enable app0 writes by default

* feat(gui): add overlay modes, corner controls, and GPU usage

* fix(cpu): remove unsafe SSE4a instruction rewrites

* fix(cpu): preserve call width and registers in TLS load patches

* fix(loader): copy TLS initialization bytes after relocation

* fix(memory): extend retained reservations through free ranges

* fix(memory): retry faults after backing view restoration

* perf(memory): use binary search for mapped ranges

* perf(memory): track direct allocations and free ranges

* perf(gpu): reuse completed drains when no work is pending

* fix(gpu): identify image upload failures

* feat(diagnostics): profile guest memory operations

* feat(gui): add performance profiling options

* fix(cpu): allow concurrent native fault callbacks

* fix(cpu): respect instruction boundaries in TLS patches

* fix(cli): preserve the assembly path during relaunch

* fix(cpu): recognize time reads in import loop checks

* fix(diagnostics): capture native thread state safely

* fix(agc): validate shader headers and direct register exports

* fix(gpu): apply bounded embedded vertex offsets

* test(shader): verify unsigned difference instructions

* fix(shader): separate global memory source and destination registers

* fix(debugger): restrict frontend requests and validate tokens

* feat(gui): add the community link

* fix(shader): preserve buffer descriptors across branches

Keep incoming register values for alternate resource paths. Resolve each vertex fetch once and preserve shared bindings. Add branch regression tests and a gated invalid-address trace.

* fix(diagnostics): handle Windows debugger breaks during crash capture

Identify debugger breaks from the system module and thread entry. Resume only verified first-chance breaks and preserve application exception handling. Add regression tests for repeated breaks and crash dumps.

* fix(memory): recover late writes after page protection is removed

Track write-access restoration for each watched page. Retry a late write only when the mapping and host permissions still allow it. Reject repeated faults and stale mapping evidence. Add page-watch and native fault regression tests.

* feat(gpu): replace shader and pipeline caches

Resolve shader resources with a cached value graph. Share binding layouts between Vulkan and Metal. Keep descriptors until each GPU submission completes. Add shader access to device addresses and global data share memory. Correct typed buffer formats. Remove the replaced code paths.

* fix(shader): add signed 24-bit multiplication

Decode signed 24-bit multiplication in both instruction formats. Sign-extend the low 24 bits of each input. Store the low 32 bits of the product. Preserve the destination when its lane is inactive. Add Vulkan and Metal translation support and regression tests.

* fix(shader): add signed 16-bit packing

Decode signed 16-bit packing in the extended instruction format. Clamp each signed input to the 16-bit range and pack both results into one register. Add Vulkan and Metal translation support. Preserve inactive destinations and add regression tests.

* fix(shader): add 64-bit shared-memory reads

Decode single and paired 64-bit reads. Load local or global shared memory with the required offsets. Preserve the source address when destination registers overlap it. Keep inactive destination registers unchanged. Add Vulkan and Metal translation support and regression tests.

* fix(shader): add paired 64-bit shared-memory writes

Decode both paired-write forms. Write independent source-register pairs to local or global shared memory. Apply eight-byte or 512-byte offset scales. Use only the first source pair when offsets are equal. Preserve memory for inactive lanes and add regression tests.

* fix(shader): add thread-indexed local-memory writes

Decode the thread-indexed write instruction. Form each address from the low 16 bits of M0, the byte offset, and four times the lane index. Preserve memory for inactive lanes. Enable the required Vulkan lane input and add regression tests for both shader backends.

* fix(shader): handle traps without an installed handler

Decode the shader trap instruction and retain its eight-bit trap ID. Treat the instruction as a no-op under the current no-handler model on Vulkan and Metal. Preserve shader state and continue execution. Keep unknown-opcode checks and add regression tests.

* perf(gpu): avoid allocations in small image queries

Store up to 16 image identifiers inline and use overflow storage for larger queries. Replace the capturing owner callback with an indexed loop. Preserve query order, duplicate filtering, and snapshots during image removal. Add allocation and query regression tests.

* perf(avplayer): reuse private video frame buffers

Recycle a delivered frame only after a new frame is available. Keep reusable storage separate for each playback and drain queued frames before reporting completion. Reuse the conversion buffer after its pending bytes are consumed. Add frame-lifetime and storage-reuse tests.

* perf(agc): copy register packet payloads in bulk

Copy separate register ranges through the guest memory interface. Keep forward word copies for overlapping ranges or when bulk copying fails. Leave null-source payloads unchanged for the caller to fill.

* perf(memory): avoid segment lists for single-mapping transfers

Write and copy through the backing alias when each range fits one mapping. Preserve range checks, locking, and overlap handling. Keep the existing path for transfers across mappings.

* perf(memory): allow independent protection updates

Use address-range locks for transient protection updates. Keep mapping changes and disposal exclusive. Preserve lock order, release locks on failure, and reject protection requests after disposal.

* perf(cpu): wake ready threads with queue notifications

Replace ready-thread polling with notifications under the scheduler lock. Notify the dispatcher when work is queued, an executor is released, or shutdown starts. Finish slice accounting before releasing executor ownership.

* fix(videoout): validate output settings and report refresh codes

Use refresh-rate codes in output status. Check configuration arguments and reject unavailable output modes. Preserve validation errors and output buffer boundaries.

* fix(videoout): share vblank counts across waits and events

Use one display clock per port for vblank status, waits, and events. Report counters for the latest vblank boundary. Wake the event worker when subscriptions change or shutdown starts.

* fix(videoout): apply per-port flip mode timing

Use each port refresh rate to pace flips. Apply window margins and keep ASAP flips unrestricted. Let multiple-flip requests share the next vblank boundary after readiness without applying the flip rate. Cancel CPU waits when the port closes.

* test(agc): cover register copies across backing views

Verify register payload bytes, the returned packet address, and the command-buffer cursor when the source and destination span separate backing views.

* feat(diagnostics): report slow window event polls

Record poll totals and the four longest calls per report interval. Require frame tracing and an active render scope. Preserve event handling and omit event types when no event is returned.

* feat(diagnostics): profile register packet writes

Measure import dispatch, packet setup, and payload copies. Count copy paths and emitter results. Require performance profiling and frame tracing, and report cumulative inclusive times.

* feat(diagnostics): profile shader resource resolution

Measure resource resolution phases, managed allocations, and source activity. Require performance profiling and frame tracing. Report cumulative inclusive times with invariant decimal formatting.

* feat(diagnostics): save shader planning inputs

Save decoded instructions and resource graphs before descriptor validation. Record planning failures under the existing shader dump switch and address filter. Report file errors without replacing the shader rejection.

* feat(diagnostics): save shader variant inputs and modules

Save cache comparisons before translation and pair each variant with its compiled module. Capture indirect image failures under the existing shader dump controls. Create failure callbacks only when the dump gate matches.

* perf(memory): avoid copies in dirty-page range checks

Test intersecting page-mask words directly instead of creating a masked copy. Preserve half-open range bounds and existing region validation.

* feat(diagnostics): profile buffer uploads

Report buffer upload ranges, byte counts, and slow synchronization calls under frame tracing. Associate device-address uploads with shader program hashes. Read hot-page byte counts under the region lock. Keep counters bounded and report decimals in a fixed format.

* feat(diagnostics): trace guest thread handoffs

Record bounded thread transitions, mutex handoffs, and semaphore signals under frame tracing. Preserve thread identities and wake results. Reset traces at session start and write them at shutdown. Keep trace output failures from interrupting native cleanup.

* feat(diagnostics): trace command submission flow

Record bounded submission, queue, and presenter wait events under frame tracing. Correlate records by queue and submission identifiers. Reset the trace at session start and close it at shutdown. Keep trace output failures from interrupting cleanup.

* feat(diagnostics): profile memory faults and protection

Record fault, protection, and lock timings under frame tracing. Separate tracker and image-cache activity. Prepare thread-local state before guest execution. Report cumulative measurements without changing memory protection or lock ownership.

* feat(gui): expose strict shader mode and enable it by default

Add the strict compute switch to global and per-game debug options. Stop on shader rejections unless skip mode is explicitly selected. Preserve disabled overrides and apply the saved choice at launch. Document the default and skip behavior.

* fix(shader): implement unsigned vector xor-add

Decode V_XAD_U32 and apply XOR followed by wrapping addition in Vulkan, Metal, and resource evaluation. Add tests for literal sources, overlapping destinations, and inactive execution lanes.

* fix(gpu): replace incompatible image backing types

Replace single-row image backings when a view requires a different dimensional type. Preserve cached subresources, current contents, and deferred release. Copy between 1D and 2D images through a GPU buffer. Add cache and binding regression tests.

* fix(shader): add thread-indexed local-memory reads

Decode the thread-indexed read instruction. Form each address from the low 16 bits of M0, the byte offset, and four times the lane index. Preserve inactive destination registers. Add Vulkan and Metal translation support and regression tests.

* test(stdio): synchronize concurrent file operations

Run blocking file operations on dedicated threads. Check that close and seek block before releasing the read. Join workers before cleanup and identify failed synchronization checkpoints.

* fix(cpu): read Linux diagnostic memory safely

Use process_vm_readv to read host memory outside the allocation tracker. Reject incomplete reads and test access across a protected page.

* fix(cpu): defer native callback allocation

Create the import callback thunk when it is first needed. Reject native execution outside an x64 process and declare that requirement in backend tests.

* fix(memory): use address-space protection granularity

Keep the native page-size requirement and let test address spaces declare their granularity. Use one host-memory backend for tracker allocation and queries. Keep the test fatal handler active through cleanup.

* test(cpu): isolate POSIX signal recovery state

Prevent signal recovery tests from overlapping other test collections. Restore the previous backend after each test.

* test(metal): resolve fixture buffer descriptors

Materialize buffer descriptors before compiling compute fixtures. Check the resolved formats and update the matching shader snapshots without changing expected device results.

* test(memory): require supported native page protection

Declare the 4 KiB host page requirement for image tracking and backing-view fault tests. Skip these tests on unsupported hosts without changing production checks.

* fix(memory): isolate write-tracker warm-up pages

Use a dedicated host mapping for protection warm-up. Release the mapping after removing its tracker entry. Check native page permissions in the tracker tests.

* fix(memory): track POSIX backing-view query state

Share view mapping and protection records across host memory queries. Preserve committed state for inaccessible pages and round partial-page changes to host pages. Update protection records without allocating during faults.

* test(memory): use valid portable guest ranges

Probe free addresses within the guest range instead of relying on fixed or host-selected addresses. Respect guest and host alignment. Allocate output storage independently of fixed guest image addresses.

* fix(memory): preserve execute permissions during writes

Request writable protection without execute permission for non-executable private memory. Add a regression check that rejects unnecessary executable protection. Restore the original protection after the write.

* fix(cpu): resolve Linux TLS through libpthread

Load Linux thread-local storage functions from libpthread.so.0. Resolve the native TLS lookup from the same library to support glibc versions before the libc merge. Keep macOS imports unchanged.

* fix(kernel): connect sockets without thread-pool waits

Use synchronous TCP connection handling for the blocking guest call. Remove the fixed task wait that could reject connections under thread-pool load. Add tests for occupied workers, refused connections, and descriptor lifetime.

* fix(gpu): prepare device memory and guard inactive loads

Keep active buffers resident and upload read-only device-address ranges before use. Preserve pending faults when the output buffer is full. Prevent inactive lanes from loading global memory.

* feat(gpu): trace device-address memory faults

Select the first fault page when memory tracing is set to auto. Record the shader, instruction, and address for a fault. Trace buffer registration, preparation, and collection for the selected page.

* feat(gpu): trace buffer readback callers

Record buffer readback callers, ranges, byte counts, and elapsed time. Bound the collected ranges and report calls beyond the limit. Enable these details only when both performance profile switches are set. Add tests for caller separation, report gates, and counter limits.

* perf(kernel): skip occupied ranges during address search

Check kernel reservations before asking the backing store to reserve an address. Keep aligned gaps and reusable reservation hints. Add tests for occupied ranges, alignment, and address overflow.

* fix(gpu): correct shader resource selection and data sharing

Support data-share swizzles on Vulkan and Metal. Bound indirect resource selectors from wave-mask inputs. Resolve sampled image variants with their resource types and include selector state in program identity. Add compiler and device regression tests.

* refactor(gpu): centralize guest buffer ownership and lifetime

Track buffer identity, page ownership, recency, and retirement in one registry. Separate upload preparation, download planning, merge bounds, and collection policy. Preserve GPU synchronization and add registry and device regression tests.

* fix(shader): support unsigned 16-bit packing

Clamp each unsigned input to 16 bits. Pack the first input into the low half and the second input into the high half on Vulkan, Metal, and the resource evaluator.

* fix(memory): reject overlapping fixed allocations

Reject committed pages when an exact allocation shares a host reservation. Keep adjacent allocations valid and preserve existing memory regions.

* fix(kernel): implement AIO waits and correct array arguments

Return recorded completion states for single-request waits. Use the correct argument order for array waits and polls. Validate request IDs and guest memory accesses.

* fix(shader): implement scalar signed absolute value

Decode S_ABS_I32 and emit it on Vulkan and Metal. Preserve the minimum signed value and update the scalar condition flag. Match these results in resource evaluation.

* fix(shader): implement three-input vector XOR

Decode V_XOR3_B32 and combine all three inputs on Vulkan and Metal. Apply the same operation during resource evaluation.

* fix(shader): implement scalar bit clear

Decode S_BITSET0_B32 and clear the selected destination bit on Vulkan and Metal. Preserve the condition flag and retain the destination input for scalar bit updates.

* fix(gpu): keep depth export format from enabling pixel shaders

Use shader control flags to detect depth and coverage effects. Do not activate a pixel shader for a depth-only draw because an export format remains set.

* feat(runtime): trace failed imported data writes

Report whether a failed relocation target is readable and list its tracked memory regions. Keep the details under the existing data-rebind logging switch.

* feat(kernel): trace guest-tracked mutex self-locks

Include adaptive mutex self-lock failures in the existing fast-path trace. Keep the lock behavior and return codes unchanged.

* feat(savedata): trace failed mount requests

Report the directory and mount mode when a save mount is missing or busy. Use the existing save-data logging switch.

* feat(gpu): trace pixel shader draw inputs

Include the pixel shader address, hash, and user data in the existing AGC trace.

* feat(gpu): trace pixel register writes

Log pixel shader register writes when AGC tracing is enabled. Include the packet or table address and the shader address.

* feat(gpu): report sources of invalid shader resources

Include shader and descriptor details when a buffer starts in unmapped memory or a sampler has an invalid anisotropy ratio. Keep the existing validation checks.

* fix(kernel): correct AMPR event filter identifiers

Use the required filter identifiers for AMPR and AMPR system events. Add a regression test for both values.

* fix(gui): add missing Azerbaijani option text

Add labels and descriptions for crash dumps, performance profiling, strict shader checks, and the performance display.
2026-09-20 04:09:27 +03:00
Berk 5c0adf81ca
[CI] Fixes to warnings in workflows and scripts (#892)
* [CI] Fixes to warnings in workflows and scripts

* [CI] rename build job to win-x64

* [CI] sorry for version

* [AGC] Disable warning SHEM004 for AgcExports.cs
2026-09-20 02:44:53 +03:00
ParantezTech ca64b4a7e8 [languages] fixed language keys for Azerbaycanca and correct order for keys 2026-09-20 01:26:55 +03:00
Jamal Kamaladdinoglu 9d99fefd13
[GUI] Azerbaijani language (#890) 2026-09-20 00:11:51 +03:00
Berk c69e88e0a8
Gui improvements part3 (#891)
* [docs] Add documentation for undocumented GUI environment variables

* [GUI] Added Custom Environment Variables dialog and settings

* reuse
2026-09-19 23:26:30 +03:00
Berk 179fcbbfe5
[Kernel] Fix printf %S to be treated as %ls, including for the wide-output family (#889) 2026-09-19 15:00:24 +03:00
Berk b6f71b312b
[appcontent] Saving temp to user directory instead of appdata (#885) 2026-09-17 18:31:36 +03:00
ParantezTech dfb7efa5df Merge branch 'main' of https://github.com/sharpemu/sharpemu 2026-09-11 16:47:44 +03:00
ParantezTech 3c91c768f3 Added official Discord server readme and GUI. 2026-09-11 16:43:38 +03:00
Mathias f84246cf90
Fix/agc rendering and module loading (#881)
* test(cpu): verify VEH thunk preserves nonvolatile registers

* fix(memory): commit complete native guest mappings

* fix(cpu): treat time polling as import-loop progress

* fix(cli): preserve DLL path in mitigation relaunch

* refactor(cpu): remove obsolete inflate recovery hack

* fix(agc): accept shaders without embedded program registers

* fix(cpu): treat user event polling as import-loop progress

* fix(shader): lower unsigned sum of absolute differences

* fix(agc): emit direct shader register packets

* fix(agc): report native wait packet sizes

* fix(agc): propagate embedded vertex fetch offsets

* fix(vulkan): keep vertex attributes relative to upload

* fix(agc): avoid double applying embedded vertex offsets

* docs(agc): clarify embedded vertex offset handling

* fix(gpu): separate vertex IDs from stream offsets

* fix(shader): decode global load destinations

* fix(runtime): index application-root PRX modules
2026-09-11 14:17:37 +03:00
Mathias 0a461f8d69
fix: improve Direct Execution guest runtime and AGC compatibility (#880)
* test(cpu): verify VEH thunk preserves nonvolatile registers

* fix(memory): commit complete native guest mappings

* fix(cpu): treat time polling as import-loop progress

* fix(cli): preserve DLL path in mitigation relaunch

* refactor(cpu): remove obsolete inflate recovery hack

* fix(agc): accept shaders without embedded program registers
2026-09-11 13:37:31 +03:00
Berk ca90d17541
[aerolib-catalog] Add support for multiple Aerolib catalogs (#867) 2026-09-08 18:25:48 +03:00
Berk 6d4e5b4f65
chore: bump version to 0.0.3-release.4 (#866) 2026-09-08 16:04:47 +03:00
Berk d93ecaafa3
[CPU] Fixes a Windows VEH synchronization regression (#865) 2026-09-08 15:56:54 +03:00
Berk 33f590b381
chore: bump version to 0.0.3-release.3-patch.1 (#864) 2026-09-07 14:08:50 +03:00
Mathias b8f24219ae
fix VEH trampoline REX prefix and TLS patch boundaries (#859)
Fixes the REX.B prefix on the VEH entry spinlock's lock cmpxchg, which was
compare-exchanging on [rcx] instead of [r9] and therefore never guarding
anything.

Matches TLS and stack-canary patterns at instruction boundaries instead of
byte by byte. Without it GTA V dies at ~5s on a write through a register the
patcher had left unloaded; with it the title reaches its pause menu.

Co-authored-by: Foued Attar <attar.foued@gmail.com>
2026-09-04 21:07:58 +02:00
Berk 3831f3c407
Fix debugger frontend (#858)
* [security] harden debugger frontend API

* [docs] document debugger frontend security boundaries
2026-09-03 05:24:41 +03:00
frangametv b226dca3df
feat(hle): prefer loaded guest exports for selected registrations (#844)
Co-authored-by: Acelogic <miguelc4600@gmail.com>
2026-08-25 22:32:57 +02:00
Foued Attar 51e5480049
fix(cpu): preserve TLS instruction boundaries after rel8 branches (#846) 2026-08-24 22:52:30 +02:00
frangametv 600fcde637
fix(memory): expand the HLE guest-allocation arena (#843)
Co-authored-by: Foued Attar <foued.attar@lyceeastier.com>
2026-08-24 19:23:43 +03:00
frangametv 807aad18b3
tools(shader): inspect raw Gen5 shader dumps (#842)
Co-authored-by: Acelogic <miguelc4600@gmail.com>
2026-08-24 19:23:18 +03:00
frangametv f4f36b558f
fix(agc): refine vertex layouts without rebasing captured inputs (#841) 2026-08-24 14:49:36 +02:00
frangametv 2b8ef7d8fa
shader: add compact f16 arithmetic and compare lowering (#840)
Co-authored-by: Foued Attar <attar.foued@gmail.com>
2026-08-24 13:59:51 +02:00
angleyanalbedo f8a826ec1b
fix(agc,video): CMASK/DCC metadata state machine , remove heavy heavy motion trails in DeadCell (#836)
* fix(agc): add support for CMASK fast clear in rendering process

* fix(agc): prevent stale cross-frame label writes from bypassing WAIT_REG_MEM

Add frame-ID tracking to GpuWaitRegistry so that WAIT_REG_MEM in frame N+1
is not satisfied by a label written in frame N. Previously, a label that
persisted in guest memory from the previous frame could satisfy a new
WAIT_REG_MEM immediately, causing the waiting DCB to bypass its fence and
execute out of order.

Changes:
- GpuWaitRegistry: add _labelFrameIds, _currentFrameId, AdvanceFrame(),
  IsLabelFresh(); RecordProduced now stamps the frame ID on each write
- AgcExports: call AdvanceFrame() at RFlip (frame boundary);
  HandleSubmittedWaitRegMem checks IsLabelFresh before bypassing

This is one piece of the Dead Cells character visibility fix (issue #833).
The cross-frame label reuse could cause composite passes to execute before
character-layer passes, resulting in invisible characters.

* fix(video): simulate CMASK 'all clear' at frame boundary to prevent trails

On real PS5, CMASK is reset to 'all clear' at each frame boundary,
so render targets are effectively cleared. SharpEmu did not implement
this behavior, causing GBuffer targets to LOAD stale data from the
previous frame, resulting in visible trails/ghosting.

Reset Initialized=false for all offscreen guest images at the present
boundary (when _currentFrameSlot changes). This ensures every render
target starts each frame with LoadOp.Clear, matching the hardware's
CMASK behavior.

This is the trail fix for Dead Cells (issue #833), complementing the
cross-frame label staleness fix in GpuWaitRegistry.

* fix(video): only reset render targets (not textures) for CMASK simulation

Previous commit 89b4610 reset Initialized for ALL guest images, including
textures and storage images, causing a completely black screen. Only images
with a RenderPass (actual color render targets) should be reset, as CMASK
'all clear' only applies to color render targets, not sampled textures.

* Revert "fix(video): simulate CMASK 'all clear' at frame boundary to prevent trails"

This reverts commit 89b4610a015619848e4000372822dca1c08051ff.

* fix(agc): implement CMASK state machine for render target clearing

Implement CMASK (Color Mask) hardware behavior based on shadPS4's approach:

1. Track CMASK addresses from CB_COLORn_CMASK registers
2. Detect compute shaders that write to CMASK addresses (IsComputeMetaClear
   heuristic: no bitwise XOR in shader = clear shader)
3. Mark CMASK as 'all clear' when compute shader writes to it
4. EliminateFastClear mode now checks CMASK state before clearing

This addresses the trail/ghosting issue in Dead Cells (issue #833) where
642x362 GBuffer targets were never cleared after first use, causing stale
data from previous frames to persist.

Reference: shadPS4's IsComputeMetaClear + EliminateFastClear implementation.

* fix(agc): implement CMASK state machine for render target clearing

Based on shadPS4's PM4 implementation, add CMASK (Color Mask) hardware
behavior tracking:

1. TrackCmaskAddresses: Read CB_COLORn_CMASK registers to get CMASK addresses
2. CheckCmaskWrite: When DMA fill targets CMASK address with value 0,
   mark CMASK as 'all clear' (shadPS4's FillBuffer logic)
3. EliminateFastClear: Check CMASK state before clearing - only clear
   if CMASK is 'all clear', then mark as 'dirty'
4. DMA fill: Call TrackCmaskAddresses before processing to ensure
   CMASK addresses are registered when DMA fills happen

This addresses the trail/ghosting issue in Dead Cells (issue #833) where
642x362 GBuffer targets were never cleared after first use.

Reference: shadPS4's IsComputeMetaClear + EliminateFastClear + FillBuffer.

* refactor(agc): remove diagnostic CMASK traces from hot path

Remove 4 diagnostic TraceAgcShader calls that were added during CMASK
investigation:
- agc.cb_regs=[...] - allocated List<string> + string.Join on every draw
- agc.cmask_track - per-slot interpolated string on every draw
- agc.cmask_write - per-DMA-fill interpolated string
- agc.eliminate_fast_clear - per-draw interpolated string

These were debugging probes and are no longer needed.

* fix(agc): sync constant-fill zero writes to Vulkan render targets

Dead Cells GBuffer trails: the constant-fill compute kernel
(TrySubmitConstantFillKernel) wrote zeros to guest memory but never
invalided the host Vulkan GuestImageResource that backs the render
target, so the GBuffer pass kept LoadOp.Load stale previous-frame pixels.

After the guest write, request a guest color clear尷 ... clear for the
destination address (RequestAgentColorClear projection (clear)) so the next render pass
uses LoadOp.Clear. The pending clearandray mechanism already exists; the
fill kernel was simply bypassing it.

* Remove AGC lifecycle debug probes

Remove transient render-target lifecycle and indirect draw debug instrumentation from AgcExports. The change strips the noisy console probing while keeping the active render-target tracking logic intact for draw sequencing and validation.

* fix(video): clear frame-stale MRT groups at guest flip boundary

On hardware a colour surface whose fast-clear metadata is in reset
state reads back as the CB clear value instead of stale memory, so
games can leave per-frame MRT groups uncleaned and rely on that
implicit initialization. With no metadata layer, such a target keeps
whatever touched it last; when the group shares addresses with the
compositor's output, the entire finished previous frame bleeds
through every region the new frame does not repaint (Dead Cells
dungeon trails / ghosted characters).

Arm a reset at the guest's own flip command - the authoritative
frame boundary inside the submission stream - and let the first
multi-attachment colour group of the fresh frame consume it,
starting from LoadOp.Clear. One arm per flip yields exactly one
reset per guest frame regardless of CPU/GPU pipelining; later
groups keep load semantics so intra-frame pass chaining is
untouched. CPU-backed images are never touched.

Mirrors shadPS4's meta-state-driven attachment.is_clear at
render-pass begin (vk_rasterizer.cpp BeginRendering).

* fix(agc): correct CbColor0Cmask register offset and add EXT support

CbColor0Cmask was 0x320 (CMASK_SLICE, tile_max:14) instead of 0x31F
(CMASK_BASE_ADDRESS). TrackCmaskAddresses read the slice count instead
of the base address, so CMASK registration always produced garbage or
zero — the entire meta-state chain starved for every game.

Fix: 0x320 → 0x31F. Add CbColor0CmaskBaseExt (0x398) and decode the
full 48-bit address: ((ext & 0xFF) << 40) | ((low & 0x1FFFFFFF) << 8).

Phase 0 probe confirmed Dead Cells programs neither CMASK nor DCC
registers (all zeros) — it relies on unified memory semantics where
unwritten surfaces read as zero. The register offset fix is still
needed for games that do use CMASK metadata.

* feat(agc,video): CMASK/DCC meta-state machine with CLEAR WORD support

Replace the flat _cmaskClearedState dictionary with a proper meta-state
ledger that separates registration, clearing, and dirtying:

- MetaSurfaceInfo keyed by colour-buffer address (not meta address)
- Reverse map _cmaskToColorBuffer for fill/compute write detection
- TrackCmaskAddresses reads both CMASK (0x31F) and DCC (0x325) with
  EXT high-bit decoding; prefers CMASK, falls back to DCC
- CLEAR_WORD captured at registration time, passed through to
  BeginTranslatedRenderPass as VkClearValue
- EFC checks specific surface (not 'any cleared → clear slot0'),
  dirties only that surface (not entire table)
- CheckCmaskWrite uses reverse map for O(1) lookup
- Presenter bind loop queries IsMetaClearedForSurface → LoadOp.Clear
- _metaStateOverridesFlipArm flag gates flip-arm for gradual retirement

Phase 0 probe confirmed Dead Cells programs neither CMASK nor DCC
(all registers zero) — flip-arm remains the correct fix for that game.
The meta-state machine serves games that do use CMASK/DCC metadata.

* fix(agc,video): implement surface clearing at guest flip boundary

* refactor(video): remove flip-arm heuristic, meta-state machine fully replaces it

The flip-arm (_frameColorResetArmed) was a heuristic that cleared the
first multi-attachment colour group at guest flip boundary.  The
CMASK/DCC meta-state machine now handles all clearing: at flip time
MarkAllSurfacesCleared() marks every registered surface as cleared;
at bind time IsMetaClearedForSurface() triggers LoadOp.Clear and
consumes the state.  This replaces the flip-arm with a per-surface
state machine that correctly handles both explicit clears (DMA fill,
compute, EFC) and implicit frame-boundary resets.

Also fixed a bug where TrackCmaskAddresses overwrote IsCleared to
false on every call, defeating the frame-boundary reset.  Now
preserves existing IsCleared state when re-registering.

* Thread-safe meta-surface state & meta-clear decode

Add synchronization for metadata state: introduce _metaSurfaceGate and guard accesses to _metaSurfaces and _cmaskToColorBuffer in AgcExports to avoid concurrent-dictionary corruption and ensure small critical sections. Preserve cleared state only when metadata binding matches during re-registration. Make Gen5 texture format constants internal. Update logic to set/consume IsCleared under lock and check CMASK nearby windows safely.

Decode meta clear values in VulkanVideoPresenter: skip CPU-backed targets for meta clears, add UnpackMetaClearValue and HalfToFloat to convert CLEAR_WORD0/1 into proper ClearColorValue for R8G8B8A8_UNORM and R16G16B16A16_FLOAT formats (with an 8_8_8_8 fallback). This ensures correct clear colours and thread-safe metadata handling.
2026-08-24 13:56:44 +03:00
Foued Attar 4a7a45d1b3
Fix TLS-load patcher corrupting short jumps immediately before FS:[0] reads (#838)
The linear scanner consumed leading 0x66 bytes without checking that the
candidate starts on an instruction boundary. A short jump whose disp8 is
0x66 (EB 66) directly followed by a 66-prefixed FS:[0] load made the
patcher treat the displacement as a prefix and write its call opcode over
it, rewriting 'jmp forward past the TLS access' into 'jmp backward' -- an
infinite loop. GTA V's AGC resource destructors hit exactly this shape and
leaked the entire heap inside a container drain before any frame was
presented.

Reject candidates whose preceding byte is 0xEB: a bare EB can never be the
last byte of a valid instruction, so such a position is provably
mid-instruction. The outer byte scan then retries at the next offset,
which is the true boundary, and the patch lands correctly.
2026-08-23 14:52:44 +02:00
angleyanalbedo 3a744c991e
fix(agc): record write_data produced labels so WAIT_REG_MEM can resume (#834)
ApplySubmittedWriteData wrote label values to guest memory but never called
GpuWaitRegistry.RecordProduced, unlike ApplySubmittedReleaseMem. A suspended
WAIT_REG_MEM on a write_data label (e.g. Dead Cells frame fence 0x10243CFD8)
then could never be latched or deadlock-broken: guest memory is reset to 0 for
frame reuse before the next re-check, and _lastProduced had no value to replay.
Record each written dword and, for increment count2, the combined 64-bit value
so 32/64-bit waits latch like ReleaseMem dataSel=2.
2026-08-21 20:18:30 +03:00
urmoit 35a28f0143
[GUI] Estonian language (#835)
* [GUI] Estonian language

Implements Estonian language support for the SharpEmu's GUI.

* [GUI] Update and refine Estonian translations

Correct grammatical errors, refine technical UI terminology (e.g., view layout descriptions), and improve overall phrasing for better consistency across the launcher UI.
2026-08-21 20:18:07 +03:00
Marcin Mitura e79a1cc70a
[GUI] Polish language (#832) 2026-08-20 19:34:52 +02:00
Mathias a2241d0e83
Fix/agc zero dim and storage (#828)
* fix(videoout): promote guest images to storage usage

* fix(agc): treat zero-dimension indirect compute dispatches as valid no-ops
2026-08-18 21:20:54 +02:00
Foued Attar fe6521f617
Fix unaligned BufferLoad/GlobalLoad dword access
BufferLoadDword/x2/x3/x4, BufferStoreDword/x2/x3/x4, and their GLOBAL
counterparts were routed through LoadUnalignedBufferWord /
StoreBufferBytes, which reconstruct every dword one byte at a time
(4 bounds-checked buffer accesses per dword, each with its own
OpArrayLength + OpSelect + OpAccessChain + OpLoad/OpStore, plus
shift/mask/or reassembly on top).

The GCN ISA guarantees these opcodes are always dword-aligned - only
the byte/short/D16 variants legitimately need unaligned access, and
those already have their own dedicated path
(LoadSubdwordBufferValue / StoreBufferBytes with an explicit byte
count). The generic dword-count loop reached by every other
BufferLoad*/BufferStore*/GlobalLoad*/GlobalStore* opcode was paying
the same per-byte cost for no reason.

Route the dword-granularity path straight through the existing
LoadBufferWord / StoreBufferWord helpers (one bounds check and one
load/store per dword) instead. Measured on a compute shader with 6
BufferLoadDwordx4 instructions in its hottest basic block, this drops
GPU dispatch time for that shader from ~430-448ms to ~86-92ms (~5x)
with no change in output correctness - it is a pure translation
inefficiency fix, independent of any specific title.
2026-08-18 12:20:24 +02:00
Mathias 034ddcc092
fix(vmem): use ConcurrentDictionary for _pageProtections to prevent race corruption (#823) 2026-08-18 12:13:32 +02:00
Berk d9b599a1fd
chore: bump version to 0.0.3-release.3 (#826) 2026-08-18 00:19:31 +03:00
Astell ad115ddcbc
VMovrelsB32 opcode fix (#825) 2026-08-18 00:00:28 +03:00
Foued Attar 7521295ee1
[Codec/Native] Real H.264 decode for sceVideodec2, fix TLS loader missing the main module (#824)
* [Codec/Native] Real H.264 decode for sceVideodec2, fix TLS loader missing the main module

sceVideodec2 was a capability-only stub: the game's video pipeline
worked end-to-end but never produced a picture, so intro/cinematic
videos stayed black even though playback "completed" without errors
(confirmed on Ghost of Yotei's intro cinematic).

- Videodec2Decoder: owns an FFmpeg H.264 session per decoder handle,
  running decode and presentation pacing on their own threads (never
  the guest thread) so a whole clip isn't decoded faster than it can
  be displayed. Converts to BGRA and submits straight to
  VulkanVideoPresenter, bypassing guest memory the same way the
  existing Bink2 path does.
- Videodec2Exports: wires the real decoder into
  sceVideodec2CreateDecoder/Decode/Flush/Reset/DeleteDecoder, falling
  back to the original no-picture stub whenever FFmpeg is unavailable
  or a given decoder failed to open.
- VulkanVideoPresenter: decoded frames were being dropped under a
  single "latest wins" slot the render loop didn't always poll in
  time before the next frame overwrote it. Queues pending video
  presentations the same way guest-image flips already are.
- DirectExecutionBackend: the TLS load patcher's one-shot scan missed
  the main game module when the entry point resolves to a separate
  bootstrap allocation, and never re-scanned lazily-committed pages
  patched in afterward -- both left FS:[0] TLS loads unpatched,
  causing an early mutex-spin boot stall (reproduced on Demon's
  Souls: stuck at import #256). Now scans both the entry point's own
  allocation and the standard PS5/PS4 image base, and re-scans each
  newly committed executable range as it's touched.

* [GUI] Add a toggle for SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES

This flag already existed as an AGC workaround (forces queued GPU
command-buffer preambles through when their target queue never picks
them up, unblocking titles stuck on a WAIT_REG_MEM that never
signals) but was only reachable by setting the environment variable
by hand. Expose it as a checkbox next to the other env toggles, in
both the global Options panel and the per-game settings panel, so it
can be turned on for a specific title without touching a shell.

* [GUI] Add remaining language translations for the new env toggle

The previous commit only added the new key to en.json/fr.json,
relying on Localization's runtime fallback to English -- but
LocalizationTests.EmbeddedLanguages_ContainEveryEnglishOptionsKey
requires every Options.* key to exist in every embedded language
file, which broke CI on all three build jobs. Fills in the
remaining 13 languages.
2026-08-18 00:00:01 +03:00
Astell 1660111189
A vplayer fix (#821)
* Astrobot-video-fix

* Astrobot-video-fix with updated files

* Astrobot-video-fix with tests files
2026-08-17 00:57:42 +03:00
Atson Melo 7caf430aa9
test(cpu): cover Gen5 native return smoke path (#818) 2026-08-11 04:52:49 +03:00
Astell 1e96eca316
Astrobot - Canonicalize fix and ffmpeg library on build (#817) 2026-08-11 00:36:12 +03:00
Foued Attar 498d402577
[HLE] Fix AGC OOB, kevent coalescing, and VideoOut flip/buffer-registration bugs (#808)
sceAgcAddPrimStateRegisters (NID unconfirmed, resolved from the decrypted
eboot's call site) reads 32 register pairs from a buffer that
sceAgcCreatePrimState only partially fills, treating the unfilled part as
guest-stack garbage -- an out-of-bounds probe index sourced from that
garbage was the AV. Zero the unfilled part instead.

GPU interrupt kevents were being coalesced when several arrived close
together, but the AGC driver's interrupt thread expects exactly one
completion per delivered kevent -- coalescing silently dropped completions
and wedged its dependency counters. Deliver one kevent per trigger instead,
with a cap so an undrained queue can't grow unbounded.

sceVideoOutGetFlipStatus left an extended region of its output struct
un-zeroed; some titles poll a flag past the classic struct layout and spin
forever on garbage that never clears. Zero the whole region -- flips
already complete synchronously in this emulator, so it should read as
not-pending anyway.

sceVideoOutRegisterBuffers2 rejected any nonzero category/option, which
some titles use, making them unable to ever register display buffers or
flip. Treat unknown categories as the standard layout instead of failing
the registration.

Tested on Ghost of Yotei (PPSA26344): combined with the separate
ajm-acm-audio change, gets past its audio-init stall and renders/presents a
real GPU frame; neither change alone reaches that point.

Tested on Cult of the Lamb: calls sceAgcAddPrimStateRegisters 39 times, 0
crashes (previously an unresolved import on this exact NID).

Regression-checked on Demon's Souls (both with and without the separate
native-tls-fix change, to reach further into boot), Astro Bot, Outer Wilds,
Ghost of Tsushima, GTA, Minecraft, Quake: no change in behavior on any of
them.
2026-08-09 18:52:23 +03:00
Foued Attar 4b8d45520e
[HLE] Fix AJM/ACM no-op stubs and accept Gen5 AudioOut2 mastering/batch calls (#807)
AjmModuleUnregister/AjmFinalize were pure no-ops (always returned success
without touching state); they now validate the context and actually remove
the codec registration / context entry, returning a real error when the
context is unknown. AjmModuleUnregister traces whether the codec was
actually registered, to help spot a title unregistering something it never
registered.

MaxCodecType was hardcoded to 25 based on known Sony codec ids, incorrectly
rejecting valid Gen5 codec types (e.g. 24). Registration is pure bookkeeping
(HashSet.Add); the only real constraint is that codecType must not overflow
the 32-bit instanceId it gets packed into, i.e. codecType < 2^18. Named the
instanceId bit-packing constants (InstanceIdSlotBits/InstanceIdSlotMask) so
the four call sites that used to hardcode 14/0x3FFF independently can't
drift out of sync, and MaxCodecType's formula is self-evident.

Accept sceAcmBatchInitialize/InitializeLite/Start/StartMultiple/Process as
successful no-ops -- the emulator runs no ACM DSP jobs, but Scream's workers
trap on int 0x41/0x42 asserts whenever a submission call reports failure.

Accept sceAudioOut2MasteringInit/Set3DLatency as no-ops -- the host mixer
has no mastering/object pipeline to tune, but returning failure makes
titles tear down their whole ACM context and abort audio arena bring-up.

Tested on Ghost of Yotei (PPSA26344): sceAudioOut2MasteringInit/Set3DLatency
unresolved-import warnings go from 2 to 0, unblocking 5 audio-related guest
threads that never spawned before (MovieDecoder, snd_stream_parsing_thread,
snd_stream_reader_thread, Psn, NCA::PumpThread), watchdog stall errors from
1 to 0. This alone gets the title past its Scream audio-init stall into
further boot, but it still hits an unrelated infinite retry loop in
sceVideoOutGetFlipStatus shortly after -- fixed by the separate
videoout-agc-hle-misc change; combined, the title renders and presents a
real GPU frame.

Regression-checked on Demon's Souls, Astro Bot, Outer Wilds, Cult of the
Lamb, Ghost of Tsushima, GTA, Minecraft, Quake: 0 calls to any export
touched by this change on any of them, byte-for-byte identical to
upstream/main.
2026-08-09 18:52:15 +03:00
Foued Attar 62c3852556
[AGC] Fix arena/fence tracking stalls and recover lost GPU progress (#770)
* [AGC] Fix arena/fence tracking stalls and recover lost GPU progress

Fix several AGC synchronization issues that could leave GPU work
unsubmitted or fences permanently unsatisfied:

- Recover fence writes missed during arena transitions by tracking
  closed arena tails and resweeping unresolved fence locations.
- Fix chunk submission accounting so only actually parsed ranges are
  considered submitted, avoiding missed orphan recovery.
- Extend arena sweeps to follow chained command ranges without
  duplicate submissions.
- Preserve GPU state tracking across native worker memory wrappers by
  using canonical guest memory identity.
- Fix builder-ring orphan submissions and stale ring-tail waits.
- Improve frame cycling by handling arena reuse, cursor regression,
  and transiently unavailable builder headers.
- Record conditional DCB execution (IT_COND_EXEC) instead of
  silently dropping the packet.

These changes improve AGC progress tracking and prevent GPU fence
starvation/deadlocks on titles relying on builder arenas and indirect
command chains.

* chore: trigger CI
2026-08-07 14:37:09 +03:00
Fa1dz 418eb7ecea
fix(audioout2): wire skipped-submit trace counter (#761) 2026-08-04 03:22:39 +03:00
Foued Attar c086e32f3d
Fix GuestDataPool lease leak on non-GPU compute dispatch paths (#759)
ObserveComputeDispatch only returned evaluation's pooled arrays when
evaluationHandledByCpu was set. Dispatches rejected before submission
(empty resource tables, oversized workgroup, compile failure) or a
compute submit that got dropped instead of enqueued (workSequence == 0)
never handed those buffers to a consumer that would return them,
leaking one lease per occurrence and growing GuestDataPool.Shared
without bound over a long session.

Adds GuestDataPool.DiagnosticStats() (outstanding lease count, idle
cached bytes) surfaced in the periodic [LOADER][PERF] line, to catch
this class of regression going forward.

Verified on Ghost of Yotei and Demon's Souls: pool_leases grew
unbounded before the fix (525 in 5 min on Demon's Souls, 1114 in 180s
on Yotei) and stays flat/bounded after (0-6 and 2-18 respectively),
with no behavioral regression observed.
2026-08-03 21:18:23 +03:00
shpeenut22 9e10d7c44a
Functions exports in kernel and NpTrophy2 (#749)
* sceNpTrophy2GetTrophyInfoArray export

* sceNpTrophy2GetTrophyInfoArray comment changed

* sceKernelIsTrinityMode & sceKernelGetOpenPsId exports

* deleted console log in KernelIsTrinityMode

* sceKernelGetOpenPsId: fix ORBIS_GEN2_ERROR_INVALID_ARGUMENT casts with uint.
2026-08-03 12:54:07 +03:00
ParantezTech 207441ca95 [asset] upload transparent logo image 2026-08-03 10:43:43 +03:00
ParantezTech 6b8f11a468 [asset] Add logo.psd file for future edits and modifications 2026-08-03 10:37:44 +03:00
Berk da0de5cf92
chore: bump version to 0.0.3-release.2 (#756) 2026-08-03 10:15:53 +03:00
Berk 26bda041fa
[videoout] guard degenerate guest buffer ranges (#755) 2026-08-03 10:09:07 +03:00
Berk 8eb2c1e9cb
Renderdoc (#753)
* [videoout] added renderdoc in-app capture

* [gui] added renderdoc toggle and debug group

* [gui] added renderdoc strings to all languages
2026-08-03 10:01:41 +03:00
Foued Attar f3d9439952
Fix Vulkan presenter synchronization and frame handling issues (#747)
* [VideoOut/Vulkan] Fix boot deadlock, writeback stall, and presentation bugs

- Presenter thread now starts on the compute dispatch path too, fixing
  a boot deadlock when a title's first GPU work is compute (Ghost of
  Yotei G-Buffer clear).
- Guest render-target format swaps between sibling pixel formats now
  reinterpret in place instead of recreating blank, preserving
  GPU-written content.
- Vectorized the guest-buffer writeback scan (equal-byte skip + coarse
  per-page pre-check), fixing multi-second stalls on fragmented buffers
  that starved JobWorker completion signals.
- _presentedSequence now advances on every Render() early-return path,
  fixing an unthrottled busy loop on stale/dropped presentations.

* Remove unnecessary Silk.NET.Windowing dependency
2026-08-03 00:36:33 +03:00
999sian 8df4039ca4
Ampr: fix path case on Linux (#750) 2026-08-03 00:36:00 +03:00
Foued Attar f36ce4084a
[Memory/Kernel] Fix Windows allocation-granularity and mutex-resolution bugs (#748)
- Fixed guest mappings now go through a granule-aware allocator so
  adjacent PS5 16 KiB pages sharing a 64 KiB Windows allocation
  granule no longer collide and fail.
- TryBackFixedRange routes free/reserved gaps through the same
  granule-safe path, fixing strays that stranded the rest of a granule.
- NORMAL pthread mutex self-relock reverted to real EDEADLK instead of
  silent compatibility recursion, which was starving other threads.
- TryResolveMutexState now checks the handle-keyed lookup before
  falling through to "not found" on a fresh, never-cached address.
2026-08-03 00:15:42 +03:00
Arseny Yankovsky 4b5ea6a793
AGC: honour DCC fast clears instead of drawing the clear quad (#738)
On GFX10 a colour clear is not a packet. The driver programs
CB_COLORn_CLEAR_WORD0/1 and draws a covering quad which the colour block
turns into DCC clear codes, discarding whatever the pixel shader
exported. We executed that quad as an ordinary draw, so the shaded output
landed in the surface instead of a clear.

That alone would be a wrong-pixels bug, but the blend those quads use
makes it compound. Every draw into the target blends src=ONE,
dst=ONE_MINUS_SRC_ALPHA, so alpha follows a <- a_src + a_dst*(1 - a_src),
whose fixed point is 1. A guest colour attachment is cleared once on
first use and loaded on every pass after, so nothing ever resets it and
the channel climbs until it saturates. Where such a surface is a
compositing layer, the final image is ui.rgb + scene.rgb*(1 - ui.a) and a
saturated alpha multiplies the scene away entirely - the scene renders
correctly the whole time and is then masked to black.

Recognise the clear and perform it: the attachment is reset and the quad
is dropped, which reproduces the observable effect without modelling DCC
block state. The reset drops the image's Initialized flag so the next
render pass clears via AttachmentLoadOp.Clear, rather than enqueuing a
CmdClearColorImage - the latter lands outside the following render pass,
and a target cleared that way was still observed reading back its
previous contents.

Restricted to clear-to-zero: the reset clears to zero, so a nonzero
CLEAR_WORD would be cleared to the wrong colour and is left to be drawn.
Zero is zero under every encoding the register pair can carry, so the
test needs no format handling.

The clip-space span test is load-bearing rather than defensive. Fills
sharing the vertex count, topology and blend outnumber the clears by two
orders of magnitude and sit well outside the frame; treating those as
clears erases the UI and blanks video surfaces.
2026-08-02 16:39:25 +03:00
Astell cf3bd0b4f2
Astrobot - Vulkan fix : 0x8A (#736) 2026-08-02 13:28:41 +03:00
AlexC 5ee7cd1dfa
Icons in about section (#735) 2026-08-01 18:01:38 +03:00
Arseny Yankovsky ea9be7484f
AGC: correct GS program registers, stop dropping rect-list draws, add a missing size export (#734)
Three defects found while bringing up a PS5 title. None are title-specific.

SPI_SHADER_PGM_LO_GS / HI_GS were 0x8A/0x8B, which are actually
SPI_SHADER_PGM_RSRC1/RSRC2_GS. Reading them as an address produced a
58-bit value (observed live: 0x30004622C008300). The correct offsets are
0x88/0x89, consistent with SPI_SHADER_PGM_CHKSUM_GS = 0x80 and
SPI_SHADER_PGM_LO_ES = 0xC8 already in the table.

Draw translation dropped any RECT_LIST draw whose vertex program exported
no parameters while the pixel shader had interpolated inputs. A
disposition census over a real run measured this deleting ~620 of every
5000 draws (12%). Rect lists are what AMD drivers emit for clears, blits
and resolves, so the guard was deleting clears and leaving previous frame
contents on screen as trails. Rendering a draw whose interpolants are
undefined is strictly better than deleting it.

sceAgcDcbDrawIndexIndirectMultiGetSize was unimplemented while
sceAgcDcbDrawIndexIndirectMulti emits an eight-dword packet. A title that
sizes its command buffer from the missing export under-reserves by three
dwords. NID derived with Ps5Nid.Compute, which reproduces the committed
NIDs of the neighbouring exports.

Tests: AgcContextRegisterTests and AgcShaderStageRegisterTests drive real
PM4 packets through sceAgcDriverSubmitDcb and assert what the parser
retained for the context and SH register dictionaries, via two new
internal accessors; neither dictionary had any test surface, which is why
register questions previously cost five-minute game runs.
2026-08-01 17:30:02 +03:00
Astell a8fa9c96dc
Astrobot - Vulkan fix (#733)
* log and vulkan fix

* Astrobot - Vulkan fix : OpControlBarrier
2026-08-01 17:17:45 +03:00
Berk c387b969e1
[GUI] few stability patches for GUI (#732) 2026-08-01 15:05:13 +03:00
Berk 7c9740fee8
[GUI] Add grid settings to GUI and implement grid snapping (#729) 2026-07-31 13:27:43 +03:00
Emil 544f588cfd
Required imports for Astro Bot (#710)
* Required imports for Astro Bot

* Add missing clone to match ValueIndexCString
2026-07-31 12:31:14 +03:00
kuba ecd657006a
AGC: retain label producers and report failed release writes (#719)
* AGC: retain active label producer records

Ported from the archived silent-hill-minimal branch (274ccfdf).

RegisterLabelProducer bounded its history with RemoveRange(0, 1024) once it
reached 4096 entries, which evicts the oldest records regardless of whether
their label write has completed. Those records are not a diagnostic cache:
a suspended DCB resolves its wait_reg_mem by finding the producer that will
write the watched label. Dropping an active record hides an earlier
same-submission label write, so a legitimate in-stream fence suspends
forever and the graphics queue stops.

Compact only completed history, and let the list exceed the soft bound when
every record is still active — correctness over a diagnostic limit.

Two hardening changes over the ported version, because a suspended queue is
exactly when every record is active:
- compaction is a single order-preserving pass instead of repeated RemoveAt,
  which would shift the tail per eviction (quadratic) while the label gate is
  held;
- when a pass frees nothing the bound is raised to twice the current count, so
  registration does not rescan the whole list on every subsequent add, and it
  is reset once compaction can make progress again.

Tests: 664 pass, 0 fail (SharpEmu.Libs.Tests 598 -> 600).

* AGC: keep produced label values a suspended wait still needs

GpuWaitRegistry.RecordProduced dropped the entire produced-value table once it
reached 8192 entries:

    if (_lastProduced.Count >= 8192) { _lastProduced.Clear(); }

Those entries are release state, not a cache. CollectDeadlockBroken is the only
way out for a DCB suspended on a WAIT_REG_MEM whose label the guest has since
recycled for other data: it replays the value a real producer already wrote to
that label. Clearing the table wholesale erases exactly the records live waiters
depend on, so every such waiter is stranded permanently — the suspended graphics
queue never resumes, and with it the render thread and the whole engine thread
graph park.

That is what Silent Hill (PPSA10112) hits. Its final unsatisfied waits watch
labels holding guest allocator metadata (each reads a pointer into its own
region rather than the expected 1), i.e. recycled memory, which is precisely
the case the deadlock breaker exists to handle — yet agc.deadlock_break fires 0
times across a run with 490 wait suspensions and 1068 successful release_mem
writes, because the producing values had already been cleared.

Prune only values no registered waiter is watching, and let the table exceed the
soft bound while they are all watched. Same reasoning as the label-producer
history in the preceding commit: correctness of synchronization state takes
precedence over a diagnostic bound.

Tests: 666 pass, 0 fail. The regression test fails against the previous
wholesale Clear() and passes with the prune, verified by reverting the fix.

* AGC: report release-label writes that never reach guest memory

Both release_mem handlers recorded the produced label value only on success:

    if (wroteData && dataSelection is 1 or 2) GpuWaitRegistry.RecordProduced(...)

with no else. A failed write is not benign here. That packet is the producer a
suspended WAIT_REG_MEM is waiting for, so when it fails the label is neither
written nor recorded, CollectDeadlockBroken has no value to replay, and the
graphics queue stays suspended forever — the render thread and the engine
thread graph park behind it with nothing in the log to say why.

Measured on Silent Hill (PPSA10112): the fatal AccessViolation lands exactly
here, on the presenter thread, in ApplySubmittedReleaseMem's label write into a
protected guest page. The faulting variant kills the process; the
returns-false variant wedges the GPU silently. Only the first was ever visible.

Report the failure (rate-limited: first 16, then powers of two) instead of
dropping it. This does not fix the underlying write failure — it makes a
permanently suspended queue diagnosable rather than silent.

Tests: 666 pass, 0 fail.
2026-07-31 12:14:38 +03:00
Daniel Freak a7ec3d5a77
[GUI] Add inline per-game settings and unify options styling (#728)
* [GUI] Add inline per-game settings and unify options styling

* [GUI] Update options styling & add scrollable area

* [GUI] Remove focus and pointover styles for options
2026-07-31 12:14:21 +03:00
kuba 97bd8c422e
Combined: Pad, AGC, Metal, Vulkan, Ampr, overlay, and CPU hot-path fixes (#727)
* VideoOut: track guest-flip FPS and label load/stall gaps

Headline FPS follows VideoOut submit cadence rather than host presents, and long gaps show LOAD/STALL instead of a stale multi-second MS average.

* Metal: skip waitUntilCompleted when the command buffer is already Completed

Tiny write-back batches often finish before the wait; checking status avoids redundant ordered-queue round-trips.

* Metal: opt-in long-edge drawable cap via SHARPEMU_METAL_CAP_DRAWABLE

Default presentation resolution is unchanged; set SHARPEMU_METAL_CAP_DRAWABLE=1 to cap the drawable long edge at 1920.

* Pad: implement scePadGetTriggerEffectState under its own NID

NID znaWI0gpuo8 was mapped to sceUserServiceGetUserName as a "title-captured
alias". It is not that symbol. Recomputing the NID of every catalogued name
(base64 of the reversed first eight sha1 bytes of name+salt) resolves
znaWI0gpuo8 to scePadGetTriggerEffectState, and sceUserServiceGetUserName
hashes to 1xxcMiGu2fo instead. Auditing all 1087 export declarations the same
way found this to be the only NID whose declared name is wrong.

The consequence was not a missing export but a wrong one: the user-service
handler rejected the pad's arguments and returned
SCE_USER_SERVICE_ERROR_INVALID_PARAMETER about eighteen thousand times per
run in PPSA10112, so every poll fell back to a cached button bitmask. It
also hid the calls from every search for pad activity, which is why this
title was believed never to touch scePad at all.

The state size is taken from the caller's own frame rather than assumed: the
guest points the out-param at rbp-0x30 and stores its stack cookie at
rbp-0x28, leaving eight bytes for the state. Writing the sixteen the frame
superficially suggests would land on the cookie and fail the guest's stack
check - the same failure this codebase has already hit three times from
oversized HLE writes - so the test pins the size by asserting the cookie
survives.

No host pad exposes DualSense adaptive-trigger feedback, so the neutral
all-zero state is reported as success, which lets the caller take its normal
path instead of the fallback.

* Apply SDWA ABS/NEG as float sign-bit, not integer, modifiers

SDWA's ABS and NEG source modifiers are floating-point sign-bit
operations on GCN: ABS clears the sign bit, NEG flips it. We applied
them as integer operations instead - SAbs, and a two's-complement
negate of the raw bit pattern. That turns 1.0 into -4.0 and -3.0 into
1.5.

UE4 compiles the final line of DrawRectangle, OutPosition.xy *=
float2(1,-1), into a single V_MOV_B32 with SDWA NEG, so every UE
fullscreen pass had its clip-space Y silently skewed. The canonical
fullscreen triangle (1,-1) (-3,-1) (1,3) became (1,-4) (-3,-4) (1,1.5),
which covers 6/11 of the viewport instead of all of it.

That reproduces the measured defect exactly, on four independent
quantities: hypotenuse slope 8/11, crossings of y=+1 and y=-1 at
x=+7/11 and x=-9/11, and covered area 6/11 = 54.55% of the 2304x1296
viewport. It also explains why the edge was resolution-independent and
identical across six unrelated shaders - it is the same instruction in
every one of them.

The distinguishing evidence is the transform's fixed point. A wrong
scale would hold NDC -1 in x and +1 in y; the observed transform holds
the opposite corner in both. Independently, and using only the measured
line rather than any assumed vertex position: a wrong multiplier alone
leaves a residual of -18 whatever the multiplier, and a wrong addend
alone forces slope 1, not 8/11. Both terms had to be wrong at once,
which only a Y-only sign-bit corruption produces.

Float instructions are unaffected: GetFloatSource passes
applySdwaIntegerModifiers: false and applies its own modifiers, so this
path only ever fed raw-source reads - where the hardware behaviour is
the sign-bit one regardless of the opcode being a bit-move.

The sign bit is selected by the SDWA source-select width so a 16-bit
select flips bit 15 rather than bit 31.

Verified: emitted SPIR-V for the same shader changes from
OpISub %uint %uint_0 %2147 to OpBitwiseXor %uint %2147 %uint_2147483648;
25-program synthetic conformance gate passes; 805 tests green; three
100-110s live runs with no crashes and no new shader failures. The
end-to-end pixel re-measurement is NOT yet closed - see task #26.

* gpu: size single-channel 16-bit and two-channel 8-bit formats

GetFormatCompatibilityClass listed only R16Sfloat in the 16-bit class, so
GetVulkanImageByteCount computed zero bytes for R16Unorm, R16SNorm, R16Uint,
R16Sint and the R8G8 family. UploadGuestImageInitialData treats a zero
expected size as an incompatible upload and drops it, which leaves the
texture blank for the rest of the run rather than failing loudly.

Silent Hill uploads R16Unorm at 144x81, 240x135, 256x256, 512x512 and
1024x1024, and every one was rejected: the guest supplied exactly
width*height*2 bytes each time (23328 for 144x81) against an expected zero.

Also adds R8SNorm to the 8-bit class, which was missing for the same reason.

* Vulkan: enable textureCompressionBC when the device supports it

Guest BC1–BC7 textures can be sampled directly when the feature is available; warn when it is not.

* Share one guest image across sRGB/UNORM aliases

Ported from origin/fix/view-compatible-guest-images 7fb8fdf.

Rendering as sRGB and ImageLoad/Store-ing as UNORM at the same guest
address are the same surface accessed through different number formats.
Recreating the guest image per number format ping-pongs content between
two VkImages and loses the rendered pixels on every transition; the
mutable-format image now accepts the counterpart identity and serves it
through alias views. The commit names AvPlayer movie copies as the
pattern that needs this.

Adapted for this base: GetOrCreateGuestImage has since grown resolution
scaling and 3D/array support, so the alias accept is folded into the
current predicate (LogicalWidth/LogicalHeight/LogicalDepth/Type) rather
than the old Width/Height pair, and the storage-counterpart widening is
placed before the physical-dimension computation. The helper functions it
relies on (GetStorageImageFormat, IsCompatibleViewFormat) already existed.

* fix(gpu): one vertex attribute per guest stream view

The scalar evaluator gave every buffer_load_format instruction its own
attribute location. Two things multiply those: the CFG walk visits one
instruction on several paths, and an uber vertex shader fetches the same
stream from every material branch. UE's larger shaders reached 56 bindings
from 8 distinct views, and one reached 701 from 5.

Metal caps a vertex function at 31 attributes, so MoltenVK failed the MSL
compile with "'attribute' attribute parameter is out of bounds" and the
surrounding vkCreateGraphicsPipelines returned ErrorInitializationFailed.
Every draw using those pipelines was dropped, which is why Silent Hill:
The Short Message rendered a black scene. The vertex buffer count drove
Metal's buffer indices out of range too, giving the companion
"cannot reserve 'buffer' resource location at index 0" failures.

Key attributes by the guest stream view they read - absolute element
address, record stride and format - and alias every other fetch that
resolves to the same view onto that binding, so both translators map
those instruction PCs to one input variable. On PPSA10112 this takes the
worst shader from 56 attributes to 8 and pipeline failures from 840 to 0.

* AGC: deliver compute-queue completion events at the queue fence

sceAgcDriverSubmitAcb submissions never produced a completion interrupt.
NotifySubmittedDcbCompleted returned early for anything that was not the
graphics queue, so an ACB reaching its ordered-queue fence published
nothing. UE 4.27's dynamic-resolution GPU-timing heuristic parks the game
thread on exactly that interrupt, so the render side never advanced and
Silent Hill: The Short Message deadlocked after its first frame.

Give every queue a CompletionEventId: 0 for graphics (what it already
published) and the owner handle from sceAgcDriverSubmitAcb rdi for a
compute queue, which is the same value the guest passes to
sceAgcDriverAddEqEvent. Publish under that ident from the existing fence
point, which both PumpSubmittedQueue and ResumeSuspendedDcb already reach
only after the submission is fully parsed.

Delivery is synchronous on the ordered guest-action queue rather than on a
ThreadPool hop with a sleep. That action runs after the logical queue has
flushed and waited for its latest fence, which is the moment hardware
would raise end-of-pipe. Deferring past it can only make the interrupt
late and reorder it against registration changes.

Gating: the per-queue completion event is unconditional, because
completion interrupts do fire on real hardware and because delivery is
registration-gated -- TriggerRegisteredEvents only queues onto equeues
that registered this exact (ident, graphics filter) pair, and
sceAgcDriverAddEqEvent is the only producer of graphics registrations. A
title that never registers its ACB owner handle observes no change.
SHARPEMU_AGC_SUBMIT_COMPLETION_EVENT is left to gate only the broad
ident-ignoring fan-out (TriggerRegisteredEventsDistinct), which is a
compatibility guess rather than hardware behavior; it also stays scoped to
the graphics queue where it was measured, so enabling the flag does not
newly fan out across compute queues.

* fix(gpu): reflect guest CPU writes into large and render-target-aliased images

Ports the still-applicable half of the archived fork's stale-texture fix
(a42ccae) onto current main. Silent Hill: The Short Message (PPSA10112)
shows both faces: black title-screen UI (glyph atlas frozen at its first
upload) and stale/garbage rows on the brightness screen (a 3840x2160 UI
sheet whose backing bytes were never re-read).

1. Write tracking is armed under a byte budget, not a resolution cap.

   GetOrCreateGuestImage armed GuestImageWriteTracker only when
   target.Width <= 1920 && target.Height <= 1080. Silent Hill renders at
   3840x2160, so every one of its render targets was excluded from
   CPU-write tracking and no guest rewrite of one could ever invalidate
   it: SyncCpuWrittenGuestImages (the flip / ACQUIRE_MEM re-upload path)
   only ever visits ranges the tracker armed.

   The cap presumably existed as a perf guard, but resolution is the
   wrong proxy for the cost. Arming is one mprotect over the range, and
   the fault handler unprotects the whole range on the first store, so a
   write burst costs one fault regardless of size. What actually scales
   with the surface is the dirty re-upload: one byte[byteCount]
   allocation plus a full guest-memory read per dirty flip. So the guard
   is now a byte budget, set equal to the 128 MiB limit that
   SyncCpuWrittenGuestImages itself enforces before re-uploading. Above
   that, arming can only cost faults; it can never produce a re-upload.
   That is generous for 4K (RGBA8 32 MiB, RGBA16F 63 MiB, RGBA32F
   127 MiB all fit) while still excluding volume textures that a
   resolution cap could not see at all (512^3 RGBA8 is 512 MiB behind a
   "512x512" surface).

   Both sites now also arm the exact extent recorded in
   _guestImageExtents (GetTextureByteCount) instead of
   Width*Height*depth*GetTextureBytesPerPixel, so the armed range and
   the range the sync path reads back are the same bytes; the old
   expression over-counted block-compressed and unknown formats.

2. The CPU-texture refresh path no longer gates on IsCpuBacked alone.

   TryCreateCpuTextureRefreshResource bailed on !guestImage.IsCpuBacked.
   That flag is a latch: it flips false the first time an address is
   used as a render target and never flips back. A surface that was
   rendered into once and is afterwards rewritten by the guest CPU was
   therefore frozen at its last GPU content forever, even when the parse
   thread had already shipped fresh texels for it.

   The gate is replaced by ShouldRefreshGuestImageFromCpu: CPU-backed,
   or the parse-time write generation is above zero and differs from the
   generation recorded by the last upload. Keeping the positive-
   generation requirement preserves the pure GPU-feedback case (render
   into an image, then sample it) that IsCpuBacked used to protect: such
   a surface is now tracked (change 1) but never CPU-written, so its
   generation stays zero and its live image is left alone. This matters
   more than it did in the fork, precisely because change 1 arms
   tracking on far more render targets. Dropping the gate outright, as
   the fork did, would let a target sampled under a format tag that the
   availability map does not match be overwritten once with whatever
   sits in guest memory.

   The existing content fingerprint still suppresses redundant uploads,
   and MarkSampledImagesInitialized records the uploaded generation, so
   a rewritten surface re-uploads exactly once per guest write burst.

Not ported: the fork's third part added a PeekDirty guard to the
parse-time snapshot fast path in AgcExports. It is superseded. That path
now calls IsGuestImageUploadKnown, which already compares
_cpuBackedUploadGenerations against the tracker's write generation - a
monotonic value that survives another owner consuming the dirty flag,
unlike PeekDirty, which both EvictDirtyCachedTextures and
SyncCpuWrittenGuestImages clear. Guest images with no generation entry
are covered instead by SyncCpuWrittenGuestImages. Adding a non-consuming
PeekDirty there would also make every draw between a CPU write and the
next flip fall through to a full texel re-read of the surface (33 MiB
for a 4K sheet), since Track re-arms without clearing the dirty flag.
The fork's promotion-path Track ("vulkan.cpu-backed-image") is likewise
superseded: AgcExports already arms every sampled texture's backing
extent as "agc.decoded-texture" before reading its texels.

Both decisions are extracted as pure internal predicates so they can be
unit-tested; the Vulkan device code around them needs a real device.

* Kernel: correct equeue event delivery and waiter lifetime

Ports the event-queue rework from the archived silent-hill-minimal branch
(968e9606, 9e3abb12, 2be9cfbf, f157a115) onto current upstream. The equeue
implementation had not been touched upstream since that branch forked, so
none of it had landed.

Behaviour fixed:

- Per-waiter event reservation. A blocked waiter used to wake on "the queue
  has any pending event" (TryWake => HasPendingEvents) and then re-read the
  queue on resume, so a woken waiter could find the event already drained by
  another waiter and park again with the wake consumed. Events are now
  reserved to the waiter they are delivered to.
- Level-triggered events are preserved instead of being cleared by an
  unrelated read; only events that declare clear-on-read reset their trigger
  state.
- Queued interrupts are bound to the registration generation that produced
  them, so an event registered after a queue was reused cannot consume an
  interrupt raised for the previous registration.
- Deleting an equeue now terminates its waiters instead of leaving them
  blocked on a handle that no longer resolves.
- sceKernelTriggerUserEvent stores its third argument in the event's udata
  (0x18) rather than its data word (0x10). The guest reads it back with
  sceKernelGetEventUserData, which loads 0x18, so every triggered user event
  previously read back as 0. This matches the reference behaviour in shadPS4,
  where TriggerEvent takes udata and sceKernelGetEventUserData returns
  ev->udata. The upstream test asserting the data word is updated, since it
  encoded the inconsistency rather than the ABI.

KernelPthreadState gains TryGetCurrentThreadIdentity and a new
KernelSyncTraceFormatter carries the shared, opt-in diagnostic formatting the
ported code calls; both are gated behind the existing trace flag and do no
work when it is off.

Tests: 662 pass, 0 fail (SharpEmu.Libs.Tests 588 -> 598).

* Implement VOP3 0x149 V_BFE_I32 and 0x36A V_CVT_PKRTZ_F16_F32

PPSA10112 dropped three shaders per run on two unimplemented VOP3
opcodes. The translator fails a whole shader on an opcode it does not
know, so each one costs a dropped draw rather than wrong pixels.

0x149 sits between 0x148 V_BFE_U32 and 0x14A V_BFI_B32, and 0x36A
between 0x369 V_CVT_PKNORM_U16_F32 and 0x36D V_ADD3_U32; the
surrounding table entries already match the canonical map densely on
both sides of each gap. V_CVT_PKRTZ_F16_F32 needed no emitter - it was
already implemented for the VOP2 form at 0x2F and its VOP3 alias at
0x12F, and only the VOP3-only opcode number was missing.

V_BFE_I32 mirrors its unsigned sibling, masking offset and width to 5
bits, and differs only in extracting through a signed type so the field
sign-extends. GCN defines width 0 as returning 0 where SPIR-V leaves a
zero Count unspecified; VBfeU32 has the same gap, so this matches it
deliberately rather than diverging - fix both together if it matters.

Also widen ReportGuestPointerSplit, which filtered to an incoming value
of exactly 1. The crash being hunted leaves 0x0000007000000000, whose
low dword is 0, so the one detector built to catch this bug class could
never have reported it.

Verified: unsupported-opcode errors 3-4 per run -> 0, over a 130s run
that survives to the same stage. Metal translator still lacks VBfeI32;
Vulkan/MoltenVK is the macOS path so it is a divergence, not a blocker.

* AGC: retain active label producer records

Ported from the archived silent-hill-minimal branch (274ccfdf).

RegisterLabelProducer bounded its history with RemoveRange(0, 1024) once it
reached 4096 entries, which evicts the oldest records regardless of whether
their label write has completed. Those records are not a diagnostic cache:
a suspended DCB resolves its wait_reg_mem by finding the producer that will
write the watched label. Dropping an active record hides an earlier
same-submission label write, so a legitimate in-stream fence suspends
forever and the graphics queue stops.

Compact only completed history, and let the list exceed the soft bound when
every record is still active — correctness over a diagnostic limit.

Two hardening changes over the ported version, because a suspended queue is
exactly when every record is active:
- compaction is a single order-preserving pass instead of repeated RemoveAt,
  which would shift the tail per eviction (quadratic) while the label gate is
  held;
- when a pass frees nothing the bound is raised to twice the current count, so
  registration does not rescan the whole list on every subsequent add, and it
  is reset once compaction can make progress again.

Tests: 664 pass, 0 fail (SharpEmu.Libs.Tests 598 -> 600).

* AGC: keep produced label values a suspended wait still needs

GpuWaitRegistry.RecordProduced dropped the entire produced-value table once it
reached 8192 entries:

    if (_lastProduced.Count >= 8192) { _lastProduced.Clear(); }

Those entries are release state, not a cache. CollectDeadlockBroken is the only
way out for a DCB suspended on a WAIT_REG_MEM whose label the guest has since
recycled for other data: it replays the value a real producer already wrote to
that label. Clearing the table wholesale erases exactly the records live waiters
depend on, so every such waiter is stranded permanently — the suspended graphics
queue never resumes, and with it the render thread and the whole engine thread
graph park.

That is what Silent Hill (PPSA10112) hits. Its final unsatisfied waits watch
labels holding guest allocator metadata (each reads a pointer into its own
region rather than the expected 1), i.e. recycled memory, which is precisely
the case the deadlock breaker exists to handle — yet agc.deadlock_break fires 0
times across a run with 490 wait suspensions and 1068 successful release_mem
writes, because the producing values had already been cleared.

Prune only values no registered waiter is watching, and let the table exceed the
soft bound while they are all watched. Same reasoning as the label-producer
history in the preceding commit: correctness of synchronization state takes
precedence over a diagnostic bound.

Tests: 666 pass, 0 fail. The regression test fails against the previous
wholesale Clear() and passes with the prune, verified by reverting the fix.

* Correct VOP3 0x36A: V_CVT_PK_U16_U32, not V_CVT_PKRTZ_F16_F32

a2d186e mapped 0x36A to V_CVT_PKRTZ_F16_F32 on the strength of a gap in
our own opcode table. That was wrong, and wrong in the worst available
way: an emitter for V_CVT_PKRTZ_F16_F32 already existed, so instead of
failing loudly like an unknown opcode, the mapping would have emitted
float-pack semantics for an integer-pack instruction and produced
silently incorrect results.

LLVM is unambiguous:
    defm V_CVT_PK_U16_U32    : VOP3Only_Real_gfx10<0x36a>;
    defm V_CVT_PKRTZ_F16_F32 : VOP2_Real_gfx6_gfx7_gfx10<0x02f>;
so on Gen5 the float pack is VOP2 0x2F with VOP3 alias 0x12F - both of
which our table already had - and there is no VOP3-only encoding of it
to add. 0x36A sits with the other integer/normalised pack conversions
at 0x368/0x369/0x36B.

silent-hill-minimal-rebased had this right all along.

The V_BFE_I32 half of a2d186e stands: LLVM confirms 0x149, and that
branch maps it identically.

Lesson, since I had just warned someone else about exactly this: a gap
in a table is evidence about numbering, not about identity. Inference
from neighbouring entries is fine for narrowing candidates and worth
nothing as a conclusion - especially when a plausible emitter already
exists to swallow the mistake quietly.

* AGC: report release-label writes that never reach guest memory

Both release_mem handlers recorded the produced label value only on success:

    if (wroteData && dataSelection is 1 or 2) GpuWaitRegistry.RecordProduced(...)

with no else. A failed write is not benign here. That packet is the producer a
suspended WAIT_REG_MEM is waiting for, so when it fails the label is neither
written nor recorded, CollectDeadlockBroken has no value to replay, and the
graphics queue stays suspended forever — the render thread and the engine
thread graph park behind it with nothing in the log to say why.

Measured on Silent Hill (PPSA10112): the fatal AccessViolation lands exactly
here, on the presenter thread, in ApplySubmittedReleaseMem's label write into a
protected guest page. The faulting variant kills the process; the
returns-false variant wedges the GPU silently. Only the first was ever visible.

Report the failure (rate-limited: first 16, then powers of two) instead of
dropping it. This does not fix the underlying write failure — it makes a
permanently suspended queue diagnosable rather than silent.

Tests: 666 pass, 0 fail.

* CPU: gate periodic import logs and fast-path memcpy/memmove leaves

SHARPEMU_LOG_IMPORT_PERIODIC=1 re-enables periodic Import# tracing. memcpy/memmove use a thin TryCopy leaf that skips register marshalling.

* Ampr: cooked-id index preload and host FD LRU cache

Preload the app0 APR index during bind and bound the open host-file cache so streaming reads stay responsive under large title archives.

* Vulkan: abandon timed-out guest fences without blocking later frames

Move fence-timeout submissions out of the blocking queue, keep GPU objects alive until the fence signals, and retire them from an abandoned list so one hung dispatch cannot re-block capacity waits.

* AGC: follow command-buffer branches across arena switches

Implement sceAgcCbBranch and walk INDIRECT_BUFFER so submissions that continue in a linked buffer keep their flip and end-of-frame labels.
2026-07-31 12:14:02 +03:00
kuba c4ae4a2059
Vulkan: abandon timed-out guest fences without blocking later frames (#726)
Move fence-timeout submissions out of the blocking queue, keep GPU objects alive until the fence signals, and retire them from an abandoned list so one hung dispatch cannot re-block capacity waits.
2026-07-31 12:13:42 +03:00
kuba 93c9f14081
Ampr: cooked-id index preload and host FD LRU cache (#725)
Preload the app0 APR index during bind and bound the open host-file cache so streaming reads stay responsive under large title archives.
2026-07-31 12:13:33 +03:00
kuba 532251c0c3
CPU: gate periodic import logs and fast-path memcpy/memmove leaves (#724)
SHARPEMU_LOG_IMPORT_PERIODIC=1 re-enables periodic Import# tracing. memcpy/memmove use a thin TryCopy leaf that skips register marshalling.
2026-07-31 12:13:24 +03:00
kuba 816ec4ad27
Kernel: correct equeue event delivery and waiter lifetime (#723)
Ports the event-queue rework from the archived silent-hill-minimal branch
(968e9606, 9e3abb12, 2be9cfbf, f157a115) onto current upstream. The equeue
implementation had not been touched upstream since that branch forked, so
none of it had landed.

Behaviour fixed:

- Per-waiter event reservation. A blocked waiter used to wake on "the queue
  has any pending event" (TryWake => HasPendingEvents) and then re-read the
  queue on resume, so a woken waiter could find the event already drained by
  another waiter and park again with the wake consumed. Events are now
  reserved to the waiter they are delivered to.
- Level-triggered events are preserved instead of being cleared by an
  unrelated read; only events that declare clear-on-read reset their trigger
  state.
- Queued interrupts are bound to the registration generation that produced
  them, so an event registered after a queue was reused cannot consume an
  interrupt raised for the previous registration.
- Deleting an equeue now terminates its waiters instead of leaving them
  blocked on a handle that no longer resolves.
- sceKernelTriggerUserEvent stores its third argument in the event's udata
  (0x18) rather than its data word (0x10). The guest reads it back with
  sceKernelGetEventUserData, which loads 0x18, so every triggered user event
  previously read back as 0. This matches the reference behaviour in shadPS4,
  where TriggerEvent takes udata and sceKernelGetEventUserData returns
  ev->udata. The upstream test asserting the data word is updated, since it
  encoded the inconsistency rather than the ABI.

KernelPthreadState gains TryGetCurrentThreadIdentity and a new
KernelSyncTraceFormatter carries the shared, opt-in diagnostic formatting the
ported code calls; both are gated behind the existing trace flag and do no
work when it is off.

Tests: 662 pass, 0 fail (SharpEmu.Libs.Tests 588 -> 598).
2026-07-31 12:13:15 +03:00
kuba 82c2c7f48c
fix(gpu): reflect guest CPU writes into large and render-target-aliased images (#722)
Ports the still-applicable half of the archived fork's stale-texture fix
(a42ccae) onto current main. Silent Hill: The Short Message (PPSA10112)
shows both faces: black title-screen UI (glyph atlas frozen at its first
upload) and stale/garbage rows on the brightness screen (a 3840x2160 UI
sheet whose backing bytes were never re-read).

1. Write tracking is armed under a byte budget, not a resolution cap.

   GetOrCreateGuestImage armed GuestImageWriteTracker only when
   target.Width <= 1920 && target.Height <= 1080. Silent Hill renders at
   3840x2160, so every one of its render targets was excluded from
   CPU-write tracking and no guest rewrite of one could ever invalidate
   it: SyncCpuWrittenGuestImages (the flip / ACQUIRE_MEM re-upload path)
   only ever visits ranges the tracker armed.

   The cap presumably existed as a perf guard, but resolution is the
   wrong proxy for the cost. Arming is one mprotect over the range, and
   the fault handler unprotects the whole range on the first store, so a
   write burst costs one fault regardless of size. What actually scales
   with the surface is the dirty re-upload: one byte[byteCount]
   allocation plus a full guest-memory read per dirty flip. So the guard
   is now a byte budget, set equal to the 128 MiB limit that
   SyncCpuWrittenGuestImages itself enforces before re-uploading. Above
   that, arming can only cost faults; it can never produce a re-upload.
   That is generous for 4K (RGBA8 32 MiB, RGBA16F 63 MiB, RGBA32F
   127 MiB all fit) while still excluding volume textures that a
   resolution cap could not see at all (512^3 RGBA8 is 512 MiB behind a
   "512x512" surface).

   Both sites now also arm the exact extent recorded in
   _guestImageExtents (GetTextureByteCount) instead of
   Width*Height*depth*GetTextureBytesPerPixel, so the armed range and
   the range the sync path reads back are the same bytes; the old
   expression over-counted block-compressed and unknown formats.

2. The CPU-texture refresh path no longer gates on IsCpuBacked alone.

   TryCreateCpuTextureRefreshResource bailed on !guestImage.IsCpuBacked.
   That flag is a latch: it flips false the first time an address is
   used as a render target and never flips back. A surface that was
   rendered into once and is afterwards rewritten by the guest CPU was
   therefore frozen at its last GPU content forever, even when the parse
   thread had already shipped fresh texels for it.

   The gate is replaced by ShouldRefreshGuestImageFromCpu: CPU-backed,
   or the parse-time write generation is above zero and differs from the
   generation recorded by the last upload. Keeping the positive-
   generation requirement preserves the pure GPU-feedback case (render
   into an image, then sample it) that IsCpuBacked used to protect: such
   a surface is now tracked (change 1) but never CPU-written, so its
   generation stays zero and its live image is left alone. This matters
   more than it did in the fork, precisely because change 1 arms
   tracking on far more render targets. Dropping the gate outright, as
   the fork did, would let a target sampled under a format tag that the
   availability map does not match be overwritten once with whatever
   sits in guest memory.

   The existing content fingerprint still suppresses redundant uploads,
   and MarkSampledImagesInitialized records the uploaded generation, so
   a rewritten surface re-uploads exactly once per guest write burst.

Not ported: the fork's third part added a PeekDirty guard to the
parse-time snapshot fast path in AgcExports. It is superseded. That path
now calls IsGuestImageUploadKnown, which already compares
_cpuBackedUploadGenerations against the tracker's write generation - a
monotonic value that survives another owner consuming the dirty flag,
unlike PeekDirty, which both EvictDirtyCachedTextures and
SyncCpuWrittenGuestImages clear. Guest images with no generation entry
are covered instead by SyncCpuWrittenGuestImages. Adding a non-consuming
PeekDirty there would also make every draw between a CPU write and the
next flip fall through to a full texel re-read of the surface (33 MiB
for a 4K sheet), since Track re-arms without clearing the dirty flag.
The fork's promotion-path Track ("vulkan.cpu-backed-image") is likewise
superseded: AgcExports already arms every sampled texture's backing
extent as "agc.decoded-texture" before reading its texels.

Both decisions are extracted as pure internal predicates so they can be
unit-tested; the Vulkan device code around them needs a real device.
2026-07-31 12:12:54 +03:00
kuba 531e35b6d5
AGC: deliver compute-queue completion events at the queue fence (#721)
sceAgcDriverSubmitAcb submissions never produced a completion interrupt.
NotifySubmittedDcbCompleted returned early for anything that was not the
graphics queue, so an ACB reaching its ordered-queue fence published
nothing. UE 4.27's dynamic-resolution GPU-timing heuristic parks the game
thread on exactly that interrupt, so the render side never advanced and
Silent Hill: The Short Message deadlocked after its first frame.

Give every queue a CompletionEventId: 0 for graphics (what it already
published) and the owner handle from sceAgcDriverSubmitAcb rdi for a
compute queue, which is the same value the guest passes to
sceAgcDriverAddEqEvent. Publish under that ident from the existing fence
point, which both PumpSubmittedQueue and ResumeSuspendedDcb already reach
only after the submission is fully parsed.

Delivery is synchronous on the ordered guest-action queue rather than on a
ThreadPool hop with a sleep. That action runs after the logical queue has
flushed and waited for its latest fence, which is the moment hardware
would raise end-of-pipe. Deferring past it can only make the interrupt
late and reorder it against registration changes.

Gating: the per-queue completion event is unconditional, because
completion interrupts do fire on real hardware and because delivery is
registration-gated -- TriggerRegisteredEvents only queues onto equeues
that registered this exact (ident, graphics filter) pair, and
sceAgcDriverAddEqEvent is the only producer of graphics registrations. A
title that never registers its ACB owner handle observes no change.
SHARPEMU_AGC_SUBMIT_COMPLETION_EVENT is left to gate only the broad
ident-ignoring fan-out (TriggerRegisteredEventsDistinct), which is a
compatibility guess rather than hardware behavior; it also stays scoped to
the graphics queue where it was measured, so enabling the flag does not
newly fan out across compute queues.
2026-07-31 12:12:43 +03:00
kuba 3f9bd2b92b
AGC: follow command-buffer branches across arena switches (#720)
Implement sceAgcCbBranch and walk INDIRECT_BUFFER so submissions that continue in a linked buffer keep their flip and end-of-frame labels.
2026-07-31 12:12:35 +03:00
kuba eb0653eded
fix(gpu): one vertex attribute per guest stream view (#718)
The scalar evaluator gave every buffer_load_format instruction its own
attribute location. Two things multiply those: the CFG walk visits one
instruction on several paths, and an uber vertex shader fetches the same
stream from every material branch. UE's larger shaders reached 56 bindings
from 8 distinct views, and one reached 701 from 5.

Metal caps a vertex function at 31 attributes, so MoltenVK failed the MSL
compile with "'attribute' attribute parameter is out of bounds" and the
surrounding vkCreateGraphicsPipelines returned ErrorInitializationFailed.
Every draw using those pipelines was dropped, which is why Silent Hill:
The Short Message rendered a black scene. The vertex buffer count drove
Metal's buffer indices out of range too, giving the companion
"cannot reserve 'buffer' resource location at index 0" failures.

Key attributes by the guest stream view they read - absolute element
address, record stride and format - and alias every other fetch that
resolves to the same view onto that binding, so both translators map
those instruction PCs to one input variable. On PPSA10112 this takes the
worst shader from 56 attributes to 8 and pipeline failures from 840 to 0.
2026-07-31 12:12:26 +03:00
kuba e1695cf87f
Share one guest image across sRGB/UNORM aliases (#717)
Ported from origin/fix/view-compatible-guest-images 7fb8fdf.

Rendering as sRGB and ImageLoad/Store-ing as UNORM at the same guest
address are the same surface accessed through different number formats.
Recreating the guest image per number format ping-pongs content between
two VkImages and loses the rendered pixels on every transition; the
mutable-format image now accepts the counterpart identity and serves it
through alias views. The commit names AvPlayer movie copies as the
pattern that needs this.

Adapted for this base: GetOrCreateGuestImage has since grown resolution
scaling and 3D/array support, so the alias accept is folded into the
current predicate (LogicalWidth/LogicalHeight/LogicalDepth/Type) rather
than the old Width/Height pair, and the storage-counterpart widening is
placed before the physical-dimension computation. The helper functions it
relies on (GetStorageImageFormat, IsCompatibleViewFormat) already existed.
2026-07-31 12:12:09 +03:00
kuba 0dd543354d
Vulkan: enable textureCompressionBC when the device supports it (#716)
Guest BC1–BC7 textures can be sampled directly when the feature is available; warn when it is not.
2026-07-31 12:11:53 +03:00
kuba fc5b6baaa7
gpu: size single-channel 16-bit and two-channel 8-bit formats (#715)
GetFormatCompatibilityClass listed only R16Sfloat in the 16-bit class, so
GetVulkanImageByteCount computed zero bytes for R16Unorm, R16SNorm, R16Uint,
R16Sint and the R8G8 family. UploadGuestImageInitialData treats a zero
expected size as an incompatible upload and drops it, which leaves the
texture blank for the rest of the run rather than failing loudly.

Silent Hill uploads R16Unorm at 144x81, 240x135, 256x256, 512x512 and
1024x1024, and every one was rejected: the guest supplied exactly
width*height*2 bytes each time (23328 for 144x81) against an expected zero.

Also adds R8SNorm to the 8-bit class, which was missing for the same reason.
2026-07-31 12:11:44 +03:00
kuba e5e02c0908
Shader: implement V_BFE_I32 and correct VOP3 0x36A mapping (#714)
* Implement VOP3 0x149 V_BFE_I32 and 0x36A V_CVT_PKRTZ_F16_F32

PPSA10112 dropped three shaders per run on two unimplemented VOP3
opcodes. The translator fails a whole shader on an opcode it does not
know, so each one costs a dropped draw rather than wrong pixels.

0x149 sits between 0x148 V_BFE_U32 and 0x14A V_BFI_B32, and 0x36A
between 0x369 V_CVT_PKNORM_U16_F32 and 0x36D V_ADD3_U32; the
surrounding table entries already match the canonical map densely on
both sides of each gap. V_CVT_PKRTZ_F16_F32 needed no emitter - it was
already implemented for the VOP2 form at 0x2F and its VOP3 alias at
0x12F, and only the VOP3-only opcode number was missing.

V_BFE_I32 mirrors its unsigned sibling, masking offset and width to 5
bits, and differs only in extracting through a signed type so the field
sign-extends. GCN defines width 0 as returning 0 where SPIR-V leaves a
zero Count unspecified; VBfeU32 has the same gap, so this matches it
deliberately rather than diverging - fix both together if it matters.

Also widen ReportGuestPointerSplit, which filtered to an incoming value
of exactly 1. The crash being hunted leaves 0x0000007000000000, whose
low dword is 0, so the one detector built to catch this bug class could
never have reported it.

Verified: unsupported-opcode errors 3-4 per run -> 0, over a 130s run
that survives to the same stage. Metal translator still lacks VBfeI32;
Vulkan/MoltenVK is the macOS path so it is a divergence, not a blocker.

* Correct VOP3 0x36A: V_CVT_PK_U16_U32, not V_CVT_PKRTZ_F16_F32

a2d186e mapped 0x36A to V_CVT_PKRTZ_F16_F32 on the strength of a gap in
our own opcode table. That was wrong, and wrong in the worst available
way: an emitter for V_CVT_PKRTZ_F16_F32 already existed, so instead of
failing loudly like an unknown opcode, the mapping would have emitted
float-pack semantics for an integer-pack instruction and produced
silently incorrect results.

LLVM is unambiguous:
    defm V_CVT_PK_U16_U32    : VOP3Only_Real_gfx10<0x36a>;
    defm V_CVT_PKRTZ_F16_F32 : VOP2_Real_gfx6_gfx7_gfx10<0x02f>;
so on Gen5 the float pack is VOP2 0x2F with VOP3 alias 0x12F - both of
which our table already had - and there is no VOP3-only encoding of it
to add. 0x36A sits with the other integer/normalised pack conversions
at 0x368/0x369/0x36B.

silent-hill-minimal-rebased had this right all along.

The V_BFE_I32 half of a2d186e stands: LLVM confirms 0x149, and that
branch maps it identically.

Lesson, since I had just warned someone else about exactly this: a gap
in a table is evidence about numbering, not about identity. Inference
from neighbouring entries is fine for narrowing candidates and worth
nothing as a conclusion - especially when a plausible emitter already
exists to swallow the mistake quietly.
2026-07-31 12:09:50 +03:00
kuba 539baa66e7
Apply SDWA ABS/NEG as float sign-bit, not integer, modifiers (#713)
SDWA's ABS and NEG source modifiers are floating-point sign-bit
operations on GCN: ABS clears the sign bit, NEG flips it. We applied
them as integer operations instead - SAbs, and a two's-complement
negate of the raw bit pattern. That turns 1.0 into -4.0 and -3.0 into
1.5.

UE4 compiles the final line of DrawRectangle, OutPosition.xy *=
float2(1,-1), into a single V_MOV_B32 with SDWA NEG, so every UE
fullscreen pass had its clip-space Y silently skewed. The canonical
fullscreen triangle (1,-1) (-3,-1) (1,3) became (1,-4) (-3,-4) (1,1.5),
which covers 6/11 of the viewport instead of all of it.

That reproduces the measured defect exactly, on four independent
quantities: hypotenuse slope 8/11, crossings of y=+1 and y=-1 at
x=+7/11 and x=-9/11, and covered area 6/11 = 54.55% of the 2304x1296
viewport. It also explains why the edge was resolution-independent and
identical across six unrelated shaders - it is the same instruction in
every one of them.

The distinguishing evidence is the transform's fixed point. A wrong
scale would hold NDC -1 in x and +1 in y; the observed transform holds
the opposite corner in both. Independently, and using only the measured
line rather than any assumed vertex position: a wrong multiplier alone
leaves a residual of -18 whatever the multiplier, and a wrong addend
alone forces slope 1, not 8/11. Both terms had to be wrong at once,
which only a Y-only sign-bit corruption produces.

Float instructions are unaffected: GetFloatSource passes
applySdwaIntegerModifiers: false and applies its own modifiers, so this
path only ever fed raw-source reads - where the hardware behaviour is
the sign-bit one regardless of the opcode being a bit-move.

The sign bit is selected by the SDWA source-select width so a 16-bit
select flips bit 15 rather than bit 31.

Verified: emitted SPIR-V for the same shader changes from
OpISub %uint %uint_0 %2147 to OpBitwiseXor %uint %2147 %uint_2147483648;
25-program synthetic conformance gate passes; 805 tests green; three
100-110s live runs with no crashes and no new shader failures. The
end-to-end pixel re-measurement is NOT yet closed - see task #26.
2026-07-31 12:09:41 +03:00
kuba b572738547
Pad: implement scePadGetTriggerEffectState under its own NID (#712)
NID znaWI0gpuo8 was mapped to sceUserServiceGetUserName as a "title-captured
alias". It is not that symbol. Recomputing the NID of every catalogued name
(base64 of the reversed first eight sha1 bytes of name+salt) resolves
znaWI0gpuo8 to scePadGetTriggerEffectState, and sceUserServiceGetUserName
hashes to 1xxcMiGu2fo instead. Auditing all 1087 export declarations the same
way found this to be the only NID whose declared name is wrong.

The consequence was not a missing export but a wrong one: the user-service
handler rejected the pad's arguments and returned
SCE_USER_SERVICE_ERROR_INVALID_PARAMETER about eighteen thousand times per
run in PPSA10112, so every poll fell back to a cached button bitmask. It
also hid the calls from every search for pad activity, which is why this
title was believed never to touch scePad at all.

The state size is taken from the caller's own frame rather than assumed: the
guest points the out-param at rbp-0x30 and stores its stack cookie at
rbp-0x28, leaving eight bytes for the state. Writing the sixteen the frame
superficially suggests would land on the cookie and fail the guest's stack
check - the same failure this codebase has already hit three times from
oversized HLE writes - so the test pins the size by asserting the cookie
survives.

No host pad exposes DualSense adaptive-trigger feedback, so the neutral
all-zero state is reported as success, which lets the caller take its normal
path instead of the fallback.
2026-07-31 12:09:24 +03:00
kuba b75e4e01a0
Metal: opt-in long-edge drawable cap via SHARPEMU_METAL_CAP_DRAWABLE (#711)
Default presentation resolution is unchanged; set SHARPEMU_METAL_CAP_DRAWABLE=1 to cap the drawable long edge at 1920.
2026-07-31 12:09:09 +03:00
kuba 79aa764d03
Metal: skip waitUntilCompleted when the command buffer is already Completed (#709)
Tiny write-back batches often finish before the wait; checking status avoids redundant ordered-queue round-trips.
2026-07-31 12:08:56 +03:00
kuba ec65419c0a
VideoOut: track guest-flip FPS and label load/stall gaps (#708)
Headline FPS follows VideoOut submit cadence rather than host presents, and long gaps show LOAD/STALL instead of a stale multi-second MS average.
2026-07-31 12:08:46 +03:00
ParantezTech c990b7799f Merge branch 'main' of https://github.com/sharpemu/sharpemu 2026-07-31 01:47:13 +03:00
Berk e7149bf41f Shader cfg ssa (#707)
* [shader] add scalar control flow graph
* [shader] add gen5 branch resolver
* [shader] add scalar reaching definitions
* [shader] add cfg and dataflow tests
* [shader] guard descriptors from unresolved registers
2026-07-31 01:43:28 +03:00
Berk 444af50f4b
Shader cfg ssa (#707)
* [shader] add scalar control flow graph

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

* [shader] add scalar control flow graph

* [shader] add gen5 branch resolver

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

* [shader] add gen5 branch resolver

* [shader] add scalar reaching definitions

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

* [shader] add scalar reaching definitions

* [shader] add cfg and dataflow tests

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

* [shader] add cfg and dataflow tests

* [shader] guard descriptors from unresolved registers

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

* [shader] guard descriptors from unresolved registers

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 01:39:56 +03:00
Berk 5864328e35
Merge media decoding into one FFmpeg bridge (#706)
* [media] merge bink into shared ffmpeg bridge

* [avplayer] decode in process and fix stream info size

* [font] add glyph and teardown exports

* [build] bump ffmpeg runtime to 3b502d4

* [font] add glyph and teardown exports

* [build] bump ffmpeg runtime to 3b502d4

* [avplayer] decode in process

* [font] add glyph and teardown exports

* [build] bump ffmpeg runtime to 3b502d4
2026-07-31 01:31:14 +03:00
Daniel Freak 753ddf93be
[GUI] Update window chrome controls (#700) 2026-07-30 17:03:57 +03:00
Astell 5f1bd5a77c
fix vulkan error from astrobot (#689)
* fix vulkan error from astrobot

* a bit of a clean

* a bit of a clean
2026-07-30 12:58:43 +03:00
Daniel Freak f08308daf2
[GUI] Redesign options page (#699) 2026-07-30 12:58:22 +03:00
Daniel Freak 77f22973cb
[GUI] Update library page layout (#690)
* [GUI] Add horizontal game tiles

* [GUI] Update launching elements layout

* [GUI] Change cards format to squares
2026-07-30 02:47:15 +03:00
Berk 996de70f52
Game compat updates (#691)
* [agc] Add indirect draws

* [video] Match render target views

* [savedata] Allow dialog reinit

* [ime] Add default profile

* [GUI] update missing translations for profile box
2026-07-29 15:59:57 +03:00
Berk d5108e854d
chore: bump version to 0.0.3-hotfix-2 (#687) 2026-07-29 01:50:26 +03:00
ParantezTech b020f1676a Merge branch 'main' of https://github.com/sharpemu/sharpemu 2026-07-29 01:48:28 +03:00
ParantezTech 02938b5d5b [GUI] removed not official Discord link from GUI 2026-07-29 01:47:35 +03:00
Berk 7b7a48a834
[CI] Commits are now automatically added to the release notes (#686) 2026-07-29 01:47:21 +03:00
Daniel Freak faf49f689d
[GUI] Add live localization bindings and fill missing locales (#685) 2026-07-29 01:25:48 +03:00
Daniel Freak 882a6c06e0
[GUI] add custom top-bar across al platforms (#682) 2026-07-28 21:24:43 +03:00
ParantezTech b21dd9fb92 [GUI] reworked languages for missing translations and added new ones 2026-07-28 17:38:53 +03:00
Daniel Freak b07e4f2bc6
[GUI] add game library watcher & remove rescan button (#678) 2026-07-28 17:09:22 +03:00
Berk a3130e30ff
chore: bump version to 0.0.3-hotfix-1 (#680) 2026-07-28 16:32:27 +03:00
Berk f095ed68a8
Fix ajm (#679)
* [audio] fixed demons souls audioout2 output path

* [video] restored texture cache bound and gated present diagnostics

* [cpu] made guest image cpu sync opt-in
2026-07-28 16:29:58 +03:00
ParantezTech ddcd285075 [reuse] correct license for AT9 decoder 2026-07-28 12:25:01 +03:00
Daniel Freak 6994538d87
[GUI] split shared theme into resource dictionaries (#669) 2026-07-28 12:16:43 +03:00
Berk 92e3abe752
chore: bump version to 0.0.3 (#671) 2026-07-28 03:38:49 +03:00
Berk 2b6bd5a532
Sdl backend (#670)
* [audio] added sdl audio backend and in-tree atrac9 decoder

* [input] replaced per-platform pad readers with sdl gamepad input

* [video] added sdl window and host display plumbing

* [gui] added host display options and per-game render settings

* [bink] synced host movie playback to the guest audio clock

* [cpu] hooked windows write faults into guest image tracking

* [perf] added guest and render profiling, reserved host cpu lanes

* [kernel] fixed stale pthread mutex handle alias

* [host] wired the sdl session, save-data paths and project references

* [audio] hoisted ajm trace stackalloc out of its loop

* [video] Add guest image sync setting

* [build] Strip native symbols

* reuse
2026-07-28 03:33:26 +03:00
Daniel Freak b4cc5f88ca
[GUI] Upgrade Avalonia to 12.1.0 and enable compiled bindings (#666)
* [GUI] bump to avalonia 12

* [GUI] add compiled bindings for cards/console logs/game metas

* [GUI] enable compiled bindings across launcher XAML
2026-07-27 23:00:51 +03:00
MarcelMediaDev db4339f698
fix(gta): restore wiped GTA foundation and gameplay path (PPSA04264) (#650)
* fix(kernel): implement APR ResolveFilepathsWithPrefixToIdsAndFileSizes

Resource streamers resolve relative paths against a shared prefix; without
this HLE every call returned NOT_FOUND and assets never got real ids/sizes.

* fix(remoteplay): stub Initialize and GetConnectionStatus as disconnected

Titles probe Remote Play during pad/network bring-up; unresolved imports
returned NOT_FOUND. Report initialized + disconnected so callers take the
normal offline path.

* fix(agc): accept Gen5 hull shaders that omit PGM_LO/HI in CreateShader

Type-5 headers can start with RSRC1/RSRC2; rejecting them left null handles
and Main Thread AVs. Scan the SH table and skip PGM patch when absent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kernel): reject getdents on file fds and emit . / .. for empty dirs

Returning rax=0 for non-directory or empty listings looked like EOF and
let GTA treat the fd as a pointer (fiWriteAsyncDataWorker AV at 0xB1).

* fix(hle): enable GuestImageWriteTracker CPU sync on Windows

Windows previously hard-disabled the tracker, so CPU-written guest
planes never marked dirty and host textures stayed empty. Arm pages
with VirtualProtect, handle write AVs in VEH, and warm/test on
VirtualAlloc memory so protect cannot poison the CRT heap.

* fix(agc): skip CB metadata draws for EliminateFastClear/Fmask/DCC

CB_COLOR_CONTROL modes 2/5/6 are colour-buffer metadata ops; applying
the bound shader as a normal colour draw corrupts subsequent composites.
Decode MODE from bits [6:4] and return before translate.

* fix(agc): merge Prospero attrib-table formats onto IR vertex inputs

IR-discovered BufferLoadFormat often keeps a stale float sharp format;
patch DataFormat/offset from the AGC attrib table (semantic index),
allow offen fetches, and map quirks 113/121 through NarrowVk for host
vertex input.

* fix(audio): harden AudioOut2 stack out-buffer writes against canary smash

Titles that stack-allocate AudioOut2 outs next to the frame canary were
corrupted by oversized or mistyped HLE writes; keep ContextPush pacing.

* Revert "fix(memory): reserve only large regions (#608)"

This reverts commit 8f9456229a.

* fix(gpu): decode Gen5 R16 and RG32 render-target formats

* fix(audio): AudioOut2 host beds, deeper waveOut queue, AJM MP3

GTA V Enhanced routes intro/menu audio through AudioOut2 and FMOD's AJM
MP3 path. Wire PortCreate/PortSetAttributes/ContextPush to dual host
stereo streams, deepen WinMM queue to 128KiB, and decode AJM codec 0
with a stateful NLayer helper so menu music is not silent.

* fix(agc): map PS interpolants via SPI_PS_INPUT_CNTL semantics

Identity ATTR→param wiring ignored hardware remapping, so UI draws
got wrong (or empty) interpolants. Pack CNTL from matched PS/GS
semantics, thread it into Vulkan/Metal as Location/Flat, and fingerprint
it in the graphics shader cache key.

* fix(agc): rect-list/NGG strips, Index8 expand, and GE_INDX_OFFSET

NGG single-rect UI needs triangle-strip expansion; Prospero Index8 must
expand to host u16; glyphs need base vertex from GE_INDX_OFFSET. Skip
param-less rect-lists instead of inventing colour draws.

* fix(np): report GTA Story Mode addcont entitlements as owned

NpEntitlementAccess was returning an empty add-on list, so GTA V Enhanced offered Buy Story Mode. Publish the installed license labels and stub premium-event registration so offline sessions take the owned path.

* fix(cpu): prefer native workers for all guest entry stubs

Route thread entry, continuation, and main entry through RunGuestEntryStub so guest stubs are not invoked above CLR-managed frames (UnmanagedCallersOnly FailFast). Keep requireNativeWorker for tbb_thead; other paths prefer workers with calli fallback.

* fix(agc): implement Rewind/Jump writers and IT_REWIND waits

GTA Subrender AVs came from AcbJumpGetSize / DcbRewind returning NOT_FOUND as packet sizes. Add IT_REWIND and INDIRECT_BUFFER writers, patch SetRewindState into the GPU wait registry, and nest-parse 4-dword jumps.

* fix(gpu): use AddrLib ExactXor for Gen5 Standard256B (mode 1)

Mode 5 already had Standard4K ExactXor; mode 1 still used the generic StandardSwizzle block table, which mis-detiles Gen5 UI atlases.

* Revert "fix(cpu): prefer native workers for all guest entry stubs"

This reverts commit 31c4db0d3886dbc51cc6cbc1a4a5432d678ab113.

* fix(memory): commit-first large maps; reserve only on failure

Replace the #608 always-reserve-only exact-map path with allocate-first and lazy reserve fallback when a huge non-exec commit cannot be satisfied. Prime and widen GetPointer commit so the fallback path is safer for native walkers. Drops the need for a hard #608 revert.

* [Agc] Implement fused shader half exports

* fix(agc): accept optional hull state in CreatePrimState

Port the CreatePrimState hull-optional path from #583 so fused HS pipelines (GTA) are not rejected with INVALID_ARGUMENT. Geometry-derived CX/UC writes are unchanged; hull is traced only.

* fix(videoout): restore thread-safe VulkanHostBufferPool (#564)

The 6db095e wipe dropped CasualcoderDev's lock-ordering-safe pool. Concurrent Return/TryTake without the gate races after the first present and can hang the submit path.

* Revert "fix(agc): implement Rewind/Jump writers and IT_REWIND waits"

This reverts commit bec77bf083dd4037455721e6874023bd05fbdb79.

* test(memory): align lazy-commit expectations with commit-first policy

Fake hosts must reject Allocate so reserve-only paths still run, and GetPointer asserts the 32 MiB prime range including AlignUp spill.

* diag(gpu): log guest-queue backlog breakdown under backpressure

Rate-limit top work types and ordered debugName prefixes when the Vulkan guest work queue stalls, so North Yankton logs show acquire/label vs draw traffic instead of only VulkanOrderedGuestAction.

* perf(agc): coalesce acquire flushes and batch non-DMA label wakes

Flush pending ACQUIRE_MEM invalidation at draw/dispatch/dma/flip boundaries instead of before every packet, and complete release/write-data producers in the same ordered action so load paths enqueue far fewer VulkanOrderedGuestAction items.

* perf(gpu): wait for ordered-action fences and keep draining sync

On Windows/Linux, block briefly for queue-visibility fences instead of deferring the whole logical queue for the tick. Prefer ordered sync/flip heads under backlog pressure, and keep macOS non-blocking defer behavior.

* perf(gpu): raise sync-item ceiling above payload guest-work cap

Apply SHARPEMU_PENDING_GUEST_WORK_ITEMS mainly to compute/draw/image payload work, and allow a higher SHARPEMU_PENDING_GUEST_SYNC_ITEMS ceiling for zero-payload ordered actions and flip markers. Keep the byte budget as the RAM safety valve.

* fix(gta): stub Voice ports and implement sceKernelCheckReachability

Resolve North Yankton-path Voice Create/Delete/Connect/Disconnect/End NIDs and EnumerationThread reachability checks so leftover unresolved imports are not on the critical path.

* diag(gta): arm flip/present/wait probes after North Audio

Rate-limited load_progress TRACE for flip submit, ordered flip enqueue, present taken/not-taken, and GPU wait backlog so North Yankton freezes can be classified without full AGC tracing.

* fix(ampr): restore sequential offset=-1 reads for streamer packs

Re-wire PakDirectoryTracker into sceAmprAprCommandBufferReadFile (dropped in #216) so RAGE sequential pack reads no longer fail while the North Yankton UI keeps flipping. Also rate-limit CheckReachability miss paths for EnumerationThread diagnosis.

* fix(hle/videoout): Windows GuestImage opt-in and keep GTA intro without sync

Default the tracker off on Windows to avoid VirtualProtect thrash, gate AGC
texel-copy skips on Enabled so guest Bink planes keep shipping pixels, and
drain CPU-written images on the present thread when sync is opted in.

* fix(videoout): probe guest content when tracker off so UI can skip copies

Restores upload-known/texture-cache skips for Dead Cells menus, and uses a
sparse guest-memory fingerprint when GuestImageWriteTracker is disabled so
CPU-updated Bink planes still force texel copies for GTA intro.

* fix(audio): keep 128KiB host queue AudioOut2-only

Restore the default 32 KiB (~171 ms) PCM bed for classic AudioOut so
titles like Dreaming Sarah stay in sync; only AudioOut2 opens the deeper
queue needed for bursty FMOD Push on GTA.

---------

Co-authored-by: samto6 <123419830+samto6@users.noreply.github.com>
2026-07-27 01:58:55 +03:00
Berk 0535783f46
Update README with project details and usage instructions 2026-07-26 15:13:17 +03:00
Berk 99004a3ccd
[GPU] Host cached guest buffer (#649) 2026-07-26 04:28:32 +03:00
Berk e1a3b92567
[CPU] Fix Sema ORBIS_GEN2_ERROR_BUSY loop (#621) 2026-07-25 14:42:35 +03:00
Andrew 8f9456229a
fix(memory): reserve only large regions (#608)
* fix(memory): reserve only large regions

* Potential fix for pull request finding
2026-07-25 14:25:53 +03:00
Berk 5b602c0232
[GPU] Fix detiled cache key for VulkanDetilePass (#620) 2026-07-25 14:12:17 +03:00
Digote 26c502914c
feat(audio): implement sceAudioOutOutputs (#605)
* feat(audio): implement batched output submission

* test(audio): cover batched output semantics

* test(audio): cover multi-port output batches

---------

Co-authored-by: diego <diego@DIGOTE-PC>
2026-07-24 20:13:30 +03:00
shadowbeat070 a158960c20
feat(gpu): GPU compute detile for guest tiled textures (Vulkan + Metal) (#592)
* feat(gpu): GPU compute detile for guest tiled textures (Vulkan + Metal)

Move RDNA2 exact-XOR deswizzle (swizzle modes 5/9/24/27, 4bpp) off the CPU
onto a GPU compute pass. GnmTiling.GetDetileParams resolves the shared
addressing into DetileParams; the CPU fallback and both GPU kernels consume
the same params so they never disagree.

Vulkan (verified bit-exact on NVIDIA): SpirvFixedShaders.CreateDetileCompute
hand-emits the SPIR-V kernel; VulkanDetilePass.RecordDetile records the
dispatch into the async batch command buffer (never a blocking submit on the
render thread) with transients retired via fence; VulkanDetileSelfTest
(SHARPEMU_DETILE_SELFTEST=1) checks both entry points against the CPU detile.

Metal (Mac-untested): detile_compute.msl (detile_cs) + MetalDetilePass mirror
the Vulkan pass. The active Metal path CPU-detiles via the new
GnmTiling.DetileWithParams when a texture arrives packaged (empty RgbaPixels +
TiledSource/Detile), keeping Metal correct under default-on with no regression;
wiring MetalDetilePass live is the remaining on-device step.

Flags: GPU detile is default-on (SHARPEMU_GPU_DETILE=0 disables);
[GPU-DETILE] diagnostics gated behind SHARPEMU_LOG_GPU_DETILE=1.

Tests: 17 detile unit tests pass, incl. DetileWithParams and GetDetileParams
each matching TryDetile bit-for-bit across all supported modes/bpp, plus a
SPIR-V structural-validity test.

* feat(gpu): GPU compute detile for guest tiled textures (Vulkan + Metal)

Move RDNA2 exact-XOR deswizzle (swizzle modes 5/9/24/27, 4bpp) off the CPU
onto a GPU compute pass. GnmTiling.GetDetileParams resolves the shared
addressing into DetileParams that the CPU fallback and both GPU kernels
consume, so they never disagree; everything else keeps the CPU path.

Vulkan (verified bit-exact on NVIDIA): SpirvFixedShaders.CreateDetileCompute
hand-emits the kernel; VulkanDetilePass.RecordDetile records into the async
batch command buffer (never a blocking submit on the render thread) with
transients retired via fence, falling back to CPU detile on failure.
VulkanDetileSelfTest (SHARPEMU_DETILE_SELFTEST=1) checks both entry points.

Metal (Mac-untested): detile_compute.msl + MetalDetilePass mirror the Vulkan
pass; the active Metal path CPU-detiles via GnmTiling.DetileWithParams so it
stays correct under default-on. Wiring MetalDetilePass live is a follow-up.

Flags: default-on (SHARPEMU_GPU_DETILE=0 disables); diagnostics behind
SHARPEMU_LOG_GPU_DETILE=1. Adds 17 passing detile unit tests.

* Fix: added support layered texture support for the GPU-Detiling.

* Fix: Added support for BlockTable (1 / 4 / 8 (Morton/Z-order))

* feat: added support for 8 and 16 bpp (bytes per element)

* Fixed a build failure specific to this branch

---------
2026-07-24 20:13:02 +03:00
MarcelMediaDev 5228335f15
fix(gpu): support Gen5 flat memory and 3D images (#587)
Vector-mesh UI text samples type-10 volume LUTs; treat MIMG DIM=2 as
Dim3D and transport depth through AGC and Vulkan so Z slices no longer
collapse into a single 2D plane.
2026-07-24 15:44:58 +03:00
Berk 21f964a0dc
Update README 2026-07-24 03:00:05 +03:00
ParantezTech 6133313a83 [readme] added support SharpEmu section 2026-07-24 02:58:58 +03:00
ParantezTech 6db095ec82 revert: restore state before huge regression 2026-07-23 16:03:45 +03:00
CasualcoderDev 5a08a9bb43
fix: VulkanHostBufferPool deadlock, audio overflow crash, and log grouping (#564)
* fix: VulkanHostBufferPool deadlock, audio overflow crash, and log grouping

* fix: implement thread-safe buffer pool, refactor output handling, and use unchecked cast for audio conversion
2026-07-23 15:41:51 +03:00
MarcelMediaDev f9d92135a0
fix(agc): merge Prospero attrib-table formats onto IR vertex inputs (#556)
IR-discovered BufferLoadFormat often keeps a stale float sharp format;
patch DataFormat/offset from the AGC attrib table (semantic index),
allow offen fetches, and map quirks 113/121 through NarrowVk for host
vertex input.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:39:24 +03:00
Radu Ursache 8779c96c3a
test(debugger): add C# unit tests for protocol and breakpoints (#568)
Cover DebugRequest parsing, BreakpointStore, and DebugCommandDispatcher
with a fake session. Wire the project into SharpEmu.slnx.
2026-07-23 15:38:08 +03:00
MarcelMediaDev 4c6cff1116
fix(agc): skip CB metadata draws for EliminateFastClear/Fmask/DCC (#553)
CB_COLOR_CONTROL modes 2/5/6 are colour-buffer metadata ops; applying
the bound shader as a normal colour draw corrupts subsequent composites.
Decode MODE from bits [6:4] and return before translate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:37:34 +03:00
MarcelMediaDev 82ab181861
fix(hle): enable GuestImageWriteTracker CPU sync on Windows (#550)
Windows previously hard-disabled the tracker, so CPU-written guest
planes never marked dirty and host textures stayed empty. Arm pages
with VirtualProtect, handle write AVs in VEH, and warm/test on
VirtualAlloc memory so protect cannot poison the CRT heap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:37:05 +03:00
MarcelMediaDev 007bf6fa73
fix(kernel): reject getdents on file fds and emit . / .. for empty dirs (#546)
Returning rax=0 for non-directory or empty listings looked like EOF and
let GTA treat the fd as a pointer (fiWriteAsyncDataWorker AV at 0xB1).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:36:28 +03:00
MarcelMediaDev 8e1e89c024
fix(agc): accept Gen5 hull shaders that omit PGM_LO/HI in CreateShader (#545)
Type-5 headers can start with RSRC1/RSRC2; rejecting them left null handles
and Main Thread AVs. Scan the SH table and skip PGM patch when absent.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:36:19 +03:00
Kurt Himebauch 2764aaab3f
feat: implement cosf, time, ctype tables, tracked heap access, IL2CPP lookup ABI paths (#542)
* fix: add Messenger CRT and AGC compatibility shims

* fix: keep Messenger IL2CPP bootstrap on HLE shims

* Fix Messenger compatibility ABI handling

* Make IL2CPP ABI regression tests portable
2026-07-23 15:35:59 +03:00
MarcelMediaDev 7b950166d7
fix(remoteplay): stub Initialize and GetConnectionStatus as disconnected (#536)
* fix(remoteplay): stub Initialize and GetConnectionStatus as disconnected

Titles probe Remote Play during pad/network bring-up; unresolved imports
returned NOT_FOUND. Report initialized + disconnected so callers take the
normal offline path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger gameplay CI for PR #536

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:35:28 +03:00
MarcelMediaDev eb1195e59a
fix(kernel): implement APR ResolveFilepathsWithPrefixToIdsAndFileSizes (#534)
* fix(kernel): implement APR ResolveFilepathsWithPrefixToIdsAndFileSizes

Resource streamers resolve relative paths against a shared prefix; without
this HLE every call returned NOT_FOUND and assets never got real ids/sizes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger gameplay CI for PR #534

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:34:58 +03:00
MarcelMediaDev e13cb28267
fix(audio): harden AudioOut2 stack out-buffer writes against canary smash (#532)
Titles that stack-allocate AudioOut2 outs next to the frame canary were
corrupted by oversized or mistyped HLE writes; keep ContextPush pacing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 15:34:40 +03:00
Radu Ursache 956da769a3
fix(kernel): finish Posix -1/errno for file ops and open EACCES (#567)
Map UnauthorizedAccess on open to PERMISSION_DENIED and route Posix
lseek/pread/pwrite/rename/etc failures through PosixFailure so libc-style
callers get RAX=-1 plus TLS errno, matching open/read/write.
2026-07-23 15:32:56 +03:00
h4sht d7bd814fb9
[Core/Dlsym] Restore sceKernelDlsym bootstrap argument normalization from PR #94 (#565)
* [Core/Dlsym] Restore normalize-dlsym-arguments and deferred bootstrap tracing from PR #94

PR #216 regressed two critical features from PR #94:
1. NormalizeKernelDynlibDlsymArguments — handles argument reordering
   when standalone bootstrap loaders call sceKernelDlsym through the
   bridge with (symbol_ptr, handle, out) instead of the standard
   (handle, symbol_ptr, out). Without this, payloads like elfldr-ps5
   and websrv-ps5 fail with a deterministic UnmanagedCallersOnly
   fail-fast.
2. Deferred bootstrap tracing — ring-buffered import logging that
   drains after the hot path, avoiding per-call Console.Error I/O.

Also restored:
- CompleteKernelDynlibDlsymFailure — centralized error handling
- IsPlausibleDynlibSymbolPointer — pointer bounds validation
- COW snapshot of _importEntries in ProbeReturnRip
- ResetLazyDlsymStubState and lazy-dlsym field infrastructure
- DraftDrainDeferredBootstrapTraces in Execute() finally block

Fixes #530, fixes #531

* fix: remove orphaned _importNidHashCache.Clear() reference

The field _importNidHashCache no longer exists on main (removed post PR #94).
The 3-way merge incorrectly restored the .Clear() call without the field
declaration, causing a build failure on all platforms.

---------

Co-authored-by: tru3 <tru3@tru3.com>
2026-07-23 15:24:59 +03:00
Mike Saito 96fde5764f
Astro Bot stack: VEH/TBB, title clear, swapchain fallback, Psml MFSR (#528)
* Cpu/Kernel: harden VEH trampoline and keep TBB on native workers

Route FastFail/CLR/stack-overflow around managed VEH, serialize managed
entry with a recursive spinlock, require native workers for tbb_thead,
and abandon pthread mutexes when a guest thread is torn down by worker
abort so splash waiters are not left holding locks forever.

* Cpu: soft-fail TBB native worker storms and cap concurrent Runs

Throwing on worker/prologue faults killed the process mid tbb_thead
burst (FailFast 0xC0000409). Soft-return 0x80020012, limit in-flight
native Runs (default 2), and keep prewarm small so back-to-back boots
do not need an artificial settle delay.

* Agc/VideoOut: poison-only empty-SRT reject and clear procedural ES/PS

Skip QueueSubmit only when Address-0 image slots remain; run the
Astro title clear pair via CmdClearColorImage so the pass executes
without descriptors that lose the device.

* VideoOut: recreate swapchain with fallback extent on 0x0 surface

Minimized Win32 surfaces report 0x0 / MaxImageExtent=0; deferring
recreate forever left an OutOfDate swapchain with no presents.
Clamp to last/default size and recreate instead of early-return.

* Psml: stub MFSR init/shared/context and dispatch packet size

Astro Bot asserts in GfxRenderStagePSSR when scePsmlMfsrInit is unresolved
(Mfsr initialized failed). Soft HLE for the MFSR shared-resource and
800M3_2 context path plus dispatch packet size lets boot pass splash to
first frame without claiming real upscaling.

* Psml: stub MFSR GetDispatchMfsrPacket900 for logo PSSR

Astro StartLevel ps_logo asserted GfxRenderStagePSSR.cpp:266 when
GetDispatchMfsrPacket900 (RUNLFro+qok) was unresolved. Return SCE_OK
from SizeInDwords so the 900 fill runs, soft-clear the guest packet
buffer, and register 1000/1100 siblings for the same ABI.
2026-07-23 12:37:44 +03:00
MarcelMediaDev 7a108c6f87
fix(hle): GameService Ok stubs plus NetInetPton and Json terminate (#560)
Resolve logo-accept-gate unresolved NIDs seen on PPSA04264: share/voice/
telemetry/content Ok stubs, sceNetInetPton, and Json Initializer::terminate.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 12:33:39 +03:00
Kurt Himebauch eb252af7b3
unstub: preserve notice screen skip flag (#559)
* fix(system-service): preserve notice screen skip flag

* test(bink): avoid frame timing flake on CI
2026-07-23 12:32:49 +03:00
MarcelMediaDev 8e5a0bfb19
fix(agc): implement sceAgcDcbSetUcRegisterDirect (#558)
Emit the 3-dword SET_UCONFIG_REG packet from the packed {offset,value}
in RSI. Unresolved calls left GPU config registers unset during
RenderThread/Main bring-up.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 12:32:04 +03:00
ParantezTech d991e32b15 [readme] update screenshots 2026-07-23 03:14:26 +03:00
Berk 93829e3242
chore: bump version to 0.0.2-beta.5 (#555) 2026-07-23 03:07:14 +03:00
Berk 4191a9e12b
[Bink2] rework bridge to use FFmpeg's native Bink2 decoder instead of a C bridge (#554)
* [Bink2] rework bridge to use FFmpeg's native Bink2 decoder instead of a C bridge

* [readme] update DeS screenshot
2026-07-23 03:06:14 +03:00
Mariano Zambelli 559b7f0a84
feat(voice): add QoS stubs (GetStatus, Terminate, SetMode) (#541)
* feat(voice): add QoS stubs (GetStatus, Terminate, SetMode)

Titles call these functions during voice/multiplayer setup to check
network availability and configure modes. Unresolved imports caused
WARN floods in the loader logs. Reporting initialized + disconnected
lets callers take their normal offline path.

* fix(voice): return success (0) from sceVoiceQoSGetStatus instead of disconnected state
2026-07-23 01:48:55 +03:00
MarcelMediaDev 2272b9b576
fix(ajm): silence BatchJobDecode/Start/Wait/Cancel hot-path stubs (#547)
Unresolved batch NIDs flooded Import WARNs on Bink/AJM. Claim input
consumed with silence produced; this is not a real codec.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 01:44:07 +03:00
MarcelMediaDev 8dd3172c0f
fix(systemservice): stub notice-screen skip flag setters (#549)
Settings probes Set/DisableNoticeScreenSkipFlagAutoSet; unresolved
NOT_FOUND can stall the SaveModTime/Load path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 01:41:08 +03:00
Berk e7ea186ea8
Enhance contribution guidelines with PR expectations
Added expectations for pull requests regarding observable behavior and testing requirements. Clarified guidelines for AI-assisted contributions.
2026-07-23 01:40:00 +03:00
MarcelMediaDev 74a519875b
fix(agc): add missing Cb/Dcb GetSize stubs for packet sizing probes (#535)
Unresolved GetSize NIDs returned NOT_FOUND during RenderThread startup,
leaving null packet pointers and an immediate write AV. Return fixed
packet byte sizes in rax only — no guest memory writes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 00:18:47 +03:00
Andrey Modnov 4682e64e81
[CLI] Simplify mitigated child arguments (#529) 2026-07-23 00:18:11 +03:00
Kurt Himebauch 912883de05
fix(cmake): invalidate stale FFmpeg library cache (#543) 2026-07-22 23:56:37 +03:00
Berk f704586a8d
[VideoOut] Add Bink2 support via FFMPEG bridge (#527)
* [VideoOut] Add Bink2 support via FFMPEG bridge

* [CMake] update commit

* [CMake] update commit
2026-07-22 21:41:41 +03:00
jute-ado d3600c9255
fix(ajm): accept Gen5 codec types (#526) 2026-07-22 18:24:06 +03:00
jute-ado 5f97031df5
shader: allow larger bounded Gen5 programs (#514) 2026-07-22 14:46:15 +03:00
h4sht 2a4da8c0a9
[Kernel/Semaphore] Close race between sceKernelWaitSema and sceKernelSignalSema (#504)
When sceKernelWaitSema finds the count insufficient it increments
WaitingThreads, releases the semaphore gate, and calls
RequestCurrentThreadBlock to set the thread-static block flags. A
signal arriving before the scheduler registers the block metadata
is missed by WakeBlockedThreads — the waiter has not been
registered yet and the signal's wake iteration skips it.

The scheduler's exit handler already re-checks TryWake() after
setting the thread to Blocked, but that requires the thread to
fully exit to the scheduler and back. Instead, re-check the
semaphore count under the gate immediately after the block request:
if the count is now sufficient, consume the tokens, cancel the
pending block via TryConsumeCurrentThreadBlock, and return without
ever yielding to the scheduler.

Co-authored-by: tru3 <tru3@tru3.com>
2026-07-22 14:34:19 +03:00
h4sht 4c37e64c66
[NpWebApi2] Add sceNpWebApi2PushEventCreateFilter stub (#503)
Add sceNpWebApi2PushEventCreateFilter (NID: MsaFhR+lPE4) to the
libSceNpWebApi2 module. This function is called by Unity games
during initialization and was unresolved, causing an import warning
and returning ORBIS_GEN2_ERROR_NOT_FOUND.

The stub validates the library context and returns an incrementing
filter handle, following the same pattern as the existing
sceNpWebApi2PushEventCreateHandle.

NID sourced via:
  python scripts/aerolib_catalog.py lookup MsaFhR+lPE4

Co-authored-by: tru3 <tru3@tru3.com>
2026-07-22 14:33:43 +03:00
kostyaff fc9e3ff393
fix: roll back earlier host allocations on later gap failure in TryBackFixedRange (#472) (#474)
When a fixed mapping spans multiple free runs and a later gap cannot be
backed, any earlier host allocations were leaked. Stage all allocations
during the walk and insert MemoryRegions only after every gap has been
backed successfully. On any failure, free all staged allocations.

Fixes #472

🤖 Generated with Hermes Agent
2026-07-22 14:28:25 +03:00
samto6 eb47d753f6
[Ampr] Implement the FW 4.00 write-address command exports (#510) 2026-07-22 03:00:30 +03:00
h4sht 6aa78bb55b
[Loader] Fall back to fixed-range backfill when main image base is occupied (#493)
When TryAllocateAtExact fails for the main image base (0x800000000
for PS5, 0x400000 for PS4), the loader previously threw a fatal
InvalidOperationException with no recovery path. This happens when
the host OS has already claimed part of that address range — common
under Rosetta 2, with aggressive ASLR, or when another process maps
into the guest address space.

Instead of failing immediately, attempt TryBackFixedRange which
backs the range page by page, claiming any free gaps. If the
backfill also fails, Clear() rolls back partial allocations and
the exception now includes platform-specific recovery advice.

This prevents the most common emulator startup crash on affected
hosts.

Co-authored-by: tru3 <tru3@tru3.com>
2026-07-21 18:18:58 +03:00
h4sht 9be6f85ef0
[Font] Implement sceFontGetVerticalLayout (#492)
Add sceFontGetVerticalLayout (NID: 3BrWWFU+4ts) to the Font module,
completing the vertical-text counterpart to the existing
GetHorizontalLayout. The SceFontVerticalLayout structure is three
floats (baseline, lineAdvance, decorationExtent) interpreted for
vertical writing such as CJK text rendered top-to-bottom.

- Write baseline=8.0f, lineAdvance=16.0f, decorationExtent=0.0f
- Validate output pointer and return INVALID_ARGUMENT on null
- Return MEMORY_FAULT when guest writes fail

Tests:
- GetVerticalLayout_WritesExactlyThreeFloats with sentinel guard
- GetVerticalLayout_NullBuffer_ReturnsInvalidArgument

NID sourced via: python scripts/aerolib_catalog.py lookup sceFontGetVerticalLayout

Co-authored-by: tru3 <tru3@tru3.com>
2026-07-21 18:18:16 +03:00
Kurt Himebauch 4c8c67a3dd
fix: Add ASTRO BOT compatibility stubs (#481)
* Add ASTRO BOT compatibility stubs

* Fix ASTRO BOT compatibility stubs
2026-07-21 18:17:40 +03:00
999sian ada67a1924
cpu: recover SSE4a EXTRQ/INSERTQ faults on Linux (#482)
The fault-time SSE4a fallback was Windows-only because the POSIX signal
bridge never carried XMM state: the CONTEXT scratch buffer only held the
17 general-purpose registers, so emulating EXTRQ/INSERTQ there would
have computed results from zeroed bytes and discarded the write. Bridge
the XMM registers on Linux by copying them between the mcontext's
FXSAVE image (kernel sigcontext ABI, libc-independent) and the CONTEXT
FltSave slots on capture and write-back, and gate the recovery on that
bridge instead of on Windows. Darwin still declines: its XMM area
remains unbridged.

With this, guest EXTRQ/INSERTQ on Linux hosts without SSE4a (any Intel
CPU) resumes with correct register state instead of dying on an
unrecovered SIGILL (#328).
2026-07-21 14:18:21 +03:00
Slick Daddy 2379e8988c
[Loader] Collect stub-eligible NIDs in one pass over descriptors (#489)
BuildImportStubs filtered orderedImportNids by calling ShouldCreateImportStub
for each unique NID, and every call scanned the entire descriptor list
looking for a match. On a real module both the NID count and the descriptor
count run into the thousands, so the filter degraded to O(nids * descriptors)
ordinal string comparisons on the one-time load path.

Replace the per-NID rescan with a single pass over the descriptors that
builds a HashSet of eligible NIDs, then filter orderedImportNids with O(1)
membership. Eligibility is unchanged: a NID qualifies when any of its
descriptors is non-weak, or is weak but resolvable via the module manager.

ShouldCreateImportStub is retained (still used by the DEBUG self-checks), and
a self-check now asserts the set-based collector agrees with the per-NID rule.

Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
2026-07-21 12:59:30 +03:00
Slick Daddy 105c58b380
[Tests] Isolate Gen5 scalar fallback test from parallel static mutation (#488)
ScalarLoadReadsTrackedFallbackMemory swaps the process-global static
Gen5ShaderScalarEvaluator.FallbackMemoryReader under a lock private to the
test class. The SharpEmu.Libs [ModuleInitializer] (AgcShaderCompilerHooks)
assigns the same static to TryReadShaderGuestMemory the first time any Libs
type is touched, and it does not take that lock. Under xUnit's default
cross-class parallelism a concurrent Libs test could fire the initializer
mid-test, clobbering the swapped-in reader — observed on CI (linux-x64) as
the fallback returning all zeros: Expected [1181044592, 4, 1319632096, 4],
Actual [0, 0, 0, 0].

Put the test in a DisableParallelization collection, matching the existing
convention for shared-mutable-static tests (KernelMemoryCompatState,
AjmState, AvPlayerPathState). The collection runs alone in the non-parallel
phase, so no other test can mutate the static while this one holds it.

Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
2026-07-21 12:59:03 +03:00
Slick Daddy da35f0db47
[Audio] Hoist volume clamp out of the per-sample PCM loop (#487)
Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
2026-07-21 12:58:35 +03:00
Slick Daddy 1f3963c543
[Gpu] Factor the exact-XOR swizzle equation in the texture detiler (#483)
TryDetile's exact-XOR fast path (PS5 swizzle modes 5/9/24/27) ran the
full AddrLib address equation per element: a 16-bit interleave with 32
PopCount calls for every pixel of textures that are millions of elements.

Each output bit is parity(x & XMask) XOR parity(y & YMask), and parity
distributes over XOR, so the offset factors into independent xTerm(x) ^
yTerm(y) fields. Precompute the per-column X term once and hoist the Y
term per row, collapsing the inner loop to one array load and one XOR.

Add GnmTilingDetileTests, which lays out a tiled buffer from an
independent re-derivation of the mode-27 equation and asserts TryDetile
reconstructs it byte-for-byte.

Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
2026-07-21 12:57:39 +03:00
iExplosiveRage 4bb1af93d7
SaveData: avoid invalid DeS transaction resource pointer (#480)
Demon's Souls treats the small transaction-resource handle as a guest pointer during the fresh-save path. Return a null resource for the observed call shape to prevent the repeatable access violation at address 0x9.

Co-authored-by: RedDv <RedDv@DESKTOP-EVNB4S8>
2026-07-21 02:22:51 +03:00
Nicola Pomarico 0ae785c617
[VideoPresenter] Accept padded row pitch in guest image uploads (#475)
The guest can hand initial texture data whose rows are padded out to a
hardware alignment wider than the image width, so the total byte count
exceeds the tightly packed width*height*bpp we compute. The upload path
rejected any byte count that did not match exactly, silently dropping
these uploads and leaving the texture blank.

Recover the real source row length when the byte count is consistent
with a common padding alignment (8/16/32/64/128/256 texels) and pass it
through as BufferRowLength on the copy, instead of always hardcoding 0.
Uploads that do not match a recognised padded layout are still rejected
as before.

Verified against Dead Cells (PPSA15552): a loading-transition texture
upload that previously wedged the title now uploads correctly and the
game proceeds past the load screen, running stably past 1M draw calls
with no stalls. Dreaming Sarah (tightly packed path) still renders
normally, confirming no regression to the non-padded case.
2026-07-21 01:01:28 +03:00
Slick Daddy e01092aa38
Kernel FS: close guest→host sandbox escapes in the path resolver (#478)
* Kernel FS: default-deny unmapped guest paths (fixes absolute-path host escape)

ResolveGuestPath returned any unrecognized guest path verbatim as the host
path. Because absolute paths ("/etc/passwd", "C:\Windows\...") are already
fully qualified, they skipped the relative-path app0 fallback and were handed
straight to FileStream/File.Delete/etc., giving a malicious game arbitrary
host-file read/write/delete outside the sandbox.

Return string.Empty (deny) on fallthrough instead. Most callers already treat
a nonexistent host path as NOT_FOUND; open/truncate/rename get an explicit
empty-path guard so a denied path can't reach FileStream and throw an
ArgumentException their catch blocks don't cover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Kernel FS: contain built-in mounts (fixes Windows drive-letter injection)

The built-in mount branches (app0/temp0/download0/hostapp/devlog) combined
the mount-relative guest path onto the host root without re-checking
containment. NormalizeMountRelativePath clamps ./.. but splits only on
separators, so a drive-qualified token like "C:" survives as a segment and
Path.Combine then discards the mount root, yielding a raw host path such as
"C:\Windows\..." (arbitrary host read/write).

Route every built-in branch through a new CombineWithinMount helper that
re-resolves with Path.GetFullPath and verifies the result stays under the
mount root -- the same guard TryResolveRegisteredGuestMount already applied.
Denied paths return string.Empty, which callers treat as unresolved.

AprStreamingContractTests passed a raw Path.GetTempFileName() as the guest
path, relying on the now-removed absolute-path passthrough; updated it to
address the file through a registered mount.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Kernel FS: reject reparse points inside mounts (fixes symlink escape)

Lexical containment (Path.GetFullPath + StartsWith) proves the textual
path stays under the mount root but does not follow symlinks/junctions.
A malicious game dump could plant a reparse point inside app0/temp0/etc.
pointing outside it, so a contained-looking path resolved onto the host
filesystem. Walk each existing component from the mount root to the
candidate and refuse any reparse point, in both the built-in and
registered-mount resolution paths. Mirrors AvPlayer's existing defense.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Kernel FS: fail closed when path containment cannot be verified

The reparse-point and drive-letter containment guards call Path.GetFullPath
and File.GetAttributes on untrusted guest paths. Both throw on crafted
over-long or invalid-char input, and ResolveGuestPath runs outside the file
syscalls' try blocks, so such a path was a guest-triggerable crash rather
than a denial.

Wrap the GetFullPath calls in CombineWithinMount and the registered-mount
path, and widen the GetAttributes catch, to treat any access/format failure
as an escape (deny) instead of propagating. Also tighten the ".." fallback
check so a legitimate file named "..foo" is not falsely rejected, and hoist
the repeated Path.GetFullPath(mountRoot) into a local.

Adds a regression test asserting the resolver returns without throwing for
an over-long and a NUL-embedded path under a mount.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Kernel FS: assert malformed paths resolve to empty, not just no-throw

The fail-closed regression test asserted only Assert.NotNull, which a
non-nullable string return can never violate via its value (only a throw,
which aborts the test earlier anyway). Tighten to Assert.Equal(string.Empty)
so it also locks in fail-CLOSED: a regression where a malformed path resolved
to a non-empty host path would now be caught.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Kernel FS: route new AMPR batch tests through a registered mount

Merging main brought in three AprStreamingContractTests that pass raw
Path.GetTempFileName()/temp host paths as guest paths. The default-deny
resolver from this branch rejects absolute host paths, so MissingMidBatch
failed at index 0 instead of the intended index 1. Address the present
file through a registered mount (as ResolveStatAndReadFile already does)
so entries 0 and 2 resolve and the batch fails at the genuinely-missing
entry. The two all-missing tests were unaffected but share the fix's intent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Kernel FS: make a matched-mount denial terminal; fix Unix-only test asserts

A registered mount that claims a path by prefix but denies it (failed
containment or a reparse point inside the mount) now short-circuits in
ResolveGuestPath instead of falling through to the built-in mount branches.
The fall-through let an overlapping prefix (a registered "/app0" vs the
built-in SHARPEMU_APP0_DIR branch, which resolves against a cached root)
re-resolve a denied path and turn the denial back into a resolution -- the
reparse-point escape reappeared on Linux CI through exactly this path.

Also fix two tests that asserted Windows-specific behavior unconditionally:
a "C:\..." path is not absolute on Unix (it resolves contained under the
mount there), and that case is now pinned to Windows only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 00:58:34 +03:00
TarkusTK 224a36eba7
[Gpu] Stop retrying array uploads that overrun their allocation (#476)
A 2D-array texture whose Depth times the per-slice stride runs past its
real allocation fails a slice read partway through the upload loop, and
falls through to the single-slice path after already detiling the layers
it did read. That fall-through builds the texture with ArrayLayers
defaulting to 1, so the presenter caches it under a one-layer key while
the next draw looks it up with ArrayLayers = Depth. The two never match,
so the texture misses the cache and repeats the whole read-and-detile on
every draw, throwing the result away each time.

Detiling is per-texel swizzle math, so one such texture retried a few
times per frame is expensive: it measured 568-879 ms of every second in
Demon's Souls, against a 1.4 second frame.

An allocation that is too short stays too short, so remembering the
address and not retrying it costs nothing and repairs the cache key as a
side effect: with the array upload skipped, arrayUploadLayers is 1, which
is exactly what the fall-through texture reports.

Tested on Demon's Souls (PPSA01342): 0.7 fps to 3.6-4.1 fps, CPU detile
time per second from ~700 ms to 0, and arrayed textures go from missing
the cache on every draw to hitting it every time. 28 of the 29 array
uploads in that run already succeeded and are unaffected; only the one
overrunning texture now falls back to its base slice. 495 tests pass.
2026-07-20 19:22:19 +03:00
Berk ac883e44fa
[VideoPresenter] Fix logical width/height calculation (#473) 2026-07-20 16:57:38 +03:00
TarkusTK 25d741b35b
[Gpu] Sample 2D array textures with real layers (#471)
Texture arrays were uploaded and viewed as plain 2D images, so every
layer index in a shader resolved to slice 0. The guest-texture gate
also rejected array, 3D and cube descriptor types outright, sending
those resources to the 1x1 black fallback.

UI atlases hit this constantly, since they pack several sheets as array
slices and pick one per vertex. In Demon's Souls the settings menu
bottom bar stretched a mid-atlas crop across itself, and slider thumbs
and button prompts drew the wrong sheet.

The MIMG decoder already had Dimension and IsArray, so
IsArrayedImageBinding makes one rule out of them for the SPIR-V
translator and the Vulkan backend to share. Both have to agree or the
declared image type and the bound view type mismatch. Sample and gather
bindings with an array address now declare an arrayed image and pass
(u, v, slice). AgcExports reads every slice at the per-slice mip-chain
stride and passes the layers packed in one buffer, which uploads as a
2D array image in a single copy region.

Load and store bindings are unchanged. Arrayed bindings that resolve to
a fallback or to a single-layer guest image get a one-layer 2D array
view so the descriptor still matches the shader.

Tested on Demon's Souls (PPSA01342): the bottom bar, slider thumbs and
button prompts draw their correct sheets. 470 tests pass.
2026-07-20 15:44:06 +03:00
TarkusTK dce7c87c4d
[AGC] Implement the owner-scoped resource unregister exports (#469)
sceAgcDriverUnregisterOwnerAndResources (ZLJk9r2+2Aw) and
sceAgcDriverUnregisterAllResourcesForOwner (SCoAN5fYlUM) were
unresolved. We already register owners and resources, and the guest
registers a resource owner per streaming batch, so with no way to
release one the fixed owner pool filled up: sceAgcDriverRegisterOwner
started failing and the guest logged its own "Agc registerOwner error:
0x80020003", after which it kept half-registering records. Demon's Souls
then crashed scanning that registry.

Owner-scoped teardown is straightforward because RegisteredAgcResource
already carries its owner, so both entry points share one sweep over the
resource table. UnregisterOwnerAndResources additionally drops the owner
itself and its compute queue, and reports INVALID_ARGUMENT for an owner
that was never registered. The existing single-resource
sceAgcDriverUnregisterResource (pWLG7WOpVcw) is unchanged.

Both NIDs are checked against their export names by the SHEM004
analyzer, which fails the build on a mismatch.

Tested on Demon's Souls (PPSA01342): the registerOwner error no longer
appears and the registry stays consistent across streaming batches. 470
tests pass.
2026-07-20 15:43:47 +03:00
TarkusTK 6ee445f0c2
[AGC] Read mip 0 from its GFX10 mip-chain offset (#470)
GFX10 stores a mip chain smallest-first: the mip tail packs into the
first swizzle block, the remaining mips follow in decreasing size, and
mip 0 ends up at the end of the allocation. We read the base level
straight from the descriptor address, so every mipped sampled texture
decoded as a collage of its own smaller mips - in Demon's Souls that
showed up as scrambled menu text and repeated controller icons.

GnmTiling.TryGetBaseMipPlacement ports the AddrLib chain-offset math
from Gfx10Lib::ComputeSurfaceInfoMacroTiled/MicroTiled. It returns a
byte offset to mip 0, or, when the whole chain fits inside the tail
block, the element coordinates of mip 0 within that block.
TryCreateGuestDrawTexture applies the offset to the sampled and storage
guest reads, and TryDetileTextureSource lifts a tail-resident mip 0 out
of the detiled block as a sub-rectangle.

MAX_MIP is only decoded from extended descriptors, so resources without
one, and single-level resources, keep the current behaviour.

Tested on Demon's Souls (PPSA01342): menu text and icons decode
correctly instead of showing shrunken copies of themselves. Verified
offline by dumping the raw tiled bytes and the detiled output for a
4096x4096 UI atlas and checking the art lands at the sampled
coordinates.
2026-07-20 15:27:46 +03:00
StealUrKill 9d187dec55
Prevent AvPlayer movie startup failures across supported hosts (#456)
* Prevent AvPlayer movie startup failures across supported hosts

* Prevent GR2 startup stalls during APR file checks and adaptive mutex self-locks.
2026-07-20 15:27:37 +03:00
kuba 3574a3b145
Shader: lower VOP3P V_FMA_MIX_F32/LO/HI (was dropping Unity HDR shaders) (#466)
The decoder recognises the VOP3P mix ops (0x20 V_FMA_MIX_F32, 0x21
V_FMA_MIXLO_F16, 0x22 V_FMA_MIXHI_F16) but left them opaque
(Vop3pRaw20/21/22), so at SPIR-V emission they fell through the
vector-ALU switch to the default and failed with "unsupported vector
opcode". A single unhandled instruction fails the whole compile, so any
shader using fma_mix was dropped entirely. Unity's built-in-RP /
PostProcessing v2 HDR, tone-mapping and auto-exposure shaders emit
V_FMA_MIX_F32, so those passes never translated (this is what kept
Superliminal's auto-exposure luminance chain from running).

Name the three opcodes in DecodeVop3p (like the packed v_pk_* ops) and
lower them in the SPIR-V translator. Each mix op computes a single f32
fma(a, b, c) where every source is read *independently* as either a full
f32 register/constant or one f16 half widened to f32. Per operand,
op_sel_hi selects f16-vs-f32 and op_sel picks which f16 half; the neg_hi
field is repurposed as an absolute-value modifier and neg negates,
applied abs-then-neg. This reuses the VOP3P op_sel/op_sel_hi/neg/neg_hi
bit layout with the mix-specific meaning, not the packed-math meaning.
The result is a scalar f32 for V_FMA_MIX_F32; _MIXLO/_MIXHI narrow it
back to f16 (exact round-to-nearest-even, via the existing
EmitFloatToHalf) and write it into the low/high 16 bits of vdst,
preserving the other half. The clamp modifier saturates to [0, 1]
consistently with the other VOP3P ops. Per-operand F16/F32 select and
the abs/neg modifiers follow shadPS4's GetSrcMix, the authoritative
reference for the mix semantics.

Adds Gen5FmaMixSpirvTests: assembles V_FMA_MIX_F32 (with a representative
op_sel/op_sel_hi/neg/abs) and V_FMA_MIXLO_F16 compute shaders and asserts
they translate to GPU SPIR-V without hitting the drop path and emit a
GLSL.std.450 Fma (and an FAbs for the neg_hi modifier). Both fail against
the pre-fix tree with "unsupported vector opcode Vop3pRaw20/21".
2026-07-20 14:37:40 +03:00
Job Meijer a1cbff8a9c
Fix NID BHouLQzh0X0, doubled StartupStaticTlsReservation memory. Both needed to launch GTA V. (#454)
* Increased StartupStaticTlsReservation (doubled) and fixed mistake in NID BHouLQzh0X0. Now GTA V RAGE engine seems to start loading.

* fixed NID BHouLQzh0X0, this had an issue causing GTA V not to load. Also doubled StartupStaticTlsReservation.

* Removed .vscode folder and reverted global.json
2026-07-20 14:37:30 +03:00
Spooks db9b20481c
Add internal render resolution scale and fix DPI Issue (#468)
* Add internal render resolution scale and fix embedded surface DPI scaling

Adds a GUI-configurable internal resolution scale (Graphics tab) that
renders offscreen color/depth targets below native guest resolution
and upscales on present, trading image quality for GPU headroom.
Storage/UAV images and sampled asset textures are left untouched, and
texture-alias/feedback-loop lookups compare against each target's
logical (unscaled) size so scaled render targets are still found
correctly when sampled back.

Also fixes the embedded game surface not filling the window: the
isolated emulator child process had no declared DPI awareness, so
Windows silently downscaled every window-geometry query it made
against the GUI-owned surface HWND by the display's DPI factor,
leaving an unfilled black margin on scaled displays.

* Remove flaky Gen5ScalarMemoryFallbackTests

* Restore Gen5ScalarMemoryFallbackTests

---------

Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
2026-07-20 14:37:20 +03:00
ParantezTech 8cd46243ab Merge branch 'main' of https://github.com/sharpemu/sharpemu 2026-07-20 14:23:24 +03:00
ParantezTech 3334707f7c [CI] fix rule name 2026-07-20 14:23:04 +03:00
kuba 20eda4443c
Shader: test a wave mask consumed as a per-lane predicate at the lane bit (#465)
* Shader: read a wave mask consumed as a per-lane predicate at the lane bit

A VCC/EXEC wave mask consumed as a per-lane predicate (the VCndmask
condition, a VCC/EXEC branch, or the derived _vcc/_exec bool) was tested in
single-lane emulation with a whole-word non-zero test (IsNotZero64) instead
of the current lane's bit. That is correct for comparison results (only the
lane's own bit is ever set) but wrong for bitwise-complement wave-mask idioms
(S_NOT / S_ORN2 / S_ANDN2 / S_NAND / S_NOR), which set the unused upper 63
bits: a whole-word test then reports the lane active even when its bit is
clear.

Unity's PostProcessing NaN killer does exactly this: per channel it computes
isNaN = NLT AND NGT AND NEQ (against 0), then combines the channels as
anyNaN OR NOT(v3-is-finite) via S_ORN2_B64. The complement set the upper mask
bits, so every valid pixel read as NaN and was replaced with 0, zeroing the
whole HDR scene before Bloom/Uber/tonemap. The 3D scene therefore rendered
black behind the menu while the UI survived. Extract the current lane's bit in
both single-lane and subgroup modes so IsWaveMaskActive matches the hardware.

Fixes Superliminal (PPSA06084) black 3D scene: the storage room now renders
behind the menu with natural exposure and no forced values.

(cherry picked from commit 7af6f4b6f314fe302619c0d44f4db00971c5bf24)

* test: wave-mask predicate is tested at the current lane bit

Regression test for the wave-mask lane-bit fix. Compiles a shader that
writes VCC at run time (V_CMP_EQ_F32) and asserts the emitted SPIR-V tests
the wave mask at the current lane's bit (mask & lane_bit) rather than with a
whole-word non-zero test. Fails against the previous IsNotZero64(mask) path,
which zeroed complement wave-mask idioms (S_ORN2/S_NOT, e.g. Unity's NaN
killer) across every lane.
2026-07-20 14:18:20 +03:00
Slick Daddy bb3318a503
kernel: return -1/errno from POSIX file syscalls on failure (#461)
* kernel: return -1/errno from POSIX open and fstat on failure

The POSIX-named open (wuCroIGjt2g) and fstat (mqQMh1zPPT8) exports routed
straight to the raw sceKernel* implementations, which report failure via
the 0x8002xxxx OrbisGen2Result sentinel in the return value. libc callers
follow the POSIX ABI and expect -1 with errno set, so they stored the
sentinel as a valid fd. Unity's IL2CPP file layer did exactly this while
probing the absent /app0/Media/il2cpp.usym: open returned NOT_FOUND
(0x80020002), the guest kept the sentinel as an fd, passed it back into
fstat, and eventually dereferenced a null pointer (vmovups xmm0,[rdi],
rdi=0) deep in a native .prx, crashing with 0xC0000005.

Wrap both entry points to translate a failed raw result into -1/errno,
mirroring the existing PosixStat/PosixLseek convention. Add a shared
PosixFailure helper (fstat maps a bad handle to EBADF; path calls default
to ENOENT) and route it through PosixStat too. Covered by two regression
tests reproducing the missing-file and misused-sentinel-fd cases.

* kernel: return -1/errno from POSIX close, read and write on failure

Same defect class as open/fstat: the POSIX-named close (bY-PO6JhzhQ),
read (AqBioC2vF3I) and write (FN4gaPmuFV8) exports forwarded the raw
sceKernel* core result, leaking the 0x8002xxxx sentinel to libc callers
that expect -1/errno on a bad fd. close in particular is on the crashing
Unity path, invoked on the sentinel the guest mistook for an fd.

Wrap all three through PosixFailure with EBADF as the fd-not-found errno.
Add regression tests for each, and correct the socket test that had
locked in the old raw-sentinel contract for a double close.

---------

Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
2026-07-20 14:17:08 +03:00
ParantezTech d151e151c2 [CI] fix zip inside zip 2026-07-20 10:14:55 +03:00
João Victor Amorim 472fc96a37
[AGC] Support the clamp modifier on packed f16 VOP3P ops (#460)
The VOP3P emitter rejected any packed op with the clamp bit set. Clamp
saturates each f16 output half to [0, 1] (and flushes NaN to 0, matching
RDNA), so games that emit clamped packed arithmetic fell back to a loud
emit failure.

Apply the saturation to the f32 result of each lane, before it is
narrowed back to f16. Because 0.0 and 1.0 are exact in both f32 and f16
and the clamp is monotonic, clamping in f32 and then rounding to f16
yields the same value as clamping the f16 result directly; for the fused
multiply-add the pre-narrowing value is the round-to-odd f32, which
preserves that equivalence through the final round-to-nearest-even. The
saturation uses ordered compares so a NaN result collapses to 0 without a
separate IsNan test.

Verification:
- The local exact-reference harness now also clamps: add, mul, and fma
  each compared against an f16-domain clamp reference (NaN -> 0, else
  [0, 1]) over directed boundary inputs and 24M random cases. 0
  mismatches, alongside the existing 34M unclamped fma cases.
- ShaderDump pk-f16 gains a clamped add and a clamped fma; all decode and
  emit.
- The exec program computes the pinned fma with clamp (both lanes exceed
  1.0, so each saturates to 0x3C00) and stores it at offset 28;
  GpuConformance checks it on device. All values match on an AMD Radeon
  RX 7700 XT.
2026-07-20 09:09:07 +03:00
Slick Daddy 33be88bdf9
memory: back the free pages of a partially-overlapping fixed mapping (#458)
A SCE_KERNEL_MAP_FIXED request whose window partially overlaps an
existing allocation was failing outright: AllocateAt reserves the whole
range in one all-or-nothing VirtualAlloc, which returns 0 on partial
overlap. The mapping call then returned NOT_FOUND while leaving the free
tail unmapped, so the guest faulted (0xC0000005) writing into it.

Add IGuestAddressSpace.TryBackFixedRange, which walks the range via the
host Query (VirtualQuery reports contiguous same-state runs) and fills
only the free sub-ranges, leaving already-backed pages untouched. This
matches the fixed-mapping contract on hardware. Route the fixed
reservation path through it via a new backPartialOverlap flag.

Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
2026-07-20 09:08:29 +03:00
kadu04t 184e24fbb6
PerGameSettings Null toggles (#453) 2026-07-20 01:30:13 +03:00
kuba 327018e80a
Encode linear-float flips to sRGB at present (#448)
PS5 float VideoOut buffers (A16B16G16R16F flips) hold linear scRGB
light where 1.0 is SDR white; hardware scan-out applies the display
transfer function. vkCmdBlitImage converts numerically only, so
raw-blitting a linear-float guest frame into a UNORM swapchain crushes
dim scenes to near-black.

Blit float flip sources through a cached swapchain-sized sRGB
intermediate (the sRGB store performs the linear->sRGB encode), then
raw vkCmdCopyImage the encoded bytes into the same-compatibility-class
UNORM swapchain image. Swapchains that are already sRGB keep the
direct blit (their store encodes), and swapchain formats without an
sRGB counterpart keep today's raw blit unchanged.
2026-07-20 01:29:38 +03:00
kuba 04557fd250
Refresh CPU-rewritten guest textures by write generation (#447)
* Track guest CPU write generations

* Refresh CPU-rewritten guest textures by write generation
2026-07-20 01:29:30 +03:00
Spooks 90c72ebecf
Fixes a Mutex Issue Preventing Some UE Titles From Booting (#451)
* Optimize guest import, memory, and pthread hot paths

* Fix UE adaptive mutex self-lock handling
2026-07-19 13:20:05 -06:00
Nekono 8ef5a54ee4
cpu: emulate AMD-only Zen 2 instructions in software (#449)
Handle immediate EXTRQ and INSERTQ as well as MONITORX and MWAITX when the host raises illegal-instruction faults. Add unit coverage for SSE4a bit-field semantics and preserve existing load-time patching.

Co-authored-by: zocomputer <help@zocomputer.com>
2026-07-19 21:57:42 +03:00
shadowbeat070 0c467e8c57
Add missing nids (#450)
* [Kernel] Implement clock_getres and the POSIX pthread_once alias

clock_getres (smIj7eqzZE8) was missing entirely. It reports 100ns, which
is the resolution clock_gettime here actually delivers via
DateTimeOffset.UtcNow, rather than claiming the 1ns a caller might
otherwise rely on. A null res pointer is accepted per POSIX.

pthread_once (Z4QosVuAsA0) needed no new logic: libKernel exports the
same routine under two NIDs and only scePthreadOnce (14bOACANTBo) was
registered. Shipped middleware links the plain name.

Both are imported by DOOM + DOOM II (PPSA21444): clock_getres blocked
party.prx from initialising, and pthread_once is used by libcohtml,
libPlayFabMultiplayer, party.prx and the eboot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 848f035827bb2b1217dd0859c5adbf66129ce3a5)

* [Libs] Implement sceAgcGetIsTrinityMode, NpReachability and Trophy2 info

Three exports DOOM + DOOM II (PPSA21444) imports and currently receives
unresolved-stub errors for.

sceAgcGetIsTrinityMode reports the base console this backend emulates. It
returns the flag in rax and writes no guest memory: the observed rdi at
the call site sits inside the AGC state block, immediately below the
shader handles the guest stores, so writing through it would corrupt live
state if that register is stale rather than an out-pointer.

sceNpRegisterNpReachabilityStateCallback accepts the callback and never
fires it, matching the existing sceNpRegisterStateCallback handling.
Reachability transitions only occur on a live PSN connection.

sceNpTrophy2GetTrophyInfo reports NOT_FOUND rather than success.
Succeeding requires filling SceNpTrophy2Details and SceNpTrophy2Data,
whose layouts are not confirmed here, and a title trusting zeroed details
would read an empty name and grade 0 as real data. NOT_FOUND is a
documented outcome callers already handle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 181286f6212105ba34790e7d1732b4b2e4a2eabf)

* [Kernel] Implement the POSIX libKernel exports titles link directly

libKernel exports many routines under both sce-prefixed and plain POSIX
NIDs, and shipped middleware links the latter. These nine are imported by
DOOM + DOOM II (PPSA21444) and had no registration at all.

Aliases onto existing implementations, identical argument order:
  mprotect (YQOfxL4QfeU), munmap (UqDGjXA5yUM), setsockopt (fFxGkxF2bVo)

New:
  getpagesize reports OrbisPageSize (16 KiB), not the host 4 KiB. An
  allocator rounding to the host value produces sub-page offsets that
  every mapping call here rejects for misalignment.

  pthread_rwlock_tryrdlock/trywrlock get a dedicated non-blocking core.
  They deliberately do not reuse TryAcquireBlockedRwlock, which
  decrements WaitingWriters -- correct only for a thread that previously
  incremented it. A fresh try never did, so reusing it would consume
  another thread's waiter count and let a queued writer be skipped.

  getsockopt reads back the three options this backend tracks (SO_NBIO,
  SO_REUSEADDR, SO_ERROR) and rejects the rest rather than returning
  success with an untouched buffer the caller would treat as real.

  send maps WouldBlock onto the existing net error path.

  inet_ntop converts AF_INET/AF_INET6 and returns the destination
  pointer per POSIX, failing rather than truncating when it will not fit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit 98c6851840a1342c1aa8c5a12ac1e10fd23b6554)

* [Kernel] Implement the POSIX mprotect, munmap and getpagesize aliases

mprotect and munmap forward to the existing sceKernelMprotect and
sceKernelMunmap; the argument order is identical, so they are plain
aliases rather than separate implementations.

getpagesize reports OrbisPageSize (16 KiB), the granularity this backend
maps and aligns against, not the host's 4 KiB. An allocator that rounded
to the host value would produce sub-page offsets that every mapping call
here then rejects for misalignment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [Kernel] Implement the POSIX _nanosleep symbol

libKernel exports nanosleep under two NIDs: yS8U2TGCe1A for the plain
name and NhpspxdjEKU for the underscore-prefixed _nanosleep that libc
conventionally provides alongside it. Only the former was registered.

Both are POSIX-side symbols, so this shares NanosleepCore with posix:
true - reporting failure as -1 plus errno rather than returning an
OrbisGen2Result the way sceKernelNanosleep does.

Not exercised at runtime: no title currently on this branch imports
_nanosleep, so the choice of error convention rests on it being the
same libc routine as nanosleep, not on observed behaviour.

* [Kernel] Implement the POSIX-named pthread aliases

libKernel exports each of these routines under two NIDs: a scePthread*
name and the plain POSIX name. Only the scePthread* half was registered,
so middleware compiled against POSIX headers linked an unresolved stub.

Adds the POSIX-named export for fourteen routines, each delegating to
the existing implementation:

  pthread_setprio               pthread_attr_setschedpolicy
  pthread_getschedparam         pthread_attr_setdetachstate
  pthread_attr_getschedparam    pthread_attr_setschedparam
  pthread_attr_getstack         pthread_attr_setinheritsched
  pthread_attr_get_np           pthread_attr_setguardsize
  pthread_attr_getstacksize     pthread_attr_getguardsize
  pthread_attr_getdetachstate   pthread_rename_np

Arguments are identical in both forms, and per the convention set by
scePthreadOnce's alias the POSIX name returns the same OrbisGen2Result
rather than translating to errno.

The equivalent POSIX names for mkdir, listen, accept and recv are
deliberately not included here. Those pair with sceKernelMkdir and the
libSceNet entry points, whose error convention differs from the POSIX
one, so they need a decision about error translation rather than a
straight delegation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 21:57:05 +03:00
Youss 9ff60abb9b
[Kernel] Clamp guest path traversal at the mount root (#437)
NormalizeMountRelativePath only stripped leading separators and swapped
slashes; it never resolved "." or ".." segments. The bare-relative fallback
in ResolveGuestPath did not even call it -- it combined the guest path
against the app0 root verbatim.

A guest path containing ".." therefore escaped its mount into the host
filesystem. Unreal Engine titles hit this constantly: their base directory
is <app>/binaries/<platform>, so they address content with "../../../"
prefixes that resolve back inside /app0 on real hardware. Here those walked
out of the game folder entirely -- The Invincible (PPSA06426) opened
"/app0/.." and enumerated the host's Downloads directory, listing unrelated
user files, and never located its own content tree.

Resolve "." and ".." while walking the segments and clamp the result at the
mount root, then route the bare-relative fallback through the same
normalizer. This both closes the sandbox escape and makes the engine's
relative content paths land where the title expects.

Verified on The Invincible: no resolved host path contains ".." any more,
and the title now enumerates its own content directories (content/paks,
content/movies, content/locale/*) instead of an unrelated host folder. No
regression on Dead Cells (PPSA15552): still reaches AGC rendering and
presents frames with zero mutex errors.
2026-07-19 20:34:00 +03:00
Youss 3ebfc56d4c
[PlayGo] Derive the installed chunk set from the pak files on disk (#438)
A title that ships no PlayGo sidecar was reported as a single-chunk
package. That is wrong for any package whose content is split across
chunks: the title is told everything past chunk 0 is not installed, even
though a locally dumped title has all of its data present.

The Invincible (PPSA06426) ships pakchunk0..8 and asks PlayGo which of
those are available. Receiving BAD_CHUNK_ID for chunks 1..8, it re-queried
scePlayGoGetLocus for the same chunk in a tight loop that never terminated
(observed ~1000 consecutive dispatches with identical arguments).

Discover the chunk ids from the pakchunk<N>-<platform>.pak files present
under the app0 root instead. Those N are exactly the chunks the package
has, so the answer is derived from the install rather than assumed. Chunk 0
is always included, so a title with no pak files at all keeps the previous
single-chunk behaviour, and ids outside the discovered set still return
BAD_CHUNK_ID so title-side chunk enumeration still terminates.

Verified on The Invincible: the discovered set is [0..8], matching the nine
pak files, and the GetLocus retry loop no longer occurs. No regression on
Dead Cells (PPSA15552): still reaches AGC rendering and presents frames.
The existing metadata-free contract test still passes -- its app0 fixture
has no pak files, so the discovered set stays [0].
2026-07-19 20:33:30 +03:00
Youss 73e8821d5b
[Kernel] Hand off mutex ownership directly to the head waiter on unlock (#439)
pthread_mutex_unlock cleared ownership (OwnerThreadId = 0) and only woke
the head waiter, relying on that woken thread to re-acquire the lock
itself. If the wake raced or was lost, the mutex was left "free but with
a queued waiter" — a state the fast-acquire path in PthreadMutexLockCore
explicitly refuses (OwnerThreadId == 0 && Waiters.Count == 0), so every
later locker, including the game's main thread, queued behind a head that
never advanced and the whole process wedged.

Grant the mutex to the head waiter directly inside unlock (the same
TryGrantMutexWaiterLocked hand-off the thread-exit cleanup already uses),
then wake it. The mutex is therefore never observable as free-with-waiter.

Verified against The Invincible (PPSA06426): forward progress jumps from
~3.5M to ~40M dispatched imports and the repeated unlock INVALID_ARGUMENT
errors disappear. No regression on Dead Cells (PPSA15552), which still
reaches AGC rendering with zero mutex errors.
2026-07-19 20:33:21 +03:00
StealUrKill bc51cc2c4d
Prevent invalid SaveData writes from damaging guest memory (#444)
Add an optional write monitor so the team can find future memory damage on each supported desktop system.
2026-07-19 20:27:17 +03:00
Nicola Pomarico d7f6e3f578
[Kernel] Implement sceKernelMapDirectMemory2 (#433)
The "2" variant of sceKernelMapDirectMemory was unimplemented, so titles
that call it (seen in Gex Trilogy) got an unresolved import that returned
an error the guest then used as a mapped address.

v2 inserts a memoryType argument ahead of v1's protection, shifting the
remaining arguments down one register and pushing alignment onto the
stack. Extract v1's body into a shared MapDirectMemoryCore and route both
exports through it; v2 reads its shifted arguments and the stack alignment
and accepts the memoryType (which only selects cache/GPU attributes this
HLE does not model per mapping, so it does not affect placement).
2026-07-19 14:35:40 +03:00
cse.aadi e56e74f960
Fix space-in-path game launching on Windows (#432) 2026-07-19 14:25:43 +03:00
kadu04t 0f224ec036
Gui Settings Null list Entries (#430) 2026-07-19 13:53:56 +03:00
kostyaff 85dc98dedc
test: add Fiber exports contract tests (13 tests) (#428) 2026-07-19 13:53:33 +03:00
wearr 5d7d8e0edd
[Kernel] add NID B5GmVDKwpn0 (pthread_yield) (#426) 2026-07-19 13:49:51 +03:00
wearr a60bfc9c83
[Kernel] Implement pthread semaphore exports (#424) 2026-07-19 04:18:16 +03:00
Berk 0b83b34cda
chore: bump version to 0.0.2-beta.4 (#423) 2026-07-19 03:42:52 +03:00
Adam salem 09812600a0
Add libc heap trace contract tests (#409) 2026-07-19 03:25:42 +03:00
João Victor Amorim 3005babab8
[AGC] Emit v_pk_fma_f16 with exact single rounding (#420)
Completes the fused-FMA slice deferred by the VOP3P first slice (#145).
v_pk_fma_f16 previously failed emission loudly because an f32
multiply-add followed by an f16 pack rounds twice; the pinned miss is
fma(0x4100, 0x7522, 0x04EA) = 0x7A6B fused vs 0x7A6A via f32.

The f32 product of two f16 values is exact, so only the addition needs
correcting: compute sum = RN(product + addend), recover the exact
residual with Knuth 2Sum, and if the sum is inexact with an even
significand, step one ulp towards the true value. That is round-to-odd,
and rounding the f32 result to f16 with round-to-nearest-even then
matches a true fused f16 FMA exactly (24 significand bits >= 11 + 2).
Inf/NaN inputs turn the residual into NaN, the ordered compare skips the
parity fix, and IEEE special behaviour passes through unchanged. The
op_sel/op_sel_hi/neg_lo/neg_hi source modifiers apply to src2 through
the existing operand path; clamp stays rejected like the other packed
ops.

Every op in the 2Sum chain is decorated NoContraction: without it the
AMD RDNA3 Windows driver folds the sequence, collapses the residual to
zero, and the midpoint case decays to the double-rounded result. This
was caught by running the emitted shader on a real device (see below).

Verification:
- A mirror of the emitted sequence was checked against an exact
  integer reference (every finite f16 is m * 2^-24, so a*b + c is an
  exact Int128 multiple of 2^-48, rounded once to f16 RNE) across 34M
  cases: directed midpoint pins, random sweeps over all operand
  classes, tiny-addend midpoint stress, subnormal products, and
  Inf/NaN propagation. 0 mismatches.
- ShaderDump gains a pk-f16 program covering all five packed opcodes,
  both fma modifier paths, and the pinned constants; all programs
  decode and emit.
- The executable exec program now computes the pinned fma and its
  negated-addend twin (0x7A6B7A6B / 0x7A6A7A6A, straddling an f16
  midpoint) and stores them at offsets 20/24; GpuConformance checks
  both on device. All values match on an AMD Radeon RX 7700 XT.
2026-07-19 03:24:42 +03:00
Nicola Pomarico 09bd4f028b
[Kernel] Implement sceKernelSyncOnAddressWait/Wake (#422)
libKernel's address-wait primitives were unimplemented, so every wait
returned immediately and guest runtimes that build spinlocks/queues on
top busy-spun forever. Implement them over the existing cooperative
block scheduler, keyed on the address, with a per-address wake
generation so a wait stays parked until a matching wake bumps it, and a
bounded self-heal deadline so a genuinely missed wake re-polls instead
of hanging.
2026-07-19 03:12:29 +03:00
ParantezTech 2bda253927 [script] added aerolib_catalog.py and docs/aerolib-catalog.md, renamed scripts/RELEASE-USE.md to docs/release-use.md 2026-07-19 01:33:15 +03:00
Berk 71e5912c75
[dotnet] remove lock files (#419) 2026-07-19 01:26:01 +03:00
Berk a030cb5a5d
Gpu runtime stalls (#410)
* [runtime] restore default GC mode

* [cpu] add string leaf stubs

* [ampr] allow concurrent reads

* [bink] keep guest decode path

* [kernel] streamline host memory access

* [shader] add scalar memory fallback

* [gpu] bound guest data pool

* [gpu] reduce queue stalls

* [video] stabilize guest resources

* revert lock file
2026-07-19 00:31:50 +03:00
Dafenx 336286e588
CPU: scan final TLS access pattern offset (#414)
Co-authored-by: Dafenx <196083014+Dafenxz0@users.noreply.github.com>
2026-07-19 00:07:32 +03:00
Berk cab001f265
[GUI] Fixes click the controller B/O close button to close the game (#415) 2026-07-18 23:56:56 +03:00
Berk bab965e394
[HLE] Add RandomExports HLE (#413) 2026-07-18 23:44:57 +03:00
Spooks daaeb6213e
Fix Massive Bug Preventing UE5 Titles From Booting (#406)
* Fix cross platform memcpy bug
2026-07-18 12:50:59 -06:00
Gutemberg Ribeiro 94153955b0
[Gpu] Metal backend: complete IGuestGpuBackend implementation on AppKit + Metal (#283)
* [ShaderCompiler.Metal] MSL translator core: dispatcher, EXEC model, compute stage

The Metal codegen backend, rebuilt on the merged backend-neutral
abstractions (replacing the pre-abstraction spike): consumes
(Gen5ShaderState, Gen5ShaderEvaluation) and emits MSL text; the renderer
owns MTLLibrary compilation, mirroring the emitters-produce-bytes rule
the Vulkan sibling documents.

The execution model mirrors Gen5SpirvTranslator: one invocation per GCN
lane (wave32 — natively the Apple simdgroup width), a typeless uint
register file with as_type<float> bitcasts, EXEC/VCC as per-lane bools
whose guest-visible mask registers materialize via simd_ballot, and the
same PC-dispatcher loop over basic blocks with the
SHARPEMU_SHADER_MAX_STEPS iteration guard and the dominating-scalar-
definition dataflow for buffer binding resolution. Unlike SPIR-V, MSL
permits shared prelude functions, so unaligned/subdword buffer access
is a range-checked device-uchar* helper instead of per-site inlining;
buffer byte lengths and the compute dispatch limit travel in one
reserved SharpEmuUniforms constant buffer (Metal has no OpArrayLength).

This first slice covers the compute entry point end to end: scalar/
vector ALU core (moves, int/float arithmetic, FMA family, shifts,
bitfield ops, min/max/med3, conversions, transcendentals with the Tau
scale on sin/cos), the full VCmp/VCmpx compare matrix writing VCC/EXEC,
the saveexec family, scalar compares and SCC-updating SOP2 forms, lane
ops (readfirstlane, mbcnt), VOP3 abs/neg/clamp/omod modifiers, scalar
memory, and raw global/buffer loads, stores, and atomics with EXEC
guards. Unsupported opcodes fail loudly with pc + mnemonic. SDWA/DPP,
typed format loads, LDS, images, and the pixel/vertex stages follow in
the next phases.

Tests live in their own self-contained project (the per-backend model:
depends only on the codegen under test): hand-assembled synthetic
fixtures drive the real decoder end to end, structural assertions and
golden-MSL comparisons run on every platform since translation is pure
text generation, and goldens regenerate via SHARPEMU_UPDATE_GOLDENS=1.

* [ShaderCompiler.Metal] Real-device runtime tests: compile + execute on the GPU

Lifts the spike's objc_msgSend LibraryImport harness (MTLDevice /
MTLCompileOptions with fast-math off, as a real Metal backend must
compile) onto the new translator contract: the guest data buffer binds
at index 0 and the SharpEmuUniforms constant buffer (dispatch limit +
buffer byte lengths) at index 1.

Three runtime tiers on hosts with a Metal device (no-op elsewhere so
Windows/Linux CI stays green): every fixture's emitted MSL must be
accepted by the OS runtime Metal compiler; the exec-store program must
produce bit-exact GPU results including the EXEC-masked store that must
not land; and a scalar countdown loop must iterate through the PC
dispatcher (s_cmp_lg_u32 + s_cbranch_scc1 across five round trips).
The loop fixture also fixes its own hand-assembly: s_sub_i32 sets SCC
to signed overflow, not result-nonzero, so the loop condition uses an
explicit compare.

* [ShaderCompiler.Metal] Phase 2: scalar/vector ALU parity with the SPIR-V translator

Ports the remaining ALU semantics from Gen5SpirvTranslator.Alu so the two
codegens cannot disagree on instruction behavior:

- Carry/borrow family (v_add_co/_ci, v_sub_co/_rev, v_subb/_rev) with the
  carry mask written to the VOP3 scalar destination or VCC, ANDed with
  EXEC; v_mad_u64_u32 with the 64-bit pair result and carry-out.
- Full SDWA support: byte/word source selects with sign-extension,
  integer abs/neg modifiers, and destination-select merge (zero-fill,
  sign-extend, preserve) into the previous register value.
- DPP16/DPP8: quad permute, row shl/shr/ror, mirror/half-mirror,
  broadcast and xor controls via simd_shuffle, bound-control and
  row/bank write-enable masks, fetch-inactive handling; DPP-predicated
  compares merge into VCC.
- Lane ops: readfirstlane from the first EXEC-active lane via
  ballot+ctz, readlane/writelane, permlane16/permlanex16.
- 64-bit scalar ops over SGPR pairs in real ulong arithmetic (logic
  family, shifts, bfe/bfm with width clamping, wqm quad expansion,
  cselect, mov, s_getpc) plus the B32/B64 saveexec families.
- Sopk forms decode the signed 16-bit immediate and s_cmpk compares the
  destination register; SOPC scalar compares including s_bitcmp0/1.
- Conversions: f16<->f32 via as_type<half>, pkrtz with round-to-zero
  mantissa truncation, pknorm via pack_float_to_{s,u}norm2x16, pk_u8
  byte insert, off_f32_i4 table, rpi/flr rounding; cube id/sc/tc/ma
  decision trees; v_cmp_class_f32; VCCZ/EXECZ/SCC readable as data.

Also fixes a real phase-1 bug the reference surfaced: fmamk/fmaak
sources arrive in natural order from the decoder, so all MAD/FMA forms
are fma(src0, src1, src2) — the previous operand swap computed
v1*v2+K for v_fmamk (should be v1*K+v2). The regenerated fmac golden
shows the corrected expansion, and mirroring the SPIR-V translator,
v_mul_u32_u24 is a full 32-bit multiply (only the hi/mad forms mask).

All 13 Metal tests pass including the real-GPU execution tier.

* [ShaderCompiler.Metal] Phase 3: typed format loads, LDS, and D16 subdword memory

Typed MUBUF/MTBUF loads convert through the descriptor's GFX10 unified
format at execution time, mirroring the SPIR-V translator: the prelude
bakes a 128-entry format table from the shared Gfx10UnifiedFormat
decoder (compiled shaders may be reused with new SRDs, so decoding must
stay dynamic), per-component layouts for the legacy DATA_FORMAT values
drive range-checked unaligned loads, NUM_FORMAT conversion handles
unorm/snorm (clamped at -1)/uscaled/sscaled/uint/sint/float including
f16 and the 10/11-bit unsigned mini-floats of 10_11_11 / 11_11_10, the
missing-component default is one in the format's domain, and dst_sel
swizzling comes from descriptor word 3. Format stores stay raw dword
stores like the reference.

LDS lands as 32 KB of threadgroup memory (gated on the program actually
using DS ops so occupancy is not taxed): ds_read/write b32/b64/b96/b128,
the write2/read2 pairs including st64 scaling, and ds_add_u32 as a
relaxed threadgroup atomic, with EXEC-guarded writes and the address
masked into bounds. Subdword loads/stores gain the D16/D16Hi variants
that merge into one half of the destination register (and shift the
source for high stores), classified the same way as the reference.

New GPU-executed fixture: an LDS round trip (write literal, s_barrier,
read back, store to the buffer) passes bit-exact on a real Metal device
alongside the existing tiers.

* [ShaderCompiler.Metal] Phase 4: pixel stage, images, and interpolation

The pixel entry points land with the same contract as the SPIR-V
translator (single-target and MRT forms, validated for unique guest
slots and dense host locations): the emitted fragment function takes a
stage_in struct carrying [[position]] plus the interpolated attributes
discovered from the program's V_INTERP controls, writes an output
struct with one [[color(hostLocation)]] attachment per binding typed by
its Float/Sint/Uint kind, seeds pixel-input VGPRs in SPI_PS_INPUT_ADDR
compact order from the fragment coordinate, keeps EXEC masking through
translation, and discards lanes that exit with EXEC off. Exports write
MRT targets per component under EXEC (disabled components keep their
previous value) including compressed half-pair exports; vertex-target
exports no-op until the vertex stage.

Images arrive as texture2d<float|int|uint> arguments (storage bindings
as access::read_write) with samplers alongside, classified from the
descriptor's unified format via the shared Gfx10UnifiedFormat decoder
and resolved per instruction with the same dominating-scalar-definition
scheme as buffers. The sample matrix covers implicit LOD, SampleL/Lz,
SampleB, SampleD gradients, PCF compare (manual reference<=texel,
broadcast r,r,r,1), per-lane texel offsets folded into normalized
coordinates by the selected mip extent (Metal sample offsets must be
constants), gather4 including compare and offset forms, clamped
ImageLoad/Mip, bounds-checked EXEC-guarded ImageStore, GetResinfo, and
A16 packed addresses / D16 packed data in both directions.

Graphics stages model LDS as per-invocation scratch (the SPIR-V
Private-array trick) instead of threadgroup memory. Wave ops keep the
invocation's real simdgroup in every stage — Apple fragment simdgroups
make that the same model as compute, where the SPIR-V translator
instead emulates a single logical lane; both round-trip EXEC masks
consistently.

The pixel fixture (interpolated attr0.xy plus inline constants exported
to MRT0) is golden-pinned, structurally asserted, and accepted by the
OS Metal compiler on a real device.

* [ShaderCompiler.Metal] Phase 5: vertex stage and fixed presenter shaders

The vertex entry point completes the four-entry-point contract: the
emitted vertex function takes fetched attributes as a stage_in struct
([[attribute(location)]], bound by the backend via MTLVertexDescriptor
from the reflected vertex inputs), returns [[position]] plus one
[[user(locnN)]] param output per export target 32..63 — unioned with
requiredVertexOutputCount so Metal's exact vertex-out/fragment-in
interface match succeeds, with unexported locations zero-filled —
seeds v5/v8 from [[vertex_id]]/[[instance_id]], intercepts buffer loads
the evaluator captured as fixed-function vertex inputs, and applies the
same EXEC-selected component rules to position/param exports (disabled
components default to 0,0,0,1) including compressed half pairs.

MSL vertex functions have no simdgroup attributes, so the vertex stage
models a single logical wave lane exactly like the SPIR-V translator's
graphics path: lane 0, ballot degrades to 0/1, and lane-shuffle ops
would fail Metal compilation loudly (no real guest vertex shader uses
them).

MslFixedShaders mirrors SpirvFixedShaders for the presenter surface:
the fullscreen-triangle vertex stage (position from the vertex index,
screen-space UV broadcast to every requested attribute location), the
copy/solid/attribute diagnostic fragments, and the output-free
depth-only fragment. Metal forbids "main", so each carries a stable
entry name.

The vertex fixture (constant position + one param export) is
golden-pinned and structurally asserted; it and all five fixed shaders
are accepted by the OS Metal compiler on a real device.

* [ShaderCompiler.Metal] Author static MSL blocks as template files

The prelude helpers (buffer access, ballot, tables), the format-load
conversion functions, and all five fixed presenter shaders move out of
AppendLine walls into Templates/*.msl embedded resources — real Metal
source with syntax highlighting and reviewable diffs — rendered by a
small {{placeholder}} substituter that fails loudly on any
unsubstituted token. Substitution points are deliberately few: the
stage-dependent ballot expression (vertex has no simdgroup attributes),
the baked GFX10 format table and layout cases, and the fixed shaders'
parameters. Per-instruction body emission stays programmatic, where a
template cannot express it.

Behavior-identical by construction: the golden files are untouched and
the whole suite — including the real-device execution and compile
tiers over the templated output — passes against them unchanged. The
.msl files carry no license headers (they would leak into every emitted
shader), so REUSE.toml annotates the Templates directory instead.

* [ShaderCompiler.Metal] Cover the MSL goldens in REUSE.toml

The golden files are verbatim emitter output regenerated by the test
suite; license headers inside them would either break the byte-exact
comparison or force the emitter to write SPDX text into every shader.
Annotate the Goldens directory like the Templates one.

* [ShaderCompiler.Metal] Address review: dominating-binding parity and harness binding indices

The buffer-binding fallback now mirrors the SPIR-V translator: a candidate
binding is accepted only when the descriptor registers hold the exact same
scalar definitions at the target PC as at one of the binding's own access
points (HasSameScalarDefinitions), instead of merely being non-conflicting
at the target. Resolutions are cached per PC like the reference.

The runtime test harness no longer hardcodes buffer indices 0/1 and a
20-byte uniforms blob: TryExecuteSingleThread takes the data/uniforms bind
indices, and ExecuteOrThrow derives them plus the uniforms size from the
compiled shader's GlobalMemoryBindings per the translator contract.

* [Gpu] Add the Metal guest-GPU backend: shader compilation and formats

First phase of the Metal backend behind the IGuestGpuBackend seam. The
backend compiles all three shader stages through Gen5MslTranslator and
exposes the guest render-target format table (mirroring the Vulkan table
case for case; guest format 9 maps to BGR10A2, the Metal layout matching
Vulkan's A2R10G10B10 pack). Wave64 compute is rejected with a clear error
until the two-pass emulation exists.

SHARPEMU_GPU_BACKEND=metal opts in on macOS; Vulkan stays the default on
every platform until the Metal presenter reaches parity. Presenter-side
methods fail loudly instead of dropping guest frames silently.

* [Gpu] Add the Metal presenter core: AppKit window, CAMetalLayer, CPU-frame path

The presenter opens an NSWindow hosting a CAMetalLayer and drives a manually
pumped NSApplication event loop, structured like the Vulkan presenter's
poll-and-render loop and posted onto HostMainThread the same way (AppKit
traps off the process main thread). All OS access goes through objc_msgSend
LibraryImport bindings declared locally — no windowing or binding packages on
this path, which is what keeps it NativeAOT-clean. Struct-returning ObjC
calls are avoided entirely so one calling convention works under Rosetta.

Presents CPU-produced BGRA frames and the splash through a fullscreen
triangle with a dedicated present fragment stage that flips V: with Metal's
y-up NDC the shared fullscreen triangle puts UV (0,0) at the bottom of the
screen while textures keep v=0 at the top. Frames letterbox via the viewport,
and nextDrawable paces the loop at presentation rate.

Guest-image submission now returns false (callers use their CPU-readback
fallback, which the presenter can show); draw and compute submission still
fail loudly pending later phases.

* [Gpu] Complete the guest-GPU seam: lift the AGC bypass surface onto the backend

The abstraction left AGC and VideoOut calling VulkanVideoPresenter statics
directly for guest work ordering (EnterGuestQueue, SubmitOrderedGuestAction,
SubmitOrderedGuestFlipWait, WaitForGuestWork), guest-image lifecycle (initial
data seeding, writes, fills, extents, upload tracking), the texture-content
cache probe, guest memory attachment, storage-offset alignment, perf
counters, and presenter close. With a non-Vulkan backend selected those
calls silently hit a never-started Vulkan presenter.

All of it now crosses IGuestGpuBackend: the Vulkan backend delegates to the
existing presenter statics (no behavior change), and the Metal backend
answers exactly like a presenter that is not running (sequence 0, image
unknown), which keeps callers on the same inline/CPU fallbacks they take
today. TextureContentIdentity moves to the seam types, and the bounded
AGC-to-presenter transfer pool becomes the backend-neutral GuestDataPool
(one pool by necessity: the AGC layer rents, the presenter returns).

AgcExports snapshots the backend's offset alignment once — it was a const
before and is read in per-draw loops (shader-key hashing, offset rounding).

* [Gpu] Metal guest work queue and guest images: ordered flips, writes, fills, blits

Mirrors the Vulkan presenter's execution model. AGC submissions become work
items consumed by the render loop in logical-guest-queue order: FIFO within
each guest queue, ready queues scheduled round-robin, completion tracked as
a contiguous sequence plus an out-of-order set, and producer backpressure
(count and payload caps) that consumer-enqueued follow-ups bypass to avoid
self-deadlock. The drain is budgeted (12ms, 256 items) so a backlog cannot
starve the Cocoa event pump or the present.

Guest images are Metal textures keyed by guest address, created on first
use from the registered display-buffer format tag (byte-identical encoding
to the Vulkan backend) and seeded once from pending initial data or guest
memory, since PS5 render targets alias guest memory. Coherence mirrors the
Vulkan design: DMA-style writes swap in a freshly written texture (never
mutating one an in-flight present may sample), fills clear through a
hazard-tracked render pass, and same-extent blits copy on the GPU.

Ordered flips capture the named image into an immutable version at their
exact queue position, so later work cannot change the frame a flip
selected; flip waits complete by queue position alone. Presentation picks
the newest ready queued guest frame (retiring superseded captures),
re-resolving mutable address-keyed textures at encode time so a write swap
never leaves a stale handle.

* [Gpu] Metal translated draws: pipelines, render state, bindings, write-back

Executes the seam's translated-draw surface on Metal. Offscreen, depth-only,
and storage draws are ordered guest work rendering into guest-addressed
images (published targets register as flip sources exactly like the Vulkan
backend); onscreen draws and recognized fixed-function draws ride the
presentation and render at present time into a pooled target.

Pipelines are built from GuestRenderState and cached by shader identity plus
a state hash: guest CB blend factor/op codes, write masks (bit-reversed for
MTLColorWriteMask), depth ZFUNC (bit-identical to MTLCompareFunction),
vertex attribute formats decoded from the same guest (dataFormat,
numberFormat) table the Vulkan backend uses, and RDNA 2:10:10:10 mapped to
Metal's R-low-bits 1010102 layout. Guest viewports pass through unchanged —
Metal accepts the negative heights PS5 games program, which is also how the
Vulkan backend inherits its orientation. Rect lists draw as 4-vertex strips;
Metal has no triangle fans, so those degrade to lists with a one-time warn.

Bindings follow the Gen5MslTranslator contract: global buffers at their flat
slot on both stages, SharpEmuUniforms (dispatch limit + buffer byte lengths)
after them, textures/samplers at the image slots with samplers decoded from
the raw guest descriptor words, and vertex streams at slot 26+ so they never
collide. Writable global buffers write back to guest memory before the work
item completes, preserving the CPU-visible GPU-write ordering point that
WaitForGuestWork promises. Feedback reads of a live render target sample a
blit snapshot; pooled guest data returns to GuestDataPool after upload.

Known simplifications for follow-up: textures upload a single mip level, and
the texture-content cache stays unclaimed (IsTextureContentCached=false)
until write-tracker-driven eviction exists, trading upload bandwidth for
correctness.

* [Gpu] Metal compute dispatch: the last seam gap

Guest compute dispatches are ordered guest work like draws. The uniforms
contract carries the per-axis dispatch limit (explicit thread counts when
the guest supplied them, groups x threadgroup size otherwise) so the
kernel's bounds guard clamps the overshoot threads of the last threadgroup;
threadgroup dimensions come from the translated shader, which bakes them at
compile time. Compute pipeline states cache per shader handle.

Storage images are shared live through the guest-image registry: a
dispatch's writes are visible to later draws, blits, and flips of the same
address, the address registers as a flip source at submit, and writer
sequences keep presentation waiting on exactly the work that produced the
frame. Writable buffers write back to guest memory before the work item
completes — the CPU-visible ordering point the returned sequence promises
through WaitForGuestWork.

Metal has no dispatch-base; nonzero base groups execute without the offset
behind a one-time warning until the emitted kernel grows base support.
SHARPEMU_SKIP_ALL_COMPUTE=1 skips all dispatches for hang isolation, same
as the Vulkan backend. With this the Metal backend implements the entire
IGuestGpuBackend surface — nothing throws.

* [Gpu] Address review: real bytes-per-pixel in guest-image uploads

Guest-image uploads hard-coded 4 bytes per texel, which mis-strided
Rgba16*/Rg32Float/Rgba32Float images and, in the guest-memory seed and
storage-snapshot paths, could make replaceRegion read past the managed
buffer. Texel width now comes from the pixel format, and
ReplaceTextureContents clamps the row count to what the source buffer
actually holds, so no caller can overread regardless of pitch and format.
RGBA8 initial data seeds only 4-byte-texel images; wider formats seed from
guest memory, whose layout is the image's native one. Extent byte counts
use the real texel width too.

Also restores the reference's comment on the deliberate single-item
backpressure admit: with no payload outstanding, refusing an oversized item
would wait forever since nothing is left to drain.

* [ShaderCompiler] First real-game fixes: SSendmsg no-op, scalar-state buffer declaration

Bring-up against a real title (2D engine, NGG shaders) found every draw
rejected at translation: RDNA2 NGG shaders bracket their exports with
s_sendmsg (GS_ALLOC_REQ/DEALLOC) to reserve hardware export space, and
neither translator handled the opcode — it fell through to the scalar-ALU
guard and failed with 'missing scalar destination'. Both translators now
treat SSendmsg as a no-op alongside SNop/SWaitcnt: exports are translated
directly, so the hardware message is moot. This was a shared gap, not a
backend one; the Vulkan path would reject the same shaders.

With translation unblocked, the OS Metal compiler rejected the emitted MSL:
the body reads the per-dispatch scalar-state buffer (initial SGPRs plus
per-binding byte biases) as b{initialScalarBufferIndex}, but the kernel
signature only declared the stage's own global bindings, so the name never
existed. The signature now declares it (const device — it is only read) at
its flat slot.

The presenter also logs one line when it first presents real content,
making 'window up but nothing shown' diagnosable from the log alone.
Verified: the title goes from 100% draw misses and a black screen to
~58k translated draws per minute and 4K frames presenting.

* [Gpu] Metal presenter: NSTimer-driven render loop under [NSApp run]

Replaces the hand-pumped event loop with a real running main loop. The
presenter now creates the NSApplication, orders the CAMetalLayer-backed
window on screen, and calls [NSApp run] so Core Animation's run-loop observer
actually commits presented drawables to the window server — without a running
loop the layer never composites and the window stays black regardless of what
is rendered into the drawable.

The per-frame work moves into RenderFrame, driven by a repeating NSTimer on
the main run loop (a tiny NSObject subclass whose onFrame: is an
UnmanagedCallersOnly callback, registered via the ObjC runtime — no binding
package). CADisplayLink is the natural choice and was tried first, but its
callback never fires in this process; proven in isolation against a bare
AppKit harness where a timer fires and composites and the display link does
not — the emulator runs as x86-64 under Rosetta and the display-server-backed
link is not serviced there. nextDrawable still blocks to the display, so the
timer only needs to keep up, not pace precisely.

Also fixes window sizing (the fixed 1280x720 window was being sized from the
guest 4K display mode, which macOS clamps while the layer keeps 4K geometry —
nothing visible), makes the metal layer the view's backing layer (wantsLayer
before setLayer) with an explicit frame, and stops both the AppKit loop and
the CFRunLoop on window close.

* [Gpu] Metal draws: normalize inverted viewports, resolve flips to drawn content

Two correctness fixes surfaced bringing a real title up. Guests program
Vulkan-style negative-height viewports for y-up rendering; Metal's NDC is
already y-up and rasterizes nothing for a negative height, so the viewport is
converted to the equivalent non-inverted rect (origin shifted, height
negated) with the same on-screen mapping.

Ordered flips now prefer produced content. A flip names the display buffer's
start address, but games render into the pixel surface past the buffer's
metadata block; the resolver takes the exact-address image when GPU work
wrote it, else the nearest same-extent GPU-written image within the buffer's
plausible metadata window, else the exact-address image even if only
seeded — so a flip presents the drawn frame rather than an empty seed. A
GpuWritten flag on guest images (set by draws, writes, blits, and dispatch
storage) distinguishes produced content from a speculative guest-memory
seed.

* [ShaderCompiler] Metal translator: per-stage uniforms slot, VCC/EXEC as data, exit branches

Three correctness fixes found by running a real game against the Vulkan
backend's behavior:

- Gen5MslShader carries UniformsBufferIndex: each stage emits its
  SharpEmuUniforms argument at globalBufferBase + totalGlobalBufferCount,
  and stages sharing a draw can disagree, so the presenter must bind the
  buffer per stage (Metal API validation: "missing Buffer binding at
  index 7 for sharpemu_uniforms").
- VCC (s106:s107) and EXEC (s126:s127) live in the scalar register file
  as raw 32-bit values with the per-lane bools as synced views. Programs
  legally park plain data in VCC (s_buffer_load into s[106] and then
  v_rcp_f32 of it); the bool-only model returned ballot masks instead.
- A branch to (or past) the program's end is an exit, matching the
  SPIR-V translator: sprite alpha-kill shaders use this to skip their
  tail and were rejected ("branch target outside program"), silently
  dropping every draw that used them.

Also: pixel-stage ballots use the per-lane form (this thread's own bit),
since simd_ballot is undefined inside the divergent dispatcher loop, and
v_readfirstlane returns the lane's own value under that model.

* [Gpu] Metal presenter: per-stage uniforms bind, keyboard input, perf overlay, title parity

- Bind SharpEmuUniforms at each stage's declared slot (see the paired
  translator change); one shared index left the vertex stage's slot
  unbound, zeroing its bounds-checked loads and killing interpolants.
- Vertex attribute byte offsets move onto the vertex descriptor (buffers
  bind at zero) and join the pipeline cache key, which they were silently
  missing from once baked into the pipeline.
- Unresolvable draw textures log a throttled warning instead of silently
  binding nothing.
- Keyboard input: an NSView subclass records keyDown/keyUp and feeds the
  POSIX host-input seam with a Windows-VK to macOS-keycode map covering
  the keys pad emulation polls; SHARPEMU_METAL_AUTOKEY scripts key
  presses for headless runs.
- Perf overlay (F1) drawn like the Vulkan presenter: CPU-rasterized
  panel uploaded to a small texture and composited with the present
  pipeline, with RecordPresent/RecordDraw feeding real numbers.
- Window title gains the selected GPU suffix and refreshes when the
  guest registers its application name; the layer is marked opaque so
  guest alpha never reaches the compositor.

* [Audio] Quiet sceAudioOutOutput on ports disposed by host shutdown

Closing the window disposes audio ports while guest audio threads are
still draining their last buffers; every remaining output then failed
the port lookup and logged a WARN per buffer (~190/s) until process
exit. Report success for missing ports once shutdown has begun; a bad
handle during normal operation still returns INVALID_ARGUMENT.

* [ShaderCompiler] Metal graphics stages model a single logical wave lane

The pixel stage used the real thread_index_in_simdgroup with all-ones
ballots while the vertex stage modeled lane 0 with 1-bit ballots, and
VReadlaneB32 still emitted a real simd_shuffle — reading another
fragment's register. Metal leaves simdgroup ops undefined inside the
divergent while(active){switch(pc)} dispatcher (empirically they
corrupted EXEC reconstruction), so graphics stages cannot use them.

Unify vertex and pixel on the SPIR-V translator's no-subgroup fallback:
one logical wave lane (lane 0), ballots degrade to bit 0, and the
shuffle-select family (readlane, readfirstlane, DPP16/DPP8 selects,
permlane16) resolves to the lane's own value. Writelane keeps the
lane-compare against the constant lane, matching the reference
fallback. Compute is untouched: its threads map one-to-one onto real
simdgroup lanes and still shuffle for real.

Entry parameter lists now always emit trailing commas and are closed by
one helper, so stage-specific trailing parameters no longer dictate
ordering.

* [ShaderCompiler] Metal compute mirrors the SPIR-V translator's wave semantics

Compute threads map one-to-one onto real simdgroup lanes, so restore
real simd_ballot for the compute prelude (the per-lane form was a
graphics fix that swept compute along) — masks parked in VCC/EXEC now
hold each lane's actual bit, and mbcnt/cndmask/saveexec read real
masks. VReadfirstlaneB32 broadcasts from the first guest-active lane
(ballot of EXEC, then ctz), matching the SPIR-V translator's explicit
first-active-lane broadcast rather than SPIR-V BroadcastFirst's
first-host-active semantics.

The wave64 gate moves into the translator and only rejects programs
that contain wave-sensitive operations (the SPIR-V translator's
subgroup-usage predicates: shuffle family, readfirstlane, wave control,
mbcnt, or VCC/EXEC operands). A wave64 kernel without them executes
identically per-thread on 32-wide Apple simdgroups, so it now
translates instead of being dropped.

* [Gpu] Metal draw textures resolve like the Vulkan presenter

Sampling a live guest target previously required the exact current
image at the descriptor's address; anything else silently bound
nothing. Mirror the Vulkan presenter's resolution chain:

- A descriptor naming a guest depth target's write or read address
  samples the depth image (identity channel select). Depth32Float
  cannot blit to a color format, so the ordered snapshot round-trips
  through a private staging buffer into an R32Float texture.
- Replacing a render target at the same guest address (new extent or
  format) retires the old image into a bounded variant cache instead of
  releasing it, and resolution scores the current image plus variants
  by descriptor match — exact extent over view format over
  initialization, active image breaking ties. Larger images qualify
  only for tiled descriptors, matching IsCompatibleGuestImageAlias.
- The throttled unresolved-texture warning remains the detector for
  anything the chain still cannot resolve.

* [ShaderCompiler] Document the Metal translator's wave-size model

Audit outcome for wave64 fidelity, no behavior change: every B64 mask
op, saveexec, and VCCZ/EXECZ test already reads and writes the full
register pair, lane indices never exceed 31 by construction, and the
GPU-executing runtime tests cover 64-bit exec save/restore. Record the
model in the class header.

* [Gpu] Plumb CB_BLEND constant color through both backends

The CONSTANT_COLOR / CONSTANT_ALPHA blend factors were mapped by both
backends but nothing ever supplied the constant, so any draw using them
blended against transparent black. Decode CB_BLEND_RED..ALPHA (the
constants existed unused) into GuestRenderState.BlendConstant, set it
dynamically per draw on both sides: Vulkan declares the blend-constants
dynamic state and calls CmdSetBlendConstants beside the viewport,
Metal calls setBlendColorRed:green:blue:alpha: in EncodeRenderState.

* [Gpu] Metal per-draw uploads bump-allocate from shared arena pages

Every draw created one MTLBuffer and one managed copy per binding
(padded guest globals, uniforms, vertex and index bytes), which
dominated allocation churn at hundreds of MB/s of garbage and held the
guest flip rate well under the display rate. Uploads now bump-allocate
256-aligned slices from 8 MiB shared-storage arena pages and bind by
offset; pages recycle once the last command buffer that referenced
them reports completion, polled at each drain so the ObjC interop
stays block-free.

Write-backs carry the slice's data pointer directly (the page outlives
the command buffer the caller waits on), the alignment-bias contract is
preserved by placing data at the bias inside its slice, and the padded
copies, per-draw buffer releases, and per-draw byte arrays are gone.

In-game on the test title: allocation rate ~795 to ~564 MB/s (the
remainder is the AGC-side per-draw guest snapshots), GC per stats
window ~30/30/17 to 16/16/15, CPU ~150 to ~131%. Metal validation
stays clean.

* [Gpu] Metal draw-texture cache: skip per-draw guest texel copies

Mirror the Vulkan presenter's identity-keyed texture cache: once the
render thread decodes a draw texture, the AGC submit thread skips the
guest-memory read/detile/copy for that identity entirely (the generic
IsTextureContentCached hook, which the Metal backend previously
hardcoded to false) and the render thread serves the cached MTLTexture
without re-uploading. GuestImageWriteTracker write-protects the source
pages; a guest CPU write evicts the entry at the next drain, and the
skip/eviction race self-heals by reading the texels directly.

Eviction differs from Vulkan in one deliberate way: dirty entries are
collected by address rather than identity, since ConsumeDirty clears
the flag on first read and several identities (same texels, different
samplers) can share one address.

Dreaming Sarah in-game on an M5 Max: guest flips 47 -> 60 (display
rate, matching Vulkan), ALLOC 564 -> 41 MB/s, gen0 GC 16 -> 5 per
second, CPU 131% -> 72%. Metal API validation clean; all 25 shader
compiler tests pass.

* [Gpu] Metal snapshot pool: recycle feedback-read textures and staging

Feedback reads created and destroyed an MTLTexture per draw (and for
depth sampling a private staging MTLBuffer too). Pool both with the
upload-arena lifecycle: acquisitions are tagged with the command buffer
that samples them at commit, and return to a bounded free list once it
reports completion. The command queue is serial, so the earlier
snapshot-blit command buffer is necessarily complete by then as well.

Dreaming Sarah renders correctly in-game; Metal API validation clean;
all 25 shader compiler tests pass. (The depth-sample path is exercised
only by inspection — no testable title samples depth yet.)

* [Gpu] Metal batched guest commands: one command buffer per drain

Draws and compute dispatches encode into a shared batch command buffer
committed once per drain instead of one commit per work item, mirroring
the Vulkan presenter's batched guest commands. Ordering inside the
batch is by encoder sequence: draw textures are now pre-resolved before
the consuming render or compute encoder opens, so feedback-read
snapshot blits encode into the batch (after the passes that rendered
the source) rather than committing ahead of them in separate command
buffers. Flips, image writes/blits, ordered actions, CPU-visible
write-backs, and every drain exit flush the batch first, preserving
the serial-queue ordering and WaitForGuestWork contracts.

Dreaming Sarah in-game on an M5 Max: CPU 72% -> 59% at a steady 60
guest flips; Metal API validation clean; all 25 shader compiler tests
pass.

* [Gpu] Metal vertex streams: share buffer slots, reject overflow gracefully

void Terrarium aborts with '-[MTLVertexAttributeDescriptorInternal
setBufferIndex:]: buffer index (31) must be < 31': every vertex
attribute got its own buffer slot from base 26, so six streams walk
past Metal's last vertex-stage buffer index (30) and the framework
assertion kills the process (reported by vladdenisov on PR #283).

Attributes of an interleaved vertex arrive from AGC as one stream
each, all reading the same guest buffer — assign slots by unique
(base address, stride, length) so those share one slot and one
upload. A draw whose unique streams still overflow the range is
skipped with a throttled warning instead of aborting. The assigned
slot keys the pipeline cache alongside the attribute offset, since
aliasing changes the baked vertex descriptor.

Dreaming Sarah renders correctly in-game at 60 flips with Metal API
validation clean; all 25 shader compiler tests pass. (void Terrarium
itself is not testable here — no decrypted copy.)

* [Gpu] Metal: drain guest work on enqueue, not only at render ticks

The Vulkan presenter's render loop is pulsed when guest work arrives
and waits at most a few milliseconds; the Metal render loop drained
guest work only inside its NSTimer tick, so every guest submit-then-
wait round-trip (release-mem labels, event writes, CPU-visible write-
backs) cost up to a full frame interval. Games that chain several such
waits per frame crawl: void Terrarium ran at 14 guest flips against
Vulkan's display rate, and input-to-effect latency suffered everywhere.

Enqueueing guest work now schedules a coalesced onGuestWork: message
onto the main run loop via performSelectorOnMainThread (block-free,
matching the NSTimer trampoline pattern), which drains the queue
immediately. A producer blocked on a full queue schedules the same
wake before waiting. void Terrarium's title menu: 14 -> 59 flips/s;
Dreaming Sarah unchanged at 60 with validation clean.

* [ShaderCompiler] Metal samplers: per-stage compact slots, not texture slots

Sampler argument indices copied the global texture slot (image binding
base + index), but Metal exposes only 16 sampler slots per stage
against 31 texture slots — a draw whose stages sample more than 16
images total emitted [[sampler(16+)]] and the MSL failed to compile
('sampler attribute parameter is out of bounds'), dropping the draw
(void Terrarium's in-game scenes).

Samplers now count sampled (non-storage) images from zero within each
stage, and Gen5MslShader carries the image-index -> sampler-slot map
plus the stage's image binding base so the presenter binds each
stage's samplers exactly where its shader declared them. A stage that
samples more than 16 images fails translation loudly. All 25 shader
compiler tests pass; goldens unchanged (single-texture fixtures keep
sampler 0).

* [Gpu] Metal draw textures: native guest formats, BC blocks, channel select

The draw-texture path assumed every texture was RGBA8: created
Rgba8Unorm, uploaded 4 bytes per pixel, and rejected anything whose
texel copy was smaller than W*H*4 as undersized. Games shipping
BC-compressed atlases (void Terrarium's entire in-game art) rendered
black, and because the rejected textures were never created they were
never content-cached — the AGC layer re-read and re-detiled megabytes
per draw (1.6 GB/s allocation, gen2 collections every second, 8 guest
flips).

Map guest texture formats to Metal case for case with the Vulkan
table (BC1-BC7 upload raw blocks — Mac-family GPUs sample them
natively — plus the 8/16/32-bit linear formats), size expectations
with the same block-aware byte math AGC uses, and honor the
descriptor's DST_SEL channel select through the texture swizzle,
mirroring Vulkan's component mapping. Unmapped codes keep the RGBA8
fallback.

void Terrarium now reaches gameplay past New Game: 49-54 guest
flips (from 8), no undersized-texture warnings, validation clean.
Dreaming Sarah unchanged at 60. All 25 shader compiler tests pass.

* [Gpu] Metal feedback reads: one snapshot per content version

Every draw sampling a live guest image blitted a fresh full-texture
snapshot, so compositing games that sample their render target on
most draws (void Terrarium: ~100 of ~105 draws per frame) pushed
gigabytes per second of blit traffic through the driver.

Guest images now carry a content version, bumped by every draw that
targets them, image write, blit destination, storage dispatch, and
guest-memory seed. The feedback-read path reuses one cached snapshot
until the version moves, so the blit happens per content change
instead of per draw. The image holds the snapshot's retain; consuming
command buffers keep replaced snapshots alive until they complete,
and retire/replace/write paths release the cache with the image.

void Terrarium in-game: 49 -> 58 guest flips at higher draw
throughput (Vulkan reference runs the same scene at 17-20 fps).
Dreaming Sarah unchanged at 60; validation clean; 25/25 tests pass.

* [Core] Pre-visit tracked texture pages before managed guest writes

A managed write into a page the guest-image write tracker has
protected dies with a fatal AccessViolation: the runtime surfaces
SIGSEGV in managed code as an exception before the resumable signal
bridge can restore access, unlike native guest stores which recover
through TryHandleWriteFault. Dead Cells crashed exactly there — an
AGC release-mem label write (CpuContext.TryWriteUInt64 on the render
thread) landing on a page the texture cache tracks.

TryWrite now calls GuestImageWriteTracker.NotifyManagedWrite up
front, unprotecting and dirtying any tracked pages in the span before
the copy — the hook existed for precisely this but had no callers.
Since this puts the tracker on every managed guest-write path, the
range snapshot now carries its overall bounds (one immutable object,
so the intersection test is always consistent with the array), letting
the common no-texture-pages case reject in a few instructions.

Dead Cells no longer crashes; Dreaming Sarah and void Terrarium
unaffected; all 25 shader compiler tests pass.

* [VideoOut] Name the active GPU backend in the macOS window title

macOS can run either backend — Vulkan through MoltenVK or native Metal
via SHARPEMU_GPU_BACKEND — so the window title now ends with the one in
use, e.g. "... · Apple M5 Max (Metal)" or "(Vulkan)". The suffix is
appended in SetSelectedGpuName (the single point both presenters call
to fold in the GPU name) and gated to macOS, so Windows and Linux
titles are unchanged. The name comes from a new BackendName on the
guest-GPU seam.

* [Gpu] Metal window: resizable, native full-screen, live drawable sizing

Add NSWindowStyleMaskResizable so the window can be dragged to any size
and set NSWindowCollectionBehaviorFullScreenPrimary so the green button
enters native full-screen instead of zooming. CAMetalLayer does not
track its drawable size to bounds on its own (even as a view's backing
layer), so the render loop matches drawableSize to the layer's current
bounds x contentsScale before each nextDrawable — a no-op on the common
unchanged tick. The present pass already aspect-fit letterboxes into the
drawable, so any window aspect ratio scales the frame without distortion.

Reading -bounds needs the x86-64 stret ABI for its 32-byte CGRect
return, added as SendStretRect. Verified live: drag-resize and
full-screen both scale correctly with Metal API validation clean.

* [ShaderCompiler] Metal wave64 compute: emulate cross-lane ops via scratch bridge

Replace the wave64 loud rejection with emulation, mirroring the SPIR-V
translator. A 64-lane guest wave is two 32-wide Apple simdgroups
co-resident in one threadgroup (Metal packs thread_index_in_threadgroup
0-31 into simdgroup 0, 32-63 into simdgroup 1), so sharpemu_lane becomes
thread_index_in_threadgroup & 63 and cross-lane ops that span the full
wave rendezvous the two halves through threadgroup scratch:

- ballot into EXEC/VCC/SGPR pairs: each half's simd_ballot is written to
  its scratch slot, a threadgroup_barrier syncs, and all lanes recombine
  the 64-bit mask into the low/high register pair (centralized in
  EmitBallotStore, which the wave32 path shares).
- read-first-lane: broadcasts the lowest active lane's value across both
  halves through a scratch slot (EmitWave64ReadFirstLane).
- mbcnt lo/hi: 64-lane thread-mask math (no cross-lane op, just correct
  per-lane masks; lanes >= 32 would overflow a 32-bit shift, so split).

The barriers are safe because the guest's scalar PC keeps all 64 lanes
lockstep through the dispatcher. Scope matches the SPIR-V reference: the
scratch is indexed by half, so correct for a one-wave (64-thread)
workgroup, and readlane across halves stays a 32-wide shuffle. Wave-
agnostic wave64 kernels still translate per-thread unchanged.

Verified on the real GPU (MetalRuntimeTests): the emitted wave64 MSL
compiles, and a 64-lane dispatch runs through the bridge barriers
without deadlocking, returning the broadcast value. All 27 tests pass;
Dreaming Sarah (60/60) and void Terrarium (in-game, 58 flips) show the
shared wave32 ballot path is unaffected.

* [Gpu] Metal samplers: bind through an argument buffer, lifting the 16-slot cap

Metal exposes only 16 direct [[sampler(N)]] slots per stage, but real
shaders sample more (void Terrarium's scene shader: 17 images) and were
dropped at translation. Route samplers through a per-stage argument
buffer instead: the MSL declares a Gen5Samplers struct (one sampler per
sampled image, [[id(N)]]) taken as constant& at a buffer slot past the
stage's globals/uniforms/scalar-state, and the runtime writes each
sampler's Tier 2 gpuResourceID into an arena slice bound there. Textures
stay on direct [[texture(N)]] slots (31 is enough). One sampler per
image keeps them distinct, matching the SPIR-V/Vulkan path — no dedup,
so no wrong-sampler artifacts.

Verified argument buffers lift the limit on Apple Silicon (20-sampler
pipeline probe). Dreaming Sarah renders correctly at 60/60 with Metal
API validation clean; the void Terrarium scene shader that exceeded the
limit now compiles and runs (draws 74 -> 102/frame); all 27 shader
compiler tests pass, goldens unchanged (fixtures sample nothing).

* [Gpu] Metal: Shared storage for CPU-populated, GPU-sampled textures

The MTLTextureDescriptor default is Managed, which on unified memory needs
an explicit host->device sync we never issue after replaceRegion, so the
GPU can sample stale texels. These textures are CPU-uploaded and GPU-read,
so Shared (coherent, no sync on Apple Silicon) is the correct mode.

* [HLE] Add missing AGC/AudioOut/Pad exports blocking Unity+FMOD titles

Four exports were unresolved and hard-stalled GPU/audio/input init in
Unity titles (Lunar Lander Beyond froze there before opening VideoOut):

- sceAgcDriverSetTFRing / sceAgcDriverSetHsOffchipParam: tessellation-ring
  and hull-shader off-chip config. We translate shaders directly, so these
  only need to report success for init to proceed.
- sceAudioOutGetPortState: report a connected primary output at full volume.
- scePadDeviceClassGetExtendedInformation: report a standard pad (no special
  peripheral) so device-class probes resolve.

Generic HLE, backend-agnostic (helps the Vulkan path equally).

* [VideoOut] RegisterBuffers2: mask the 32-bit category, accept COMPRESSED

sceVideoOutRegisterBuffers2's category is a 32-bit SceVideoOutBufferCategory
passed on the stack, but we read the full 64-bit slot — whose upper word
carries stale GNM magic (0xC0DEC0DE...) the caller never cleared. The old
check then rejected every call as INVALID_VALUE, so buffer registration
failed and no frame ever presented. Mask to 32 bits and accept both
UNCOMPRESSED (0) and COMPRESSED (1); we present either identically.

Fixes Lunar Lander Beyond reaching its window (now presents 3840x2160).

* [HLE] Stub sceAudioPropagation (3D-audio) so Astro Bot boots past its assert

Astro Bot hard-crashed right after the splash: it calls
sceAudioPropagationSystemQueryMemory during audio init, and because the
whole libSceAudioPropagation module was unimplemented the call failed, so
the game asserted (AudioPropagationContext.cpp:43) and executed int 0x41 to
abort — an unrecoverable trap that kills the process.

We don't model acoustic propagation (geometry-driven reverb/occlusion is a
quality feature, not a correctness gate). The API is placement-style, so
QueryMemory reports a buffer size and the rest succeed as no-ops: the system
lives in the caller's own buffer. All 39 entry points stubbed; the game now
boots past the assert to the presenter. Backend-agnostic HLE.

* [Kernel] pthread_cond_wait: don't spuriously EPERM an untracked mutex

pthread_cond_wait/timedwait required our host-side mutex tracking to show
the calling thread as the owner, else it returned EPERM. But libkernel's
uncontended mutex fast-path locks the mutex word in guest memory directly,
without an HLE call, so we often never observe the lock and see owner==0.

Real pthread_cond_wait requires the caller to hold the mutex but does not
verify it for normal mutexes, so EPERM here is doubly wrong: it spins the
guest (Hades hammered this millions of times/sec) and, worse, skips the
unlock — leaving the mutex held and wedging every thread that later blocks
on pthread_mutex_lock. When the mutex reads as untracked (owner==0), adopt
ownership so the unlock/wait/re-lock cycle is balanced and actually releases
it. Genuine ownership violations (owned by another thread) still error.

Eliminates the EPERM storm and converts the resulting livelock into correct
blocking; no effect on games that lock through the HLE (owner already set).

* [Core] SSE4a EXTRQ patch: read the xmm register from ModRM, not xmm2

The loader rewrites Sony's AMD-only SSE4a EXTRQ+blend idiom into SSE4.1 at
boot, because Rosetta 2 and Intel hosts raise #UD -> SIGILL on EXTRQ. The
matcher hard-coded the source register to xmm2 (ModRM 0xC2), but the compiler
allocates it freely: Dead Cells (PPSA15552) emits the identical idiom against
xmm1, so it slipped through unpatched and the game died with SIGILL right
after the first frame.

Read the register from the ModRM r/m field instead, covering xmm0-xmm7, and
require it to be consistent across the EXTRQ and the blend. The pure
match/encode logic is extracted into Sse4aExtrqBlendPatch, isolated from the
native page-patching, and unit-tested for every register plus the round trip
and rejection cases; DirectExecutionBackend just applies it.

Dead Cells now patches its xmm1 idioms and boots past the first frame.

* [Ngs2] Implement non-allocator sceNgs2SystemCreate / sceNgs2RackCreate

Dead Cells uses the non-allocator NGS2 create entry points, which were
unimplemented. sceNgs2SystemCreate came back as an unresolved import, so the
game got a garbage system handle; every downstream sceNgs2RackCreate /
sceNgs2RackGetVoiceHandle then failed, the voice handle stayed null, and once
gameplay started the audio path polled sceNgs2VoiceGetState/VoiceControl on
the null voice forever — freezing the game in-level at FLIP 0.

The non-allocator forms differ only in a caller buffer (rsi/rcx) vs an
allocator callback; the system/option and out-handle arguments sit at the
same positions, so they alias the existing WithAllocator implementations.
Resolves the NGS2 InvalidVoiceHandle storm (591+/run -> 0).

* [SaveData] Real save subsystem: ~/SharpEmu/Saves/<titleId>, events, full CRUD

Rework the SaveData HLE from a partial stub into a working subsystem:

- Storage moves to ~/SharpEmu/Saves/<titleId>/<dirName>/ (was next to the
  exe under user/savedata/<userId>/<titleId>), overridable via
  SHARPEMU_SAVEDATA_DIR. Metadata (title/subtitle/detail/userParam) and icon
  live in <slot>/sce_sys/. Pure path + param.json logic is isolated in a new
  SaveDataStorage type and unit-tested.
- Async event model: sceSaveDataGetEventResult now resolves (was an
  unresolved import a save worker polled forever), returning queued completion
  events or a clean 'no event' status; SyncSaveDataMemory posts a
  SAVE_DATA_MEMORY_SYNC_END event. Plus GetEventInfo/SetEventInfo/register
  callbacks.
- New exports: Mount/Mount2/Mount5/Umount, Delete/Delete5, GetParam/SetParam,
  SaveIcon/SaveIconByPath/LoadIcon, GetAllSize/GetProgress/GetMountInfo/
  IsMounted/GetSaveDataCount/GetMountedSaveDataCount/Abort, Initialize/
  Initialize2/Terminate, SaveDataMemory v1 aliases.
- Mounts are tracked so Umount2 really unregisters the /savedata0 mapping
  (new KernelMemoryCompatExports.UnregisterGuestPathMount) and params/icons
  resolve against the live mount; DirNameSearch surfaces param.json titles.

15 new unit tests (storage layout/sanitize/metadata + mount/event/param/delete
exports); full suite 277 passing.

* [Gpu] Metal: Cmd+F1 toggles Apple's Metal Performance HUD

Plain F1 keeps the built-in CPU-rasterized perf overlay; Cmd+F1 now toggles
the system Metal Performance HUD on the CAMetalLayer, Metal backend only.

Command-modified keys never reach keyDown: (AppKit routes them through the
key-equivalent chain), so the input view gains a performKeyEquivalent:
override that claims Cmd+F1 (also silencing the system beep) and leaves
everything else to the responder chain.

Configured per Apple's 'Customizing Metal Performance HUD':
developerHUDProperties with mode=default + logging=default, plus
MTL_HUD_LOG_SHADER_ENABLED=1 passed directly in the dictionary — HUD,
per-frame statistics logging, and shader-compile logging all enabled from
one property set; mode=disabled hides it again. Guarded by a
respondsToSelector: check for older macOS.

* [Gpu] Metal: also catch Cmd+F1 in keyDown: for the HUD toggle

Function keys reach keyDown: even with Command held (AppKit only reroutes
some chords through performKeyEquivalent:), so the HUD toggle was never
firing there. Handle Cmd+F1 in both the keyDown: and performKeyEquivalent:
paths, and keep it out of MetalHostInput so it can't also flip the plain-F1
perf overlay.

* [Audio] Diagnostics: NGS2 voice-param dump + AudioOut peak-amplitude trace

Two gated traces (idiomatic SHARPEMU_LOG_* style) that pinpoint where audio
dies for NGS2-based games:

- SHARPEMU_LOG_NGS2 now walks the sceNgs2VoiceControl param list and logs each
  {size,id} block header + payload bytes, confirming the real layout
  (header = u32 size, u32 id; waveform-block param id=0x10000001 carries the
  guest PCM pointer at +8; rate param id=0x10000005 carries the resample ratio).
- SHARPEMU_LOG_AUDIO_OUT logs sceAudioOutOutput call count and the peak
  amplitude of each submitted buffer.

Finding on void Terrarium: sceAudioOutOutput is called thousands of times on
both 8ch/float32 ports, but every buffer has peak=0.0 — the guest submits pure
silence. The host path (AudioOut -> PCM convert -> CoreAudio) is proven correct;
the silence originates in Ngs2SystemRender, which zeroes the output buffer
instead of mixing voices. Restoring audio for NGS2 games requires a real NGS2
software mixer (next).

* [Audio] NGS2 software mixer: decode + mix PS-ADPCM voices

NGS2-based games were silent because sceNgs2SystemRender only zeroed the
output buffer. This adds a real software mixer:

- Ngs2VagDecoder: clean-room PS-ADPCM ("VAGp") decoder producing mono PCM16
  with loop points resolved from the exact per-frame flag values (3=loop
  start, 6=loop end, 1/7=one-shot end).
- Voice control now parses the SceNgs2VoiceParamHead command list, decodes the
  waveform-blocks param's VAGp container once, and arms the voice.
- sceNgs2SystemRender mixes every armed voice belonging to the system into the
  leading grain of the render buffer as interleaved float32 (nearest-sample
  resample from the source rate to 48 kHz, additive into the front L/R pair),
  which is exactly what games copy to sceAudioOutOutput.

Verified on void Terrarium: previously peak=0.0 silence at AudioOut, now real
audible SFX/music. Voices are still armed on waveform assignment rather than an
explicit kick, so pooled/duplicate voices can overlap — trigger-state handling
is a follow-up.

* [Gpu] AGC: latch GPU-wait satisfaction to the produced value

Fixes a lost-wakeup race that stalled games at a black/splash screen. When a
RELEASE_MEM packet writes a completion label, the guest frequently resets that
label to 0 immediately to reuse it next frame. Our wake path
(GpuWaitRegistry.CollectSatisfied) re-reads *current* guest memory, so if the
reset lands before the wake pass runs, the transient satisfied window is missed
and the suspended DCB waits forever — even though the producing write executed
(traced as wrote=True) and its producer is marked completed.

RELEASE_MEM producers now call GpuWaitRegistry.LatchSatisfiedByValue with the
value they actually wrote, recording satisfaction at the moment of the write for
any waiter that value satisfies. CollectSatisfied honors the latch regardless of
the current (possibly-reset) memory value. This is fail-closed: a waiter only
latches when a real producer wrote a genuinely satisfying value.

Verified: Astro Bot's DEADBEEF sentinel wait (dcb.graphics waiting on a
release_mem label) that was permanently stuck is now resolved; void Terrarium is
unregressed (runs, audio intact, no producerless stalls). Astro still has
separate unresolved blockers (producer-behind-its-own-wait cascades and
producer=none-observed labels) tracked for follow-up; WRITE_DATA/DMA_DATA
producers could latch too but are left out until there is evidence they race.

* [Gpu] AGC: retry indirect dispatches whose GPU-computed dims aren't ready

GPU-driven games (Astro Bot) build their frame on the GPU: a compute dispatch
writes the thread-group dimensions for the next DISPATCH_INDIRECT into a guest
buffer. Our AGC parser reads those dimensions on the CPU at parse time, which
runs before the producing dispatch has executed on the render thread — so it
read 0/0/0 and dropped the work (agc.dispatch_reject zero-dimension), leaving
the scene unrendered (black) and cascading into stuck cross-queue fence waits.

Instead of dropping a zero-dimension INDIRECT dispatch, suspend the DCB on its
dimensions buffer (reusing the WAIT_REG_MEM suspend/resume + GpuWaitRegistry
machinery) until the producer writes non-zero dims, then re-parse and dispatch.
A bounded per-wait deadline (150 ms) resumes-and-drops a genuinely empty
indirect dispatch so it can never stall the queue, making the change
non-regressive: worst case matches the old drop behavior after a short wait.
Direct dispatches (dims inline) are unaffected.

Result: Astro Bot goes from a permanent black screen to actually rendering
(the presenter reports "Metal VideoOut presenting 3840x2160"). void Terrarium —
which issues no indirect dispatches — is unregressed (runs, audio intact, zero
rejects). Astro then hits a separate, newly-reached downstream crash (guest
TBB worker thread_set_state failure) tracked for follow-up.

* [ShaderCompiler] Metal: keep compute shaders within read_write and LDS limits

Two Metal limits made real Astro Bot compute shaders fail to compile/create,
which dropped their dispatches and cascaded into stuck GPU waits (splash hang):

- Textures with access::read_write are capped at 8 per function, but every
  storage image was declared read_write. Track each binding's actual access
  during body emission (ImageLoad->read, ImageStore->write, ImageAtomic and a
  load+store sharing one binding->read_write) and emit the minimal qualifier,
  so read-only/write-only storage images no longer count against the cap.

- Threadgroup memory is capped at 32 KB. A shader requesting the full 32 KB of
  LDS plus the separate 3-dword wave64 bridge overflowed by 12 bytes. Alias the
  bridge into the top of the LDS allocation when both are used, mirroring the
  SPIR-V translator's _waveScratchInLds path, keeping the total at 32 KB.

Verified on Astro Bot: "read_write access exceeds maximum (8)" and "Threadgroup
memory size (32780) exceeds maximum (32768)" are both gone; the 27 MSL golden
tests still pass (no golden used a storage image or LDS+wave64 shader).

* [Gpu] AGC: break cross-queue GPU wait deadlocks with a produced-value fallback

Real GPU-driven titles (Astro Bot) drive graphics and compute queues with
mutually dependent WAIT_REG_MEM fences: graphics waits on a compute EOP label,
compute waits on a graphics label. On hardware the two queues run concurrently
so the cycle resolves, but our submission parser is serial, so a label that gets
written -> reset for reuse -> re-waited across queues can wedge forever. The
latch fix helped the write-then-consume race but the cycle re-formed each frame
(graphics stuck at 3 flips, compute queues permanently suspended).

Producers now record the last value they wrote to each label
(GpuWaitRegistry.RecordProduced). A new deadlock breaker
(CollectDeadlockBroken, run from DrainResumableDcbs) releases any waiter stuck
past a 500 ms deadline whose condition is satisfied by that recorded value —
i.e. a real producer signalled the label at least once, guest memory has just
since been reset. It never fabricates a value, and the long deadline means
legitimate fences (which complete within a frame) never trip it.

Verified: Astro Bot goes from 3 flips (wedged on splash) to 25, loads its
splash level ("LevelDocument Loaded: ps_logo") and produces 2432x1368 frame
content. void Terrarium is untouched — 0 deadlock-break events, 1020 flips,
audio intact (its waits resolve far under the deadline). Tunable via
SHARPEMU_GPU_DEADLOCK_BREAK_MS.

* [Cpu] SSE4a EXTRQ patch: cover any blend destination register, not just xmm0

The EXTRQ+VPBLENDD idiom rewrite only matched when the blend destination was
xmm0 (VEX.vvvv byte 0x79). Sony's toolchain allocates that register freely: a
Dead Cells build emits `EXTRQ xmm4,0x28,0x00 ; VPBLENDD xmm3,xmm3,xmm4,2`
(VEX byte 0x61, dest xmm3). That instance stayed unpatched, so the AMD-only
EXTRQ reached Rosetta 2 and raised #UD -> SIGILL (0xC000001D) the moment the
game entered gameplay (loading level PrisonStart).

Read the destination register from VPBLENDD's VEX.vvvv / ModRM.reg as well as
the source from the ModRM r/m field, and emit PINSRD into that destination. Both
are still constrained to xmm0-xmm7 by the fixed VEX prefix. Match/encode stay in
the unit-tested helper.

Verified: Dead Cells now patches 14 EXTRQ blends (previously 0 on this build),
no SIGILL, and reaches PrisonStart. 15 patch unit tests pass, including the exact
xmm3/xmm4 bytes that faulted.

* [HLE] Implement Dead Cells' remaining unresolved imports

Three imports Dead Cells calls during boot/level-load were unresolved, so they
returned no defined value:
- scePadGetHandle (libScePad): returns the primary pad's handle (polled every
  frame for input); same validation as scePadOpen.
- sceNpEntitlementAccessGetAddcontEntitlementInfo (libSceNpEntitlementAccess):
  singular add-on-content lookup; we own no DLC, so zero the info out and return
  OK, matching the existing list variant.
- sceNpUniversalDataSystemEventPropertyArraySetString: telemetry setter, dropped.

Dead Cells now boots with zero unresolved imports. (It still stalls later at
PrisonStart level-load — a separate GPU/threading issue, not an import gap.)

* [Kernel] Fix pthread mutex deadlock: trylock semantics + stale-waiter clog

Hades hard-froze during boot on a "free but reserved" mutex: owner==0 yet
every acquisition failed forever. Two independent defects in the pthread
mutex compat layer combined to wedge it, both traced from real runs.

1. trylock incorrectly required an empty wait queue. POSIX
   pthread_mutex_trylock succeeds whenever the mutex is not currently held
   and owes no fairness to queued waiters; gating it on Waiters.Count==0 made
   a spin-on-trylock loop (which the game runs) spin forever against a single
   undrainable waiter even though owner==0. trylock now acquires on owner==0;
   the blocking lock still honours FIFO so genuine blocked waiters are not
   starved by a barging locker.

2. cond_timedwait timeouts leaked mutex re-acquire waiters. A cond wait's
   timeout enqueues a re-acquire waiter whose wake hand-off can be lost,
   orphaning it in the mutex queue. Multiple orphans from one thread piled at
   the FIFO head; the unlock hand-off then woke a dead wake-key and the mutex
   never drained. A thread can hold at most one pending acquisition on a
   mutex, so EnqueueMutexWaiterLocked now prunes any prior waiter for the same
   thread before enqueueing — collapsing the leaked pile.

Verified: Hades advances from a hard freeze at ~4.9M HLE calls (main and a
worker both blocked on the same free mutex) to 24.9M calls with no stall,
reaching the save-data/user-service boot stage. void tRrLM behaves
identically with and without the change (no regression); all 268 Libs tests
pass.
2026-07-18 20:32:00 +03:00
kostyaff 6dda6589d0
test: add Kernel/Loader unit tests (22 tests) (#373)
- SelfLoader: reject unknown magic, truncated headers; parse PS5 SELF embedded ELF
- KernelMemory: MapNamedFlexibleMemory/mprotect/munmap argument validation
- KernelEventQueue: create/delete/add/trigger/wait lifecycle

Co-authored-by: OMP <omp@local>
2026-07-18 20:19:55 +03:00
Aurélien Vivet 2ced3af114
AppContent: stub sceAppContentDownloadDataGetAvailableSpaceKb (#398)
Download data is not emulated as a real quota, so report a fixed 1 GiB
of free space and let titles skip the "storage full" path.
2026-07-18 18:44:30 +03:00
Berk 18708aa2d3
[GUI] Fixes and improvements for the GUI, including new image assets and updates to language files. (#400) 2026-07-18 17:44:04 +03:00
Berk a709ccca17
[shader_recompiler] Fix guest image byte count calculation for Vulkan video presenter (#395) 2026-07-18 16:09:26 +03:00
kadu04t 5309f384cf
Reject undefined numeric LogLevel values (#390) 2026-07-18 15:47:19 +03:00
Mehmed Sinan Kömek e6be48a390
GUI: harden cross-platform updater integrity and rollback (#389)
* GUI: verify updater releases by commit and SHA-256

* GUI: add updater rollback and version safeguards
2026-07-18 14:41:59 +03:00
Raiyan b3e3fe5ea8
docs: note Windows on ARM runs the x64 build via emulation (#386)
Mirror the existing Rosetta 2 note for Apple Silicon: Windows on ARM
devices (e.g. Snapdragon) can run the Windows x64 build through Windows'
built-in x64 emulation.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 14:06:19 +03:00
Dmitriy c9e2a1a390
Update Russian translations (#387) 2026-07-18 13:52:08 +03:00
Granthik 01b80fe381
UPDATE: Harden bink2 bridge (#385)
fix stride overflow, check decode return values, null movie on open failure
2026-07-18 13:49:59 +03:00
999sian f84d869795
[Kernel] Match path cache comparisons to host filesystem case sensitivity (#381)
The negative-stat cache and the apr file-size cache memoize host
filesystem probe outcomes, but both were keyed with an ignore-case
comparer while the probes themselves (File.Exists/Directory.Exists/
FileInfo) are case-sensitive on Linux. That aliases distinct paths:

- stat("/app0/DATA.BIN") fails, the miss is cached, and a later
  stat("/app0/Data.bin") is answered NOT_FOUND from the cache without
  ever probing the disk - even though the file exists and the probe
  would succeed.
- sceKernelAprResolveFilepathsToIdsAndFileSizes serves the cached size
  of a case-distinct sibling file instead of the file's own size.

The registered-mount containment guard had the inverse problem: the
ignore-case StartsWith accepted a ".." path that resolves into a
sibling directory differing from the mount root only by case
("…/Save" vs "…/save"), letting guest I/O escape the mount.

All three sites now compare with the host filesystem's semantics:
ordinal-ignore-case on Windows, ordinal elsewhere. Windows behavior is
unchanged. Tests probe actual host filesystem behavior with real temp
files and skip their case-specific sections on case-insensitive hosts.
2026-07-18 13:36:01 +03:00
Spooks 13269797bf
Add live debugger frontend and mutex stall recovery (#383) 2026-07-17 22:41:07 -06:00
jimmyjumbo 1c8cdd6537
[VideoOut] Initialize output options storage (#315)
* [VideoOut] Initialize output options storage

* [VideoOut] Keep output options size local
2026-07-18 04:14:27 +03:00
Peter Bonanni b566444df3
Add elapsed time to performance overlay (#250) 2026-07-18 03:35:56 +03:00
Raiyan 8a6f4f7826
[GUI] Per-game launch settings + shared SettingRow (#2) (#378)
Add per-game launch overrides (log level, import-trace limit, strict dynlib
resolution, log-to-file, and SHARPEMU_* environment toggles) with three-tier
resolution (per-game override -> global preference -> built-in default), stored
one file per game at user/custom_configs/<titleId>.json. Editable from a new
"Game settings..." context-menu dialog.

Introduce a shared SettingRow control and adopt it across the Options page and
the per-game dialog so the two read as one app. Fully localized (reusing the
existing Options.* keys), with the actions pinned in the dialog footer.
2026-07-18 03:19:37 +03:00
Peter Bonanni 41c9b44a8a
[AJM] Track registered codec instance lifecycle (#352) 2026-07-18 03:02:19 +03:00
Mees van den Kieboom 743fe5cc26
[ShaderCompiler/Vulkan] Match vertex input numeric types (#351)
Declare UINT and SINT vertex attributes with integer SPIR-V component types so shader interfaces match the Vulkan pipeline formats. Keep normalized, scaled, and floating-point formats on float inputs.

Signed-off-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
Co-authored-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
2026-07-18 03:01:01 +03:00
Peter Bonanni 0b1dea43e8
[CPU] Preserve blocked leaf import waiters (#350) 2026-07-18 02:59:13 +03:00
lilp c9d018db8e
Vulkan: fix guest storage image and render-state handling (#332) 2026-07-18 02:54:52 +03:00
Chris b479dc0466
videoout: implement output support query (#269)
Co-authored-by: Chris Cheng <chris@appxtream.com>
2026-07-18 02:44:23 +03:00
Peter Bonanni 22bbb4e909
[AvPlayer] Resolve guest media within app0 (#347)
Handle project-relative file URIs through the guest app0 mount, including unambiguous case-insensitive lookup for case-sensitive hosts.

Reject host paths, traversal underflow, malformed or remote URIs, and symlink/reparse escapes; cover accepted app0 forms and sandbox boundaries with nonparallel tests.
2026-07-18 02:42:30 +03:00
Peter Bonanni 3c500d2cf0
[SystemService] Write notice skip flag as byte (#346)
The Gen5 caller supplies a one-byte flag. Preserve pointer and memory-fault behavior while writing only that byte, and cover a seeded guest-memory boundary that rejects the former four-byte write.
2026-07-18 02:41:38 +03:00
Peter Bonanni bcb0ebd991
[ShaderCompiler] Fix VReadlane scalar destination field (#344)
V_READLANE uses the gfx10 VOP3A vdst byte even though its result is scalar. Decode bits 0-7 and cover the public LLVM s5 and s101 encodings so the VOP3B sdst field cannot be confused with this opcode again.
2026-07-18 02:41:07 +03:00
Mees van den Kieboom ecbb0db9be
[Kernel] Preserve socket descriptors after failed connect (#343)
Keep ownership of a socket descriptor with the guest when connect fails, and route generic close calls through the socket table. Add a deterministic regression test for the failure and close sequence.

Signed-off-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
Co-authored-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
2026-07-18 02:40:11 +03:00
Jose Olguin Lagos 81633f6d5a
Validate synthetic SPIR-V in CI (#335) 2026-07-18 02:39:07 +03:00
Zaid Yousef cc290f860b
[Kernel] Return largest available direct-memory span (#334) 2026-07-18 02:38:39 +03:00
Berk ff3ac0bba1
chore: bump version to 0.0.2-beta.3 (#345) 2026-07-17 19:01:35 +03:00
tensorcrush 847371d2de
[AGC] Decode VOP3P and emit packed f16 arithmetic (first slice) (#145)
* [AGC] Decode VOP3P and emit packed f16 arithmetic (first slice)

On gfx10 the VOP3P family lives under its own 0b110011000 prefix (word0
top byte 0xCC), which the major-opcode switch currently routes to the
SMEM branch, so packed instructions were decoded as scalar memory ops
and emitted as silent no-ops. Intercept the exact 9-bit prefix ahead of
the switch, decode the five packed-f16 arithmetic opcodes with their
op_sel/op_sel_hi/neg_lo/neg_hi/clamp modifiers, and emit them as
UnpackHalf2x16 -> component-wise f32 vec2 ops -> PackHalf2x16 so no
Float16 capability is needed.

Bit layout and opcode numbers pinned to LLVM MC test encodings
(vop3p.s, gfx10_vop3p_literalv216.txt) and VOP3PInstructions.td.
Unsupported modifiers, packed constants and out-of-scope packed opcodes
fail with a clear error instead of emitting wrong results.

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

* [AGC] Make packed f16 exact and drop v_pk_fma_f16 (review response)

Address the FP16 correctness review on the VOP3P slice.

Replace GLSL UnpackHalf2x16/PackHalf2x16 with explicit integer f16<->f32
conversions (EmitHalfToFloat/EmitFloatToHalf): exact widening with subnormal
normalisation, and narrowing with round-to-nearest-even, overflow-to-Inf and
NaN/Inf handling. Their subnormal and rounding behaviour no longer depends on
implementation-defined float-controls modes.

With exact conversions, v_pk_add_f16 and v_pk_mul_f16 are bit-exact to a true
f16 op (f32 result rounds losslessly to f16; a f16 product fits in f32). Emit
v_pk_min_f16/v_pk_max_f16 as fminnum_like/fmaxnum_like (NaN operand returns the
other; ordered numeric compare) instead of GLSL FMin/FMax.

v_pk_fma_f16 now fails emission loudly: a fused f16 FMA rounds once, an f32
multiply-add then pack double-rounds (fma(0x4100,0x7522,0x04EA) is 0x7A6B fused
vs 0x7A6A via f32). Exact fused emulation is a planned follow-up slice.

ShaderDump gains an Expect model (Translates/DecodeFails/EmitFails) and packed
regressions: arith, non-default modifiers, and loud-failure pins for the fma
case above and for clamp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: tensorcrush <tensorcrush@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Antigravity AI <antigravity@gemini.com>
2026-07-17 18:58:22 +03:00
Berk fbafd3f429
[GUI] Add native Vulkan host surface support and more (#337)
* [GUI] Add native Vulkan host surface support and more

* reuse

* [GUI] set width session menu
2026-07-17 18:57:10 +03:00
Mees van den Kieboom aa25f6e978
[Loader] Restore PS5 SELF header support (#342)
Accept both PS4 and PS5 SELF signatures without treating version, key type, or flags as layout markers. Add synthetic coverage for valid header variants and malformed structural fields.

Signed-off-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
Co-authored-by: missatjuhvdk1 <177474143+missatjuhvdk1@users.noreply.github.com>
2026-07-17 18:55:16 +03:00
Raiyan b9eee497aa
[VideoOut] Scope the AMD integrated-GPU penalty to Windows (#339)
* [VideoOut] Prefer real integrated GPUs over software rasterizers (#325)

Penalize only AMD integrated GPUs (the #97 vkCreateGraphicsPipelines
crash) instead of all integrated devices, so Intel/Apple/Qualcomm iGPUs
outrank Cpu-type software rasterizers (Mesa lavapipe). Hoist
ScorePhysicalDevice to the outer class and add unit tests for the
ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [VideoOut] Scope AMD iGPU penalty to Windows via a penalty helper

Extract ComputeDevicePenalty (the value subtracted from a device's base
score) and gate the #97 AMD-integrated penalty on Windows only. Mesa RADV
on Linux (e.g. the Steam Deck's AMD APU) is a different, working driver
and should keep its full integrated score. Add a Steam Deck test case and
drop inline comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [VideoOut] Move device-scoring helpers out of the const block

Relocate ScorePhysicalDevice and ComputeDevicePenalty below the leading
const cluster instead of splitting it, and trim the vendor-ID reference
comment to adapters an x86-64 host can realistically enumerate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 18:48:09 +03:00
Jose Olguin Lagos a9a4366ef4
test: cover Gen5 decoder fail-closed boundaries (#336)
Reimplement the five synthetic regressions from codefusion-repo/sharpemu#3 and #8 on sharpemu/sharpemu main befd20f415.

Keep the contributor-owned harness and corpus discussed in sharpemu/sharpemu#36 out of scope; use only a focused local recording memory fixture.

Signed-off-by: Jose Olguin Lagos <hellocodefusion@gmail.com>
2026-07-17 18:45:26 +03:00
ParantezTech 76e8f789c4 [github] edit contribution link in pull request template 2026-07-17 18:25:34 +03:00
ParantezTech 6fce0e3055 [github] update PR template and REUSE.toml 2026-07-17 18:20:44 +03:00
Berk befd20f415
Update contribution guidelines to enforce pull request template (#331)
Emphasize the importance of using the pull request template and outline consequences for non-compliance.
2026-07-17 13:52:41 +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
kuba fa2616d224
Linux and macOS support (#47)
* [macos/linux] Cross-platform host memory, TLS, and ABI layer for POSIX

Introduces the foundation for running SharpEmu on macOS (osx-x64 under
Rosetta 2) and Linux (linux-x64). The CPU backend executes guest x86-64
code natively, so these targets run the whole process as x86-64; this
commit replaces the Windows-only host primitives with platform-dispatched
equivalents so the guest boots and services HLE calls off Windows.

Memory (HostMemory.cs, new): a Win32-semantics facade over
mmap/mprotect/munmap with a shadow region table answering VirtualQuery.
PhysicalVirtualMemory, DirectExecutionBackend, StubManager, and the two
Kernel*CompatExports now go through it instead of kernel32 P/Invokes.
Exact-address requests use MAP_FIXED_NOREPLACE (Linux) / guarded
MAP_FIXED (macOS) so they match Win32 "map there or fail" semantics.

TLS + host helpers (PosixHostStubs.cs, new): pthread-backed TLS and
Win64-ABI-compatible stubs for the kernel32 helpers the backend embeds
into emitted x86-64 code (TlsGetValue, QueryPerformanceCounter,
SwitchToThread, Sleep). A Win64->SysV thunk wraps managed callbacks,
since .NET on POSIX compiles them for the SysV ABI while the emitted
call sites use Win64.

Guest address layout: the 0x7FFx window is Windows-only (dyld shared
cache / Rosetta runtime live there on POSIX), so stack/TLS/stub regions
relocate to 0x6FFx off Windows.

Vectored exception handling is gated off on POSIX for now (guest faults
are not yet recovered) — the signal-based bridge is the next step. Also
adds osx-x64 to the RID list and a Docker-based Linux smoke-test script.

Status: on both macOS (Rosetta) and Linux (amd64), the guest now boots,
runs native x86-64 code, and dispatches HLE imports. macOS stops at a
Rosetta translation-cache issue; Linux runs ~252 imports through C++
static-init before hitting the missing fault handler (SIGSEGV).

* [posix] Bridge the vectored exception handler to sigaction(SIGSEGV/SIGBUS/SIGILL)

Guest faults on macOS/Linux previously terminated the process because the
recovery logic in DirectExecutionBackend.Exceptions.cs was Windows-only.
This adds a POSIX front-end that reuses the existing handler bodies:

- DirectExecutionBackend.PosixSignals.cs installs SA_SIGINFO handlers via
  an [UnmanagedCallersOnly] entry, rebuilds the Win64 EXCEPTION_POINTERS /
  CONTEXT view from the platform mcontext (Darwin __ss thread state via
  the mcontext pointer at ucontext+48, Linux glibc gregs at ucontext+40 --
  offsets verified against the headers on both platforms), runs the same
  chain as the VEH path (TryRecoverUnresolvedSentinel trap-sentinel
  recovery, TryHandleLazyCommittedPage demand paging, VectoredHandler
  diagnostics incl. FS/GS TLS-fault detection), and writes register
  changes back into the mcontext so sigreturn resumes the repaired guest.
  Unrecovered faults chain to the previously installed handler so the
  .NET runtime keeps mapping its own faults to managed exceptions.

- The whole recovery path is warmed up with fabricated inputs before the
  handlers are installed. This is required under Rosetta 2: the signal
  trampoline cannot enter x86 code that has never been executed (and so
  never translated) -- a cold handler is silently never invoked and the
  faulting instruction retries forever (reproduced and verified in an
  isolated .NET test under Rosetta for Linux). It also keeps first-fault
  JIT work out of the signal frame.

- Handlers run without SA_ONSTACK: the runtime's alternate stacks are too
  small for the diagnostic path, while guest (2MB) and host thread stacks
  match where Windows dispatches exceptions anyway.

- The raw reads in the shared fault diagnostics (stack qwords, RBP walk,
  code bytes at RIP) now probe the region table on POSIX before touching
  memory, since a nested SIGSEGV inside the handler would kill the
  process before diagnostics finish. Windows keeps its try/catch reads.

- Escape hatches: SHARPEMU_DISABLE_POSIX_SIGNALS=1 skips installation,
  SHARPEMU_DISABLE_RAW_HANDLER=1 disables sentinel recovery (parity with
  Windows), SHARPEMU_LOG_POSIX_SIGNALS=1 traces every delivery (first 16
  and every 1024th are always traced).

Verified with the test game: Linux (amd64 container) previously died with
SIGSEGV right after import #252; it now recovers/diagnoses signals and the
run proceeds to the real next blocker, an unpatched negative-offset guest
TLS read (fault at TLS base - 0x1708), which gets the full NATIVE
EXCEPTION dump before terminating. macOS is unchanged: the bridge installs
and the game still stops at the known Rosetta translation-cache error at
import 12, which is the next work item.

* [posix] Fix guest memory layout faults: TLS prefix, exact mmap, map search base

Three fixes that take the test game from dying during libc init to running
its full main loop on macOS and Linux:

- Static TLS blocks live below the TCB (FreeBSD amd64 variant II) and
  libc.prx reaches past -0x1700, but only a 4KB prefix was mapped below
  the TLS base. The prefix is now 64KB on POSIX (Windows keeps 4KB); the
  fault was a read at TLS base - 0x1708 during libc init.

- HostMemory exact allocation on macOS used MAP_FIXED, which silently
  maps over untracked host memory. The direct-memory allocator's address
  scan walked into the .NET runtime's JIT heap and replaced live code,
  which under Rosetta 2 surfaced as "no code fragment associated with
  the given arm pc". Exact placement now passes the address as a hint
  and fails on relocation, like MAP_FIXED_NOREPLACE does on Linux.

- sceKernelMapDirectMemory/MapFlexibleMemory searched for free space
  starting at 4GB, which is the Mach-O image base on macOS. The default
  search base is 0x20_0000_0000 on POSIX, and TryAllocateAtOrAbove now
  asks the kernel for a placement instead of page-stepping through host-
  owned address space (Rosetta ignores mmap hints for whole VA windows),
  over-allocating when the caller needs more than page alignment.

Windows behavior is unchanged; all divergences are platform-guarded.

* [macos] Video presenter on the main thread, MoltenVK support, window keyboard input

Gets the test game from a headless loop to a playable window on macOS:

- AppKit traps with SIGILL ("NSUpdateCycleInitialize() is called off the
  main thread") when GLFW runs on a worker thread. The CLI now moves
  emulation onto a worker thread on macOS and parks the real main thread
  in HostMainThread.Pump(); the presenter posts its whole window loop
  there instead of spawning a thread, and a shutdown handler asks the
  render loop to close the window so the pump unwinds on guest exit.

- MoltenVK: enable VK_KHR_portability_enumeration (+ the portability
  instance flag) and VK_KHR_portability_subset when advertised, and gate
  robustBufferAccess2 on the device actually supporting it (Metal does
  not; the old code keyed it off robustImageAccess2 and vkCreateDevice
  failed with ErrorFeatureNotPresent).

- Input: pad exports polled user32 GetAsyncKeyState, so POSIX hosts threw
  DllNotFoundException per scePadReadState call. The presenter now
  attaches the window's keyboard via Silk.NET.Input into HostWindowInput,
  and the pad exports map the existing VK-code layout onto it off
  Windows. Headless hosts (Linux containers) report a disconnected
  keyboard and fall back to neutral pad data silently.

GLFW needs an x86-64 Vulkan loader under Rosetta: place a universal
libMoltenVK.dylib next to SharpEmu named libvulkan.1.dylib (Homebrew's
arm64-only copy cannot load into the x86-64 process) and export
DYLD_LIBRARY_PATH to that directory.

Verified: Dreaming Sarah boots to a MoltenVK-backed 2560x1440 window on
macOS (Apple M4, Rosetta 2), renders the intro, title, and menus, and
keyboard input drives it into gameplay. Linux (amd64 container) runs the
same build headless through millions of imports with no faults. Windows
paths unchanged; arm64 and x64 builds clean.

* [posix] CoreAudio playback, self-contained MoltenVK loading, input/log polish

- Audio: sceAudioOut ports now play through an AudioQueue backend on macOS
  (stereo PCM16 with the same 32KB backpressure pacing as the WinMM path).
  The WinMM port and the new CoreAudio port share an IHostAudioPort
  interface and sample converter; hosts without a backend (Linux
  containers) keep the silent fallback.

- MoltenVK: GLFW resolves Vulkan with dlopen("libvulkan.1.dylib"), which
  cannot see the app-local universal MoltenVK build, so the presenter now
  feeds vkGetInstanceProcAddr straight into glfwInitVulkanLoader (GLFW
  3.4) before creating the window. No DYLD_LIBRARY_PATH needed; the CLI
  also preloads the dylib for Silk.NET and prints setup hints when it is
  missing. scripts/fetch-macos-moltenvk.sh stages the official universal
  dylib next to a build.

- The virtual-range allocator's failure trace now names the address and
  length instead of "AllocateAt invocation threw".

Investigated and documented (not port defects): the savedata transaction
failure is identical on Linux and macOS (HLE argument-register mapping for
sceSaveDataCreateTransactionResource), and the in-game tile speckling has
no platform-specific code in its path - the one macOS-only delta is that
MoltenVK lacks robustBufferAccess2, so out-of-bounds shader reads return
garbage instead of zeros.

Verified on macOS: window, audio backend, and keyboard input all come up
with zero environment configuration; the game runs to gameplay. Linux
headless run unchanged (silent audio, no faults). Windows paths untouched;
arm64 and x64 builds clean.

* [cpu] Preserve guest registers and flags across patched TLS accesses

The TLS patch handler replaces guest `mov reg, fs:[...]` instructions,
which preserve every other register and the flags - but the handler
loaded the TLS index into ecx and called TlsGetValue (Win64: clobbers
rcx/rdx/r8-r11) with `sub/add rsp` trashing the arithmetic flags. Guest
code that keeps live values or comparison results across a TLS access
computed garbage deterministically. The handler now saves rcx, rdx,
r8-r11, and the flags around the call, keeping the same inner stack
alignment. This applies to the load patches and both store-helper stubs,
on every platform.

Also in this change, from the rendering-artifact investigation:

- The present blit picks linear filtering for any fractional scale
  (nearest only for integer upscales): a 3840x2160 guest frame blitted
  into a 2560x1440 swapchain with nearest silently dropped every third
  row/column.
- ClampViewport no longer trims the guest viewport rectangle to the
  render target; trimming changed the guest's scale/offset and skewed
  texel addressing. Vulkan permits viewports beyond the framebuffer
  (the scissor confines rendering), so only spec bounds are enforced.
- Env-gated diagnostics grown during the investigation: guest texture
  dumps (SHARPEMU_TEXTURE_DUMP_DIR), aliased guest-image readback dumps
  (SHARPEMU_TRACE_GUEST_IMAGES=alias), small-render-target write movies
  (SHARPEMU_TRACE_GUEST_WRITES=small), unattended input injection
  (SHARPEMU_AUTO_CROSS=secs,...), viewport nudging
  (SHARPEMU_VIEWPORT_EPSILON), chunked-draw toggle
  (SHARPEMU_DISABLE_CHUNKED_DRAWS), and rect-list/draw vertex traces.

Known remaining issue (root cause narrowed, not yet fixed): the game's
terrain texture pages are corrupted in guest memory before any GPU work
- the mound's solid-fill 32x32 tiles decode to fully transparent texels
and the grass page has deterministic gaps, byte-identical across runs.
Ruled out: memcpy/memmove/memset/realloc HLE semantics, sampler wrap
modes, texel-boundary rounding, chunked draws, viewport handling. Next
step is auditing the Chowdren asset decode path (custom compressed
images) against the emulator's import surface.

* [linux] ALSA playback backend for sceAudioOut

sceAudioOut ports on Linux now play through libasound instead of the
silent fallback. The PCM device opens in blocking mode with ~170ms of
device buffer (the time-equivalent of the 32KB queue the WinMM and
CoreAudio ports keep), so snd_pcm_writei provides the same backpressure
pacing without a managed queue. Underruns and suspend/resume go through
snd_pcm_recover with one retry per submit; anything else drops the
buffer rather than stalling the guest.

The "default" device routes through PulseAudio/PipeWire on desktops
and straight to hardware on bare ALSA; SHARPEMU_ALSA_DEVICE overrides
it (the null device makes the path testable in containers). A missing
libasound or device fails port creation and lands in the existing
silent fallback.

Verified in an amd64 container: the test game opens the port
(backend=alsa, 48kHz stereo float32) and streams sceAudioOutOutput
through the null device for a full run; without a usable device the
port logs a warning and falls back to silent. Playback on real Linux
audio hardware has not been tested.

* [fixes] Address review feedback: commit bounds, CoreAudio shutdown, dump errors

- HostMemory: a MEM_COMMIT that runs past its reservation now fails like
  Win32 instead of committing a prefix and reporting success. All current
  callers already clamp their ranges to the region, so this only guards
  future callers.

- CoreAudioPort: Dispose wakes a submitter waiting on backpressure and
  the wait treats ObjectDisposedException as a timed-out wait, so closing
  a port during playback can no longer throw. A failed AudioQueueStart
  tears the queue down and fails fast instead of leaving an undrainable
  queue that stalls every later submit on its timeout.

- AgcExports: texture dumping catches all write failures (bad path,
  permissions), logging a warning instead of crashing when
  SHARPEMU_TEXTURE_DUMP_DIR points somewhere unusable.

Verified with the Linux container run: game boots and streams audio with
the stricter commit check, and a dump dir under /proc produces warnings
instead of taking the process down.

* [ci] Build linux-x64 and osx-x64 archives

Adds a build-posix matrix job (ubuntu-latest / macos-latest) mirroring
the Windows build: locked restore, Release build, self-contained CLI
publish, and a tar.gz artifact per RID (tar keeps the executable bit).
The macOS archive also stages the universal MoltenVK dylib via
scripts/fetch-macos-moltenvk.sh so the artifact runs without any manual
Vulkan setup. The release job still only ships the Windows archive.

* [cli] Keep POSIX glfw natives outside the single-file bundle

The KeepGlfwOutsideSingleFile target only matched filenames starting
with 'glfw', which covers Windows (glfw3.dll) but not libglfw.3.dylib /
libglfw.so.3. Those got embedded into the single-file bundle, and
Silk.NET's library loader does not probe the bundle extraction
directory, so a published build died with "Couldn't find a suitable
window platform" (and the glfwInitVulkanLoader wiring, which loads the
library from AppContext.BaseDirectory, could not run either). Keeping
the POSIX names loose next to the executable fixes both, the same way
the Windows build already handled it.

Found by running the CI-built osx-x64 archive: video failed while local
loose-file builds worked. With the fix the published single-file build
opens the MoltenVK window, wires the loader, and reaches gameplay.

* [ci] Publish linux-x64 and osx-x64 release archives

The build-posix artifacts now ship as per-RID GitHub releases on main
pushes and manual dispatches, tagged the same way as the win64 ones
(<rid>-<ref>-<sha>). Archives stay tar.gz so the executable bit
survives extraction.

* [cli] Fail early on non-x86-64 host processes

The CPU backend executes guest x86-64 code natively, so the process
must be x86-64 (win-x64/linux-x64 on x64 hardware, osx-x64 under
Rosetta 2 on Apple Silicon). An arm64 process previously failed deep
inside emulation startup, indistinguishable from MoltenVK, signal
handler, or guest memory problems. CLI mode now checks the process
architecture up front and exits with a message naming the supported
execution model (and the Rosetta install command on macOS). The
GUI-only path stays usable on arm64.

* [video] Log the selected Vulkan device name and API version

The presenter never named the GPU it picked, so a 'no video' report
could not be told apart from a real windowing failure without guessing.
It now logs the device name, type, and API version right after
selection. A software rasterizer (llvmpipe/lavapipe/SwiftShader) shows
up here and typically lacks the device features the translated shaders
need, which is the likely cause when a window opens and presents frames
but nothing draws.

* [video] Steer GLFW to XWayland on Wayland sessions

GLFW's native Wayland backend does not reliably map the Vulkan window
with some drivers (NVIDIA in particular): frames present but the window
never becomes visible, so the game runs with audio and no picture. A
report on an RTX 5080 showed exactly this — all device features present,
frames presenting, but the log had 'libdecor-gtk.so failed to init' and
a 1.4x-scaled window, both Wayland tells.

On a Wayland session that also exposes an X server (DISPLAY set), the
presenter now clears WAYLAND_DISPLAY for its own process before GLFW
initializes, so GLFW selects its dependable X11/XWayland backend.
SHARPEMU_ENABLE_WAYLAND=1 opts back into native Wayland. Headless
(no DISPLAY) and non-Linux hosts are unaffected.

* [video] Force GLFW X11 backend via the platform init hint, log the platform

The previous attempt cleared WAYLAND_DISPLAY to steer GLFW off Wayland,
but a reporter still hit the native-Wayland path (the Wayland-only
libdecor error persisted), so that env trick doesn't switch GLFW.

Use GLFW's supported mechanism instead: glfwInitHint(GLFW_PLATFORM,
GLFW_PLATFORM_X11) before GLFW initializes, called into the same libglfw
GLFW itself loads (the pattern InitializeMacVulkanLoader already uses).
Still gated on a Wayland session with an X server present (DISPLAY set)
so we never force X11 where XWayland can't catch it, and still
overridable with SHARPEMU_ENABLE_WAYLAND=1.

Also logs 'GLFW windowing platform in use: <backend>' after init via
glfwGetPlatform, so a 'no window' report shows X11 vs Wayland outright.
Verified on macOS: the readback correctly reports Cocoa and the
presenter is unaffected (the fix is a no-op off Linux).

* [video] Run the GLFW window on the main thread on Linux too

GLFW requires window creation and event processing on the process main
thread on every platform: initialization, window creation, and
glfwPollEvents are main-thread-only, and X11 in particular has a single
event queue that must be serviced there. A window created and polled on
another thread may never map — which is why the game ran (audio, imports,
even Vulkan present) with no visible window on Linux.

macOS already routed the window loop to the main-thread pump (AppKit
needs it); Windows is fine because it has a per-thread event queue. Linux
was the gap: it spawned a background thread for the presenter. Extend the
existing HostMainThread pattern to Linux — emulation runs on a worker,
the main thread pumps the window work the presenter posts.

Refs GLFW intro guide (thread-safety): init, window creation, and event
processing are restricted to the main thread.

Verified: macOS still boots to its window unchanged; the Linux headless
container runs to millions of imports with no deadlock or regression.
On-screen confirmation on a real Linux desktop is still pending, but this
is the documented root cause for a windowless-but-running Linux session.

* [posix] Skip Win32 native guest workers

* [vulkan] Synchronize offscreen targets before present

* [vulkan] Transition fresh textures from undefined layout

* [vulkan] Report swapchain pixels before source readback

* [vulkan] Emit requested guest image diagnostics

* [agc] Diagnose guest texture fallbacks

* [linux] Keep guest GPU mappings in low address space

* [video] Reduce diagnostic stalls and drain complete frames

* [memory] Harden packed GPU address handling

* [readme] Document Linux and macOS release support

* [posix] Integrate the host platform abstraction

* [posix] Restore guest thread address window

* [video] Run the performance HUD on POSIX hosts

The FPS/CPU/work HUD bailed out unless the host was Windows; only the
per-thread hottest-thread scan actually needs Windows APIs. Keep that
scan Windows-only (POSIX reports 'idle') and let the rest of the HUD
run everywhere — the title is already set from the render thread, which
owns the window on macOS and Linux.

* [posix] Implement native guest worker threads

Guest entry stubs must not run above CLR-managed frames on CLR-created
threads (see the NativeWorker preamble); the PR previously fell back to
the inline calli path on POSIX, which reproduced the documented
'attempted to call a UnmanagedCallersOnly method from managed code'
fail-fast (observed after Dreaming Sarah's menu select) and left the
runtime's suspension machinery walking guest frames.

Provide the missing POSIX half of the worker loop:
- PosixHostStubs grows Win64-convention WaitForSingleObject/SetEvent/
  ExitThread stubs backed by dispatch semaphores (macOS) / unnamed POSIX
  semaphores (Linux) plus pthread_exit, with EINTR retry in the wait.
- Worker events are creatable/signalable/waitable from managed code too,
  so NativeGuestExecutor.Run keeps its handshake (AutoResetEvent stays
  on Windows byte-for-byte).
- PosixHostThreading implements CreateNativeThread/WaitForThreadExit/
  CloseThreadHandle over pthreads (liveness probed with
  pthread_kill(0), then joined).
- RunPrologue/RunEpilogue are routed through the existing Win64->SysV
  thunks, so the emitted loop stays identical across platforms.

* [macos] Disable concurrent GC under Rosetta's write-watch hazard

Background GC's write-watch revisit (SoftwareWriteWatch::GetDirty ->
FlushProcessWriteBuffers) calls thread_get_register_pointer_values on
every thread; under Rosetta 2 that Mach call stalls indefinitely on
threads executing translated guest code. The background mark phase then
never finishes and every allocating or Monitor-taking thread wedges
behind it — observed as Dreaming Sarah freezing at the menu/loading
screen with FPS 0 in 5 of 7 runs, dispatcher/watchdog parked in
Monitor.Enter and all BGC threads waiting in t_join.

Non-concurrent GC never takes that path; a 5-minute soak now holds
22-31 fps in-game with zero stalls. Windows and Linux keep concurrent
GC.

* [diag] Periodic guest-thread snapshots with gate-owner tracking

SHARPEMU_PERIODIC_SNAPSHOT_SECONDS=N dumps the stall snapshot every N
seconds even while imports are progressing, for soft stalls where the
game stops advancing but threads keep spinning. The periodic dump never
touches the guest-thread gate (it must keep reporting when the gate is
what's wedged): it reads a lock-free owner record — every gate
acquisition now goes through LockGate(site), which notes site/thread —
and walks the thread table without the lock, tolerating torn reads.
SHARPEMU_PERIODIC_SNAPSHOT_FILE redirects the dump to a side file for
the case where the console itself is wedged (frozen log mirror was one
of the observed failure modes).

* [nuget] Add osx-x64 RID targets to lock files

* [cpu] Back off the guest join poll

TryJoinThread polled the host thread at a fixed 1ms; a game main thread
joining a long-lived worker (Dreaming Sarah parks there for the whole
session) burned ~5% of managed CPU in Join/Sleep syscalls. Ramp the
poll interval to 10ms once the join is clearly long-lived — exit
detection latency for long joins moves from ~1ms to at most 10ms, and
short-lived joins still resolve on the first 1ms polls.

* [nuget] Add linux-x64/win-x64 RID targets to lock files

* [posix] Keep guest stacks clear of the import-stub descent

The import-stub region descends from 0x7000_0000_0000 on the same 16MB
grid as the guest thread windows; moving stacks to 0x6FFF_E000_0000 put
them inside the stub region's 64-module descent range (floor
0x6FFF_C000_0000), silently consuming the top ~32 stack slots on hosts
with many loaded modules. Drop the POSIX stack base to 0x6FFF_A000_0000:
512MB below the stub floor, still 2.5GB above the TLS window. Windows
keeps 0x7FFF_E000_0000 (its bands are ~15TB apart).

* [pad] Read window gamepads on POSIX hosts

XInput and the DualSense hid reader are Windows-only, which left
macOS/Linux with keyboard input only. The presenter's Silk/GLFW input
context already enumerates gamepads on both platforms, so track their
state in HostWindowInput (event-driven on the window thread, snapshot
guarded like the key set) translated to ORBIS conventions: GLFW's Xbox
layout maps A/B/X/Y to Cross/Circle/Square/Triangle, sticks bias from
-1..1 to 0..255 with Y growing down, and triggers rescale from GLFW's
-1..1 resting-at--1 range with digital L2/R2 bits past 25%.

The merge into ReadHostInputState is gated to non-Windows so a physical
pad is never sampled twice through both a native reader and GLFW.
Hotplug is handled via ConnectionChanged; with no pad connected the
path is inert.

Untested against a physical controller (none attached to the dev host);
axis conventions follow the GLFW gamepad-mapping contract.

* [nuget] Refresh lock files after cross-RID restores

* [posix] Adopt the host audio/input seams from main

Main's #192 abstracted audio output and pad/keyboard input behind
IHostAudioOutput/IHostInput; re-express the POSIX backends behind them:

- CoreAudioPort/AlsaAudioPort move to Host/Posix as
  PosixCoreAudioStream/PosixAlsaAudioStream implementing
  IHostAudioStream. The seam converts to stereo PCM16 before Submit, so
  the ports' own conversion (and IHostAudioPort/AudioSampleConverter)
  is gone; queueing and backpressure are unchanged.
- PosixHostAudio selects CoreAudio (macOS) / ALSA (Linux) as the
  platform's IHostAudioOutput.
- PosixHostInput implements IHostInput over an
  IPosixWindowInputSource that HostWindowInput registers when the
  presenter attaches the window's GLFW input context: keyboard with
  virtual-key translation, the window gamepad snapshot (now in seam
  HostGamepadState/HostGamepadButtons terms), and keyboard-connected as
  the focus signal. Rumble/lightbar no-op (GLFW has no such API).
- PadExports drops its direct HostWindowInput gamepad merge — pads now
  flow through IHostInput.GetGamepadStates like every platform.
- PosixHostThreading.RequestTimerResolution is a documented no-op.

All three RIDs build; SharpEmu.Libs.Tests pass (26/26).

* [nuget] Regenerate GUI lock file for RID-less locked restore

Local cross-RID builds stamped a win-x64 runtimes section into
SharpEmu.GUI's lock file; the project declares no RuntimeIdentifiers,
so CI's 'dotnet restore --locked-mode' failed with NU1004 on every
platform. Regenerated via a plain solution restore (--force-evaluate),
matching what the workflow validates.
2026-07-15 15:36:20 +03:00
Pacuka 7b86a91dfa
Add files via upload (#202)
Added Hungarian tranlation. -Pacuka
2026-07-15 14:39:42 +03:00
Gutemberg Ribeiro 72645cb373
[Host] Abstract audio output and pad/keyboard input behind the host platform seam (#192)
* [Host] Abstract audio output behind IHostAudioOutput

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

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

* [Host] Abstract pad and keyboard input behind IHostInput

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* [Host] Route PhysicalVirtualMemory through IHostMemory

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* [Vulkan] Execute translated draws with multiple color attachments

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

* [ShaderDump] Add MRT edge-case coverage

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

---------

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

* [Json] Add Initializer::setGlobalNullAccessCallback

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

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

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

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

---------

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

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

* Add NP UDS SetArray and PostEvent layout HLE stubs

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

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

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

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

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

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

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

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

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

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

* Added discord & github svgs and svg support

* Changed svg to pngs and localization text in english & spanish
2026-07-15 00:59:13 +03:00
Mike Saito caf859cc52
Fix guest shutdown when VideoOut window is closed (#184)
Propagate Silk window close to runtime teardown so audio and CPU workers stop instead of continuing after the presentation window is dismissed.
2026-07-15 00:52:01 +03:00
1272 changed files with 308645 additions and 34012 deletions

BIN
.github/images/astro-bot.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
.github/images/astros-playroom.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

BIN
.github/images/cult-of-the-lamb.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

BIN
.github/images/dead-cells.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

BIN
.github/images/demons-souls.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 227 KiB

BIN
.github/images/gow-sos.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

BIN
.github/images/hades.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

BIN
.github/images/pac-man-world-re-pac.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

BIN
.github/images/void-terrarium.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

27
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,27 @@
## Before submitting
Please read our contribution guidelines before opening a pull request:
➡️ [**CONTRIBUTING.md**](https://github.com/sharpemu/sharpemu/blob/main/CONTRIBUTING.md)
By opening this pull request, you confirm that you have read and agree to follow the contribution guidelines.
## Testing
If applicable, list the game(s) you tested and briefly describe the results.
Example:
- Demon's Souls (PPSA01341) – Boots to splash screen.
- Dreaming Sarah – Save/load works correctly.
If your changes do not affect runtime behavior (e.g. documentation, tooling, CI), write `N/A`.
## Checklist
By submitting this pull request, you confirm that:
- [ ] I have read and followed `CONTRIBUTING.md`.
- [ ] I tested my changes or marked the testing section as `N/A`.
- [ ] I wrote this pull request description myself and did not paste AI-generated explanations.
- [ ] I listed the game(s) I tested (or marked the testing section as `N/A`).

31
.github/workflows/notify-site.yml vendored Normal file
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-26.04
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
.github/workflows/pr-build-links.yml vendored Normal file
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-26.04
# 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 });
}

View File

@ -7,6 +7,8 @@ on:
push:
branches:
- "**"
tags:
- "v*"
paths-ignore:
- "**/*.md"
- "**/*.png"
@ -32,37 +34,44 @@ concurrency:
jobs:
init:
name: Init
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
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 "archive-name=${archive_name}"
echo "artifact-name=${artifact_name}"
echo "safe-ref=${safe_ref}"
echo "release-tag=${release_tag}"
echo "release-name=${release_name}"
echo "version=${version}"
} >> "$GITHUB_OUTPUT"
reuse:
name: REUSE Compliance
needs: init
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
@ -71,7 +80,7 @@ jobs:
uses: fsfe/reuse-action@v6
build:
name: Build Archive
name: Build win-x64
needs:
- init
- reuse
@ -80,7 +89,6 @@ jobs:
DOTNET_NOLOGO: true
NUGET_PACKAGES: ${{ github.workspace }}\.nuget\packages
PUBLISH_DIR: ${{ github.workspace }}\artifacts\publish\win-x64
RELEASE_DIR: ${{ github.workspace }}\artifacts\release
steps:
- name: Checkout repository
uses: actions/checkout@v6
@ -92,69 +100,244 @@ 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
- name: Publish win-x64 CLI
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r win-x64 --self-contained true --no-restore -p:PublishDir="${env:PUBLISH_DIR}"
- name: Create release archive
run: |
New-Item -ItemType Directory -Path $env:RELEASE_DIR -Force | Out-Null
$archivePath = Join-Path $env:RELEASE_DIR "${{ needs.init.outputs.archive-name }}"
if (Test-Path $archivePath) {
Remove-Item $archivePath -Force
}
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
- 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.PUBLISH_DIR }}
if-no-files-found: error
include-hidden-files: true
build-posix:
name: Build ${{ matrix.rid }}
needs:
- init
- reuse
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-26.04
rid: linux-x64
- os: macos-latest
rid: osx-x64
env:
DOTNET_NOLOGO: true
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
SPIRV_TARGET_ENV: vulkan1.2
SPIRV_TOOLS_COMMIT: 0539c81f69a3daeb706fd3477dca61435b475156
SPIRV_TOOLS_VERSION: v2026.2
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.103
cache: true
cache-dependency-path: |
Directory.Packages.props
Directory.Build.props
- name: Restore solution
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: Build pinned SPIRV-Tools
if: matrix.rid == 'linux-x64'
run: |
git clone --no-checkout --filter=blob:none https://github.com/KhronosGroup/SPIRV-Tools.git "$RUNNER_TEMP/spirv-tools"
git -C "$RUNNER_TEMP/spirv-tools" checkout --detach "$SPIRV_TOOLS_COMMIT"
test "$(git -C "$RUNNER_TEMP/spirv-tools" rev-parse HEAD)" = "$SPIRV_TOOLS_COMMIT"
git clone --no-checkout --filter=blob:none https://github.com/KhronosGroup/SPIRV-Headers.git "$RUNNER_TEMP/spirv-tools/external/spirv-headers"
git -C "$RUNNER_TEMP/spirv-tools/external/spirv-headers" checkout --detach "$SPIRV_HEADERS_COMMIT"
test "$(git -C "$RUNNER_TEMP/spirv-tools/external/spirv-headers" rev-parse HEAD)" = "$SPIRV_HEADERS_COMMIT"
cmake -S "$RUNNER_TEMP/spirv-tools" -B "$RUNNER_TEMP/spirv-tools-build" \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DSPIRV_SKIP_TESTS=ON \
-DSPIRV_WERROR=OFF
cmake --build "$RUNNER_TEMP/spirv-tools-build" --target spirv-val
- name: Generate and validate synthetic SPIR-V
if: matrix.rid == 'linux-x64'
run: |
dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
scripts/validate-synthetic-spirv.sh \
"$RUNNER_TEMP/spirv-tools-build/tools/spirv-val" \
"$SPIRV_TOOLS_VERSION" \
"$SPIRV_TARGET_ENV" \
artifacts/shader-dump
- 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"
- name: Stage MoltenVK next to the build
if: matrix.rid == 'osx-x64'
run: scripts/fetch-macos-moltenvk.sh "$PUBLISH_DIR"
- name: Upload build artifact
uses: actions/upload-artifact@v7
with:
name: sharpemu-${{ matrix.rid }}-${{ needs.init.outputs.short-sha }}
path: ${{ env.PUBLISH_DIR }}
if-no-files-found: error
include-hidden-files: true
release:
name: Publish GitHub Release
needs:
- init
- build
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
- 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-26.04
permissions:
contents: write
steps:
- name: Download build artifact
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Download build artifacts
uses: actions/download-artifact@v8
with:
name: ${{ needs.init.outputs.artifact-name }}
path: release
- name: Create or update release
- name: Package release assets
shell: bash
env:
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
VERSION: ${{ needs.init.outputs.version }}
run: |
set -euo pipefail
win_dir="release/sharpemu-win-x64-${SHORT_SHA}"
linux_dir="release/sharpemu-linux-x64-${SHORT_SHA}"
macos_dir="release/sharpemu-osx-x64-${SHORT_SHA}"
for package_dir in "${win_dir}" "${linux_dir}" "${macos_dir}"; do
test -d "${package_dir}"
done
mkdir -p release-assets
(cd "${win_dir}" && zip -q -r "../../release-assets/sharpemu-${VERSION}-win-x64.zip" .)
chmod +x "${linux_dir}/SharpEmu" "${macos_dir}/SharpEmu"
tar -czf "release-assets/sharpemu-${VERSION}-linux-x64.tar.gz" -C "${linux_dir}" .
tar -czf "release-assets/sharpemu-${VERSION}-osx-x64.tar.gz" -C "${macos_dir}" .
- name: Build release notes
shell: bash
env:
MAX_COMMITS: 200
RELEASE_TAG: ${{ needs.init.outputs.release-tag }}
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
SHORT_SHA: ${{ needs.init.outputs.short-sha }}
VERSION: ${{ needs.init.outputs.version }}
run: |
set -euo pipefail
previous_tag="$(git describe --tags --abbrev=0 --match 'v*' "${RELEASE_TAG}^" 2>/dev/null || true)"
if [ -n "${previous_tag}" ]; then
range="${previous_tag}..${RELEASE_TAG}"
else
range="${RELEASE_TAG}"
fi
git log --no-merges --reverse --pretty=tformat:"%H%x1f%h%x1f%s" "${range}" > commits.txt
total="$(wc -l < commits.txt)"
{
printf 'Automated SharpEmu v%s build for commit [`%s`](%s/commit/%s).\n\n' \
"${VERSION}" "${SHORT_SHA}" "${REPO_URL}" "${GITHUB_SHA}"
if [ -n "${previous_tag}" ]; then
printf '## Changes since %s\n\n' "${previous_tag}"
else
printf '## Changes\n\n'
fi
if [ "${total}" -eq 0 ]; then
printf '_No commits since %s._\n' "${previous_tag}"
else
head -n "${MAX_COMMITS}" commits.txt | while IFS=$'\x1f' read -r sha short subject; do
printf -- '- %s ([`%s`](%s/commit/%s))\n' "${subject}" "${short}" "${REPO_URL}" "${sha}"
done
if [ "${total}" -gt "${MAX_COMMITS}" ]; then
printf '\n_…and %s more commits._\n' "$((total - MAX_COMMITS))"
fi
fi
printf '\n'
if [ -n "${previous_tag}" ]; then
printf '**Full changelog**: %s/compare/%s...%s\n' "${REPO_URL}" "${previous_tag}" "${RELEASE_TAG}"
else
printf '**Full changelog**: %s/commits/%s\n' "${REPO_URL}" "${RELEASE_TAG}"
fi
} > release-notes.md
cat release-notes.md
- 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 }}
run: |
asset_path="release/${ARCHIVE_NAME}"
notes="Automated Windows build for commit ${GITHUB_SHA}."
mapfile -t assets < <(find release-assets -maxdepth 1 -type f \( -name '*.zip' -o -name '*.tar.gz' \) | sort)
if [ "${#assets[@]}" -ne 3 ]; then
echo "Expected 3 release assets, found ${#assets[@]}." >&2
exit 1
fi
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-file release-notes.md

5
.gitignore vendored
View File

@ -32,6 +32,8 @@ packages/
.nuget/
.dotnet-home/
.cache/
__pycache__/
*.py[cod]
.DS_Store
Thumbs.db
@ -40,3 +42,6 @@ ehthumbs.db
.vs/
.idea/
.vscode/
clean_test/
.debug/

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:
@ -21,6 +26,25 @@ Before opening a pull request, please keep the following in mind:
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
## Pull Request Expectations
Pull requests should provide real, observable emulator behavior rather than only suppressing errors or unresolved imports.
Changes that only return success, zero, or fabricated handles without implementing the expected state, output, or side effects will generally not be accepted. Functions that create resources, write output structures, register callbacks, or expose runtime state should model the behavior required by the guest.
When applicable, PRs should include:
- The affected game or application.
- Relevant logs or failing imports.
- Behavior before and after the change.
- Real game testing and known limitations.
Avoid submitting large collections of speculative NIDs or unrelated exports. Keep each PR focused on one problem or a closely related set of changes.
Large architectural changes should be discussed with the maintainers before implementation. Contributors are encouraged to ask first when they are uncertain whether a proposed direction fits the project.
Opening a PR does not guarantee that it will be merged. Maintainers evaluate changes based on correctness, evidence, testing, scope, maintenance cost, and the long-term direction of the project.
## AI-Assisted Contributions
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.

View File

@ -9,10 +9,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<SharpEmuVersion>0.0.5-nexus</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And $([MSBuild]::IsOSPlatform('Windows'))">win</_HostRidOSPrefix>
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('Linux'))">linux</_HostRidOSPrefix>
<_HostRidOSPrefix Condition="'$(RuntimeIdentifier)' == '' And '$(MSBuildProjectName)' == 'SharpEmu.CLI' And '$(_HostRidOSPrefix)' == '' And $([MSBuild]::IsOSPlatform('OSX'))">osx</_HostRidOSPrefix>
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">arm64</_HostRidArch>
<_HostRidArch Condition="'$(_HostRidOSPrefix)' != '' And '$(_HostRidArch)' == ''">x64</_HostRidArch>
<RuntimeIdentifier Condition="'$(_HostRidOSPrefix)' != ''">$(_HostRidOSPrefix)-$(_HostRidArch)</RuntimeIdentifier>
<BaseIntermediateOutputPath>$(RepoRoot)artifacts/obj/$(MSBuildProjectName)/</BaseIntermediateOutputPath>
<BaseOutputPath>$(RepoRoot)artifacts/bin/</BaseOutputPath>

View File

@ -7,16 +7,28 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.3.18" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
<PackageVersion Include="Avalonia" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.0" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.0" />
<PackageVersion Include="FFmpeg.AutoGen" Version="7.1.1" />
<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="NLayer" Version="1.14.0" />
<PackageVersion Include="ppy.SDL3-CS" Version="2026.629.0" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
<!-- Reuse the font rasterizer already shipped with Avalonia.Skia. -->
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageVersion Include="System.IO.Hashing" Version="10.0.0" />
<!-- Transitive of Avalonia.Desktop; pinned. Avalonia 12 requires 0.94.1+. -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.1" />
</ItemGroup>
</Project>
</Project>

202
LICENSES/Apache-2.0.txt Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

113
README.md
View File

@ -13,20 +13,36 @@ SPDX-License-Identifier: GPL-2.0-or-later
An experimental PlayStation 5 emulator for Windows, Linux and macOS.
</p>
---
<p align="center">
<a href="https://discord.gg/6GejPEDqpc">
<a href="https://discord.gg/zdTuUU9Uwn">
<img src="https://img.shields.io/badge/Discord-Join%20our%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join our Discord">
</a>
</p>
<p align="center">
<strong>Join our Discord for development updates, compatibility discussions, support, and community chat.</strong>
<strong>Join our official Discord server for development updates, compatibility discussions, support, and community chat.</strong>
</p>
---
> [!WARNING]
> Currently the primary development target is Windows.
<p align="center">
<a href="#support">
<img src="https://img.shields.io/badge/Support-GitHub%20Sponsors%20%26%20Crypto-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Support SharpEmu">
</a>
</p>
---
> [!IMPORTANT]
> **Official channels:** Only **sharpemu.app** and the links listed in this GitHub repository are affiliated with SharpEmu. Any other websites, accounts, or donation pages are unofficial and unauthorized.
> [!NOTE]
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
> can run the macOS x64 build through Rosetta 2, and Windows on ARM devices
> (e.g. Snapdragon) can run the Windows x64 build through Windows' built-in
> x64 emulation.
> [!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.
@ -40,52 +56,55 @@ 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
| Demon's Souls Remake | ASTRO BOT |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![DeS screenshot](./.github/images/demons-souls.jpg) | ![Astro Bot](./.github/images/astro-bot.jpg) |
| ASTRO's PLAYROOM | Dead Cells |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Astro's Playroom](./.github/images/astros-playroom.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
| God of War: Sons of Sparta | Tomb Raider V Remastered |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![GoW SOS](./.github/images/gow-sos.jpg) | ![Tomb Raider V](./.github/images/tomb-raider-v-remastered.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.
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle gpu-related functionality. Included 3D games.
Current capabilities include:
* Loading `eboot.bin` and `.elf` files
* Executing native CPU instructions
* Reading basic game metadata (title, version, etc.)
* Loading system modules (`prx` / `sys_module`)
* Partial support for some kernel functions
* `Fiber` and `AMPR` exports
* PlayGo scenarios
* Initial loading game files
* Shader/resource submits and AGC initial
* Video outputs in some games
Some games have reached like `sceVideoOut` and AGC stages.
Currently the project primarily targets Windows. Cross-platform support (Linux and macOS) is planned, but development is currently focused on Windows to simplify early-stage debugging and iteration.
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
* Build or Publish project or download in release tab.
* Open Powershell.
* Run Emulator GUI.
* Or command: `.\SharpEmu "eboot.bin" 2>&1 | Tee-Object -FilePath "log.txt"`
Download the release archive for your operating system, extract it, and launch
SharpEmu with the path to a legally obtained game's `eboot.bin`.
## Games Tested
Or command line;
* **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)
Windows PowerShell:
* **Poppy Playtime Chapter 1**
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/par274/sharpemu/issues/3)
```powershell
.\SharpEmu.exe "C:\path\to\game\eboot.bin" --log-to-file
```
* **SILENT HILL: The Short Message**
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/par274/sharpemu/issues/4)
Linux and macOS:
* **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)
```bash
chmod +x ./SharpEmu
./SharpEmu "/path/to/game/eboot.bin" --log-to-file
```
SharpEmu supports environment variables that can be configured from the GUI. Undocumented variables are listed here: [docs/sharpemu-gui-undocumented-env-vars.md](docs/sharpemu-gui-undocumented-env-vars.md)
You can set them per game or globally in the GUI, or pass them directly through the CLI.
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.
@ -94,8 +113,8 @@ Currently the project primarily targets Windows. Cross-platform support (Linux a
## Build
1. Install the **.NET SDK**.
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.
@ -121,7 +140,19 @@ 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)
## Support
Support SharpEmu via GitHub Sponsors or cryptocurrency. Every contribution helps fund ongoing development and long-term maintenance. GitHub Sponsors is the preferred way to support the project, but cryptocurrency donations are also appreciated.
### ETH/USDT
`0xF315F5d986c790bB3A58DbE60F1B2760997dEd82`
### BTC
`bc1qmr9k8899njys5ny63xsues4jgmkk96erslrkmv`
## Contributing

View File

@ -7,10 +7,12 @@ path = [
"global.json",
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
"src/SharpEmu.GUI/Languages/**",
"src/SharpEmu.ShaderCompiler.Metal/Templates/**",
"tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/**",
"_logs/**",
".github/images/**",
".github/pull_request_template.md",
"assets/images/**"
]
precedence = "aggregate"
@ -18,7 +20,13 @@ SPDX-FileCopyrightText = "SharpEmu Emulator Project"
SPDX-License-Identifier = "GPL-2.0-or-later"
[[annotations]]
path = "src/SharpEmu.GUI/Atrac9/**"
path = "src/SharpEmu.GUI/Assets/Fonts/MaterialSymbolsRounded-*.ttf"
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 Google LLC"
SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = "src/SharpEmu.LibAtrac9/**"
precedence = "aggregate"
SPDX-FileCopyrightText = "2018 Alex Barney"
SPDX-License-Identifier = "MIT"

View File

@ -5,11 +5,24 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Solution>
<Folder Name="/src/">
<Project Path="src/SharpEmu.LibAtrac9/SharpEmu.LibAtrac9.csproj" />
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
<Project Path="src/SharpEmu.DebugClient/SharpEmu.DebugClient.csproj" />
<Project Path="src/SharpEmu.Debugger/SharpEmu.Debugger.csproj" />
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
<Project Path="src/SharpEmu.ShaderCompiler.Metal/SharpEmu.ShaderCompiler.Metal.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.ShaderCompiler.Metal.Tests/SharpEmu.ShaderCompiler.Metal.Tests.csproj" />
<Project Path="tests/SharpEmu.ShaderCompiler.Tests/SharpEmu.ShaderCompiler.Tests.csproj" />
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
</Folder>
</Solution>

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

BIN
assets/images/discord.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 698 B

BIN
assets/images/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 656 KiB

BIN
assets/images/logo.psd Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
assets/images/pic0.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

23
docs/aerolib-catalog.md Normal file
View File

@ -0,0 +1,23 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Aerolib Catalog
```bash
# NID to export name
python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk
# Export name to NID
python scripts/aerolib_catalog.py lookup sceKernelWaitSema
# A comma can be used for multiple query strings.
python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk,SaKib2Ug0yI,sceVideoOutOpen,amuBfI-AQc4
# Search export names
python scripts/aerolib_catalog.py search VideoOut --limit 20
# Export all NID/name pairs to artifacts/aerolib.txt
python scripts/aerolib_catalog.py export
```

66
docs/bink2-bridge.md Normal file
View File

@ -0,0 +1,66 @@
<!--
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.
By default, SharpEmu leaves Bink decoding to the implementation linked into
the game (`SHARPEMU_BINK_MODE=guest`).
Set `SHARPEMU_BINK_MODE=native` to use the optional host bridge. It observes
guest .bk2 opens and presents decoded frames at the guest-flip boundary. The
host path decodes by calling FFmpeg's C API directly from managed
code (`src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs`, via the
[FFmpeg.AutoGen](https://github.com/Ruslan-B/FFmpeg.AutoGen) P/Invoke
bindings) against a custom FFmpeg build
(`github.com/sharpemu/ffmpeg-core`, LGPL-2.1) that adds a Bink 2 decoder to
FFmpeg 7.1.2; see "Supplying the FFmpeg libraries" below for where those
libraries come from. No proprietary RAD SDK is needed to build or run
SharpEmu, and there is no C/C++ code of SharpEmu's own involved in decoding
-- SharpEmu.CLI.csproj only downloads a prebuilt release archive.
Set `SHARPEMU_BINK_MODE=skip` only when explicitly testing a title whose
cinematics are optional.
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.
`SHARPEMU_BINK_MODE=ffmpeg` is an alias for `native`.
## Supplying the FFmpeg libraries
Both `dotnet build` and `dotnet publish` fetch a prebuilt release of
`github.com/sharpemu/ffmpeg-core` (the tag is pinned in
`SharpEmu.CLI.csproj`'s `FfmpegRuntimeTag`, matched to the `FFmpeg.AutoGen`
package version in `Directory.Packages.props` -- both need to agree on the
same FFmpeg ABI) and copy its dynamically linked libraries into a `plugins`
folder next to the resulting executable (`artifacts/bin/...` for build,
`artifacts/publish/...` for publish). No C toolchain is required to build
SharpEmu; both just download a zip once (cached under
`$(BaseIntermediateOutputPath)ffmpeg-runtime/`, so later builds/publishes
reuse it instead of re-fetching). `plugins` is a loose, unpacked folder
rather than something embedded in the single-file bundle, so the OS loader
can resolve the libraries' own inter-dependencies (`avcodec` depends on
`avutil`, etc.) itself.
A plain `dotnet build`/`dotnet publish` with no `-r` still works: it defaults
to the host machine's own RID (see `Directory.Build.props`), so it fetches
the matching `ffmpeg-core` archive and populates `plugins` without any extra
flags. Passing an explicit `-r <rid>` (e.g. to cross-publish `linux-x64` from
Windows) still overrides that default normally.
To use a different set of FFmpeg libraries, drop them into the build or
published `plugins` folder yourself (matching FFmpeg's own file-naming and versioning
conventions, e.g. `avformat-61.dll` / `libavformat.so.61` / matching
`.dylib`) -- `FfmpegNativeBinkFrameSource` points `ffmpeg.RootPath` at that
folder and does not otherwise care where the files came from.
If the libraries are absent or fail to load, `FfmpegNativeBinkFrameSource.TryOpen`
degrades gracefully: SharpEmu logs one informational line ("Bink2 bridge
could not open movie ...") and leaves the guest's own rendering path
untouched, rather than crashing.

View File

@ -0,0 +1,80 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Shader rejection diagnostics
`SHARPEMU_STRICT_COMPUTE` controls the rejections listed below for compute dispatches
and graphics draws. The existing variable name is retained; there is no separate
`SHARPEMU_STRICT_DRAW` switch. Restart the game after a change. The pipeline cache and
draw executor read the value when created.
- Unset or `1`: stop on the rejection so that the failure can be diagnosed (default).
- `0`: skip the affected dispatch or draw. Only this value enables skipping.
The GUI provides this switch under **Environment > Debug**, globally and per game.
It is on by default. Turning it off saves an explicit `0`; an inherited process
environment value does not override the GUI choice. Other values retain strict mode.
## Complete skip list
With the switch set to `0`, these are the only handled rejection categories:
| Rejection | Detection point | Operation skipped |
| --- | --- | --- |
| Resource-plan extraction throws `ResourcePlanException`, including an unsupported resource expression or a value graph that does not converge | `ShaderProgramCache.CreateEntry` | The dispatch or draw that needs the rejected shader |
| Shader compilation returns failure or no compiled program, including an unsupported opcode reported by the compiler | `ShaderProgramCache.CompilePermutation` | The dispatch or draw that needs the rejected shader |
| Indirect image candidates have incompatible image classes (`IncompatibleImageCandidates`) | `ResourceMaterializer.Materialize`, handled by `ShaderProgramCache` | The dispatch or draw that needs those resources |
| Indirect image expansion exceeds the dense image resource limit (`ImageCapacityExceeded`) | `ResourceMaterializer.Materialize`, handled by `ShaderProgramCache` | The dispatch or draw that needs those resources |
| A graphics texture's cached image type cannot support the requested view type, such as a 2D view of a 1D image | Vulkan `ValidateDrawImageTypes`, handled by `RenderExecutor.RecordDraw` | The whole indexed or automatic draw |
The last category applies to vertex and pixel resource preparation, not compute
image binding. It checks available, non-stale cache entries and excludes host movie
textures. A mismatch detected later in binding remains fatal.
The draw image-type check runs before resource binding and draw recording. Skip mode
does not create an invalid view or issue the rejected draw. The preparation scope is
released, and the caller resets the image bindings before the next operation.
## Failures that remain fatal
- Shader decoding failures and invalid shader source reads.
- Unreadable resource memory, invalid indirect table state, and materialization failures
other than the two named categories above.
- Invalid binding layouts or invalid resource snapshot state.
- Other image checks, including format, usage, aspect, component mapping, and range
checks reached during binding or view creation. The image cache checks are unchanged.
- Image-type mismatches in compute binding or outside the graphics preparation check.
- Host allocation failures, device loss, synchronization failures, and unexpected exceptions.
This is not a general exception bypass. Setting the switch to `1` also makes every
category in the skip list fatal. Supported operations use the same path in both modes.
## Warnings and test limits
Program rejections log `COMPUTE_SKIPPED` or `DRAW_SKIPPED` once per stage, shader hash,
and code size in each pipeline cache. Draw image-type rejections log `DRAW_SKIPPED`
once per shader hash, image type, and view type in each draw executor. Warning counts
are not skipped-operation counts. No rejected permutation is recorded as compiled;
a later request can retry it.
A skipped operation does not produce its outputs. Missing images, incorrect results,
later errors, and stalls are possible. FPS can rise because less work is executed.
Skip-mode smoke tests can check progress and expose other defects, but must record
the skip warnings. They do not close the skipped defects or establish a final visual
or performance baseline. This switch does not restore the old renderer.
## PowerShell
Enable strict testing in PowerShell:
```powershell
$env:SHARPEMU_STRICT_COMPUTE = '1'
```
Explicitly enable skip mode:
```powershell
$env:SHARPEMU_STRICT_COMPUTE = '0'
```

177
docs/debugger-server.md Normal file
View File

@ -0,0 +1,177 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Live debug server
SharpEmu can expose a **live debug server** so an external process can inspect
and control a running guest over TCP. The server lives in the emulator; the
companion `SharpEmu.DebugClient` executable is one client, and the wire protocol
is simple enough to script against directly.
This document describes the moving parts and the wire protocol. For day-to-day
client usage, see
[`src/SharpEmu.DebugClient/DEVELOPER_READ.md`](../src/SharpEmu.DebugClient/DEVELOPER_READ.md).
## Layering
| Assembly | Role |
| -------- | ---- |
| `SharpEmu.Core` | Defines the dispatcher seam `ICpuDebugHook` / `ICpuDebugFrame` (namespace `SharpEmu.Core.Cpu.Debug`) and the `CpuExecutionOptions.DebugHook` slot. Core has **no** reference to the debugger. |
| `SharpEmu.Debugger` | The debugger: `DebuggerSession` (implements the hook), `BreakpointStore`, the TCP `DebuggerServer`, the pluggable `IDebugProtocol` with a JSON-lines implementation, and the `DebuggerServerHost` one-call wiring. |
| `SharpEmu.CLI` | Parses `--debug-server`, builds a `DebuggerServerHost`, hands its `Hook` to `SharpEmuRuntimeOptions.DebugHook`, and manages its lifetime. |
| `SharpEmu.DebugClient` | A standalone client executable. Depends only on the BCL. |
The dependency direction is important: Core stays debugger-agnostic and only
publishes the seam. Anything that observes execution implements
`ICpuDebugHook` and is injected through the options, so the debugger can evolve
without touching the CPU core.
## Execution model
`CpuDispatcher` enters a fresh frame for the process entry point and for each
module initializer. When a `DebugHook` is attached it is notified at those
boundaries:
- `OnFrameEnter(frame)` — before the native backend runs the frame. The
`DebuggerSession` decides whether to stop (pause request, breakpoint on the
entry address, single-step, or stop-at-entry). To stop, it **parks the
emulation thread** inside this call on a gate; the frame stays live, so a
client can read and write registers and memory while parked. `continue` /
`step` release the gate.
- `OnFrameExit(frame, result)` — after the frame completes.
Because pausing parks the one thread that owns the guest context, register and
memory accessors are only served while the session reports `Paused`; otherwise
they return "not paused" so a client never observes torn state.
### What is and isn't live yet
- **Live:** attach/handshake, run-state tracking, register read/write, memory
read/write, breakpoint management, execution breakpoints at frame entry,
pause, frame-level step, continue, and stop/resume/terminate events.
- **Surface only (armed as the backend grows hooks):** per-instruction
stepping and data watchpoints (`readwatch` / `writewatch` / `accesswatch`).
The verbs and types exist so clients and tooling can be written now.
## Enabling the server
```bash
SharpEmu --debug-server "/path/to/eboot.bin" # 127.0.0.1:5714
SharpEmu --debug-server=0.0.0.0:5714 "/path/to/eboot.bin"
```
The bind address defaults to loopback; a routable address must be given
explicitly. With stop-at-entry (the default `DebuggerSessionOptions.StopAtEntry`),
the guest parks at its first frame until a client connects and issues
`continue`, giving you a window to set breakpoints before any guest code runs.
## Browser frontend
The dependency-free Python frontend can choose and launch an `eboot.bin`, attach
to its debugger automatically, and provides execution controls, registers,
memory inspection, breakpoint management, process output, and a live protocol
activity stream:
```bash
./tools/SharpEmu.DebuggerFrontend/run.sh
```
It connects to `127.0.0.1:5714` and opens `http://127.0.0.1:8765/` by default.
See [`tools/SharpEmu.DebuggerFrontend/README.md`](../tools/SharpEmu.DebuggerFrontend/README.md)
for configuration and testing options.
## Wire protocol (json-lines/1)
One JSON object per line, UTF-8, `\n`-terminated, in both directions.
### Requests
A `command` string plus command-specific fields. Numeric fields accept a JSON
number or a `0x`-prefixed hex string.
| `command` | Fields | Reply `data` |
| --------- | ------ | ------------ |
| `ping` | — | — |
| `status` (`info`) | — | `state`, `breakpoints`, `lastStop?` |
| `state` | — | `state` |
| `registers` (`regs`) | — | `registers` (rax..r15, rip, rflags, fs_base, gs_base) |
| `set-register` | `register`, `value` | — |
| `read-memory` | `address`, `length` (≤ 65536) | `address`, `length`, `bytes` (hex) |
| `write-memory` | `address`, `bytes` (hex) | `written` |
| `list-breakpoints` (`breakpoints`) | — | `breakpoints[]` |
| `add-breakpoint` (`break`) | `address`, `kind?`, `length?` | `breakpoint` |
| `remove-breakpoint` (`delete-breakpoint`) | `id` | — |
| `enable-breakpoint` | `id`, `enabled?` (default true) | — |
| `continue` (`cont`, `c`) | — | — |
| `step` (`s`) | — | — |
| `pause` | — | — |
### Replies
```json
{"ok":true,"command":"registers","data":{ "registers": { "rax":"0x…", … } }}
{"ok":false,"command":"read-memory","error":"Target is not paused."}
```
### Events (unsolicited)
```json
{"event":"hello","protocol":"json-lines/1","state":"Paused"}
{"event":"stopped","reason":"Breakpoint","address":"0x…","frameKind":"ProcessEntry","frameLabel":"eboot.bin","registers":{…},"breakpoint":{…}}
{"event":"resumed"}
{"event":"terminated"}
```
`reason` is one of `EntryPoint`, `Breakpoint`, `Watchpoint`, `Step`, `Pause`,
`Fault`, or `Stall`.
Stall stops include structured evidence in addition to the human-readable
detail. Import-loop evidence identifies the NID, resolved HLE export, repeating
guest return site, dispatch count, and first two ABI arguments:
```json
{
"event": "stopped",
"reason": "Stall",
"stall": {
"kind": "ImportLoop",
"nid": "9UK1vLZQft4",
"instructionPointer": "0x0000000801CE2418",
"dispatchIndex": 40667904,
"argument0": "0x0000000812345000",
"argument1": "0x0000000000000000",
"resolved": true,
"library": "libKernel",
"function": "scePthreadMutexLock"
}
}
```
The Python frontend uses this evidence to explain the likely failure class and
rank concrete checks/fixes. Its diagnosis is intentionally labelled heuristic:
it helps locate the responsible HLE/scheduler path but does not replace tracing.
## Swapping the protocol
`DebuggerServer` takes an `IDebugProtocol` factory. The default is
`JsonLineDebugProtocol`; a GDB remote serial stub (or any other framing) can be
dropped in without changing the session or command semantics, which live in
`DebugCommandDispatcher`.
## Embedding the server
```csharp
using SharpEmu.Debugger;
using SharpEmu.Core.Runtime;
await using var host = new DebuggerServerHost();
host.Start();
var options = new SharpEmuRuntimeOptions { DebugHook = host.Hook };
using var runtime = SharpEmuRuntime.CreateDefault(options);
var result = runtime.Run(ebootPath);
host.NotifyRunCompleted();
```

57
docs/guest-write-watch.md Normal file
View File

@ -0,0 +1,57 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Guest write watch
`GuestWriteWatch` is an optional diagnostic tool. It helps you find managed
code and HLE code that damage guest memory. The tool starts only if you set one
or more `SHARPEMU_WATCH_*` environment variables.
The tool monitors writes through the SharpEmu managed virtual-memory APIs. It
does not monitor stores that native guest code makes directly. Use a platform
debugger or a hardware watchpoint to monitor these stores.
## Watch modes
- `SHARPEMU_WATCH_WRITE=0x<address>` logs a write that overlaps the eight-byte
block at the specified guest address.
- `SHARPEMU_WATCH_POOL_HEADER=1` monitors the pointer at offset `0x40`. It
monitors the first 64 direct mappings that have a size of 64 KiB and
protection value `0xF2`.
- `SHARPEMU_WATCH_VALUE_PATTERN=1` logs an eight-byte write if its lower 32 bits
are `1`. The upper 32 bits must look like a small guest-pointer prefix.
- `SHARPEMU_WATCH_VALUE1=1` logs short writes of value `1` in the high guest
memory range. The tool logs a maximum of 128 entries for each process.
- `SHARPEMU_WATCH_BULK_TORN=1` scans aligned 64-bit words in bulk writes. It
finds damaged pointer patterns and byte-shifted pointer patterns. The tool
logs a maximum of 64 entries for each process.
- `SHARPEMU_WATCH_BULK_DEST_HI=0x<high-dword>` scans only writes that have the
specified upper 32 bits in the destination address.
For each match, the tool logs the destination address, the data pattern, and the
managed call stack. The log uses the `watch_write` or `watch_bulk_torn` warning
tag.
Use these variables together to scan bulk writes in the
`0x00000080xxxxxxxx` region.
macOS and Linux:
```sh
SHARPEMU_WATCH_BULK_TORN=1 \
SHARPEMU_WATCH_BULK_DEST_HI=0x80 \
SharpEmu /path/to/eboot.bin
```
Windows PowerShell:
```powershell
$env:SHARPEMU_WATCH_BULK_TORN = "1"
$env:SHARPEMU_WATCH_BULK_DEST_HI = "0x80"
& .\SharpEmu.exe C:\path\to\game\eboot.bin
```
To reduce unnecessary log entries, use an exact `SHARPEMU_WATCH_WRITE`
address from a crash dump.

View File

@ -0,0 +1,54 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Performance display
Open **Options → Rendering**, globally or for one game.
- **Performance display:** start with statistics on or off.
- **Display mode:** Full shows all counters and the frame graph. Minimal shows
guest FPS, process CPU usage, process GPU usage, and elapsed session time.
Title bar shows the same minimal values in the window title.
- **Screen corner:** choose any of the four corners. This setting applies only
to the on-screen panel.
F1 toggles the display for the current run. F2 cycles clockwise through the
screen corners. Neither key changes saved settings. The next run uses the saved
options again. Command+F1 retains its separate Metal HUD shortcut.
`SHARPEMU_OVERLAY=0` starts the display hidden, even when the saved option is on.
F1 can still show it. RenderDoc's F12 hint remains in the title when available.
The operating system does not show a title bar in borderless/fullscreen modes.
The launcher passes these options to the game process, including when a separate
emulator path is configured:
```text
--overlay=on|off
--overlay-mode=full|minimal|titlebar
--overlay-corner=topleft|topright|bottomright|bottomleft
```
## Usage values
The panel dimensions are separate constants in `PerfOverlay.cs`:
`PanelWidth` / `PanelHeight` for Full, and
`MinimalPanelWidth` / `MinimalPanelHeight` for Minimal. Both heights are fixed;
the renderer clips the panel only when the window is too small.
CPU usage is the emulator process's CPU time divided by wall time and the
number of logical processors. GPU usage is the busiest GPU engine used by the
emulator process, not GPU memory usage or total usage by all applications.
Windows supplies the GPU Engine performance counters. Collection runs off the
render thread and uses the existing statistics refresh interval. Rate counters
need two samples. Missing counters, unavailable samples, and other platforms
show **N/A**, not zero. No GPU waits or presentation delays are added.
The full panel keeps cache memory counters and puts CPU/GPU usage on a separate
line above elapsed time and the Vulkan allocation counters.
The counter interpretation follows [Microsoft's GPU utilization description](https://devblogs.microsoft.com/directx/gpus-in-the-task-manager/).
Wildcard samples use [PdhGetFormattedCounterArray](https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhgetformattedcounterarrayw).

90
docs/release-use.md Normal file
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.

View File

@ -0,0 +1,433 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# SharpEmu: environment variables that are not on the GUI options pages
## Scope
This list contains each `SHARPEMU_*` environment variable in the repository that the GUI does not show.
The GUI shows 14 variables on its **Options** and **Game options** pages. This list does not contain them:
`SHARPEMU_BTHID_UNAVAILABLE`, `SHARPEMU_CRASH_CAPTURE`, `SHARPEMU_DEFAULT_PROFILE`,
`SHARPEMU_DISABLE_IMPORT_LOOP_GUARD`, `SHARPEMU_DUMP_SPIRV`, `SHARPEMU_LOG_DIRECT_MEMORY`, `SHARPEMU_LOG_IO`,
`SHARPEMU_LOG_NP`, `SHARPEMU_PROFILE_PERFORMANCE`, `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE`,
`SHARPEMU_RENDERDOC`, `SHARPEMU_VK_DISABLE_IMPLICITS`, `SHARPEMU_STRICT_COMPUTE`, `SHARPEMU_VK_VALIDATION`, `SHARPEMU_WRITABLE_APP0`.
The list contains 269 variables. The source was examined on 2026-09-19, branch `dev`.
The descriptions come from the code that reads each variable. The emulator was not started for this list.
## How to use a variable
1. Set the variable in the shell before you start the emulator or the GUI.
2. You can also enter additional env vars using the button next to the "Play" button from GUI.
3. The GUI also accepts a variable in the `EnvironmentToggles` list of its settings file. Write `NAME` or `NAME=value`. An entry without a value gets the value `1`.
4. The emulator reads most variables one time, when it starts. Start the game again after a change.
5. If the **Value** column shows `1`, only the value `1` turns the function on, unless the row gives other values.
## Number of variables in each group
| Group | Variables |
| --- | --- |
| Behavior switches | 19 |
| GPU and Vulkan | 59 |
| Metal | 10 |
| Audio and video | 6 |
| Input | 5 |
| Memory and CPU | 19 |
| Loader | 11 |
| Debugger | 1 |
| Log channels | 72 |
| Traces | 25 |
| Data dumps | 12 |
| Performance measurement | 17 |
| Internal | 1 |
| Tests and tools | 12 |
## Behavior switches
These variables change how the emulator operates.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_AGC_VERSION13_DEFAULTS` | text (`legacy`, `generic` or `0`) | Selects the register defaults for AGC version 13 requests. The default uses the version 11 compact tables. The values `legacy`, `generic` and `0` use the older generic layout. | `AgcExports.Init.cs` |
| `SHARPEMU_DISABLE_LLE_LIBC` | `1` | Stops the use of the guest libc module for libc exports. The emulator then uses its HLE handlers for these exports. | `DirectExecutionBackend.cs` |
| `SHARPEMU_HLE_FAST_MEMCPY` | `0` | Set to `0` to stop the native fast-path stub for the guest libc `memcpy` and `memmove`, so those imports use the managed HLE gateway again. | `DirectExecutionBackend.FastPath.cs` |
| `SHARPEMU_HLE_FAST_PATH` | `0` | Set to `0` to stop the native fast-path stubs for `pthread_self` and `pthread_getspecific`, so those imports use the managed HLE gateway again. The stubs are on by default on macOS x64 and are off on any run that traces imports or pthreads. | `DirectExecutionBackend.FastPath.cs` |
| `SHARPEMU_HOLD_FIRST_FLIP_MS` | number | Stops the thread that submits the flip for this number of milliseconds on one guest flip. The range is 0 to 60000 and the default is 0 (off). `SHARPEMU_HOLD_FLIP_NUMBER` selects the flip. | `VideoOutExports.cs` |
| `SHARPEMU_HOLD_FLIP_NUMBER` | number | Selects the guest flip that `SHARPEMU_HOLD_FIRST_FLIP_MS` holds. The minimum is 1 and the default is 1 (the first flip). | `VideoOutExports.cs` |
| `SHARPEMU_IGNORE_GUEST_EXCEPTIONS` | `1` | `sceKernelRaiseException` returns OK and does not call the installed guest exception handler. Default is off. | `KernelExceptionCompatExports.cs` |
| `SHARPEMU_IMPORT_LOOP_GUARD_SECONDS` | number | When `SHARPEMU_DISABLE_IMPORT_LOOP_GUARD=0` enables the guard, sets how long an import call pattern can repeat before a guest exit. The default is 5 seconds; 0 turns the guard off. | `DirectExecutionBackend.Imports.cs`, `DirectExecutionBackend.cs` |
| `SHARPEMU_NET_REDIRECT` | IP address | Sends guest `connect` and `sendto` traffic to this host IP address. When unset, the emulator permits outbound guest traffic only to loopback addresses. If the value is not a valid IP address, the emulator keeps the original destination and permits the traffic. | `KernelSocketCompatExports.cs` |
| `SHARPEMU_NO_FLIP_PACING` | `1` | Stops flip pacing. The emulator completes each flip immediately and does not wait for the display refresh time. The default is off. | `VideoOutExports.cs`, `VideoOutExports.FlipRequests.cs` |
| `SHARPEMU_OVERLAY` | `0` | Hides the performance overlay at start, even when the video options have the overlay on. All other values use the video options. | `PerfOverlay.cs`, `PerformanceOverlayState.cs` |
| `SHARPEMU_POSIX_GPU_FAULT_FAST_PATH` | `0` | Set to `0` to stop the POSIX fault bridge from offering GPU-tracked page faults to the GPU memory tracker before it builds the Win64 exception context. The fault then takes the full handler order again. | `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_RETAIN_SUBMITTED_INDEX_DATA` | `0` | The default is on. The emulator keeps a copy of submitted index data until the translated draws run. Set to `0` to stop this copy. Use `0` only for a comparison test. | `AgcExports.SubmittedGeometry.cs` |
| `SHARPEMU_RETAIN_SUBMITTED_VERTEX_DATA` | `0` | The default is on. The emulator keeps a copy of submitted vertex data until the translated draws run. Set to `0` to stop this copy. Use `0` only for a comparison test. | `AgcExports.SubmittedGeometry.cs` |
| `SHARPEMU_SAVEDATA_DIR` | path | Set the root directory for save data. The default is `user/savedata` next to the executable. When this variable is set, the emulator does not move save data from the old layout. | `SaveDataStorage.cs`, `SaveDataExports.cs` |
| `SHARPEMU_STALL_WATCHDOG_SECONDS` | number (seconds) | Sets the time without import progress before the stall watchdog operates. The default is 20. The watchdog writes a snapshot and stops the process with exit code 4. It does not operate when a guest thread is ready or a known wait is in progress. `0` stops the watchdog. | `DirectExecutionBackend.cs` |
| `SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE` | `1` | Set to `1` to make a guest thread take a free pthread rwlock as the exclusive writer. By default, the thread gets a compatibility writer record and the lock has no writer thread. | `KernelPthreadExtendedCompatExports.cs` |
| `SHARPEMU_TEMP0_DIR` | path | Set the host directory for the guest `temp0` mount. The default is `user/temp/<app name>/temp0` next to the executable. When unset, the kernel code sets this variable to the default path. | `AppContentExports.cs`, `KernelMemoryCompatExports.cs` |
| `SHARPEMU_TRACE_TITLE_SHADER_STATE` | `1` | The SPIR-V translator changes the pixel shader at address 0x0000000500781200 for diagnosis. The first float output then shows the execution state, the wave mask state, and the export state as colors. Default is off. | `Gen5SpirvTranslator.cs` |
## GPU and Vulkan
These variables change the GPU path or the Vulkan presenter.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_ASYNC_GUEST_READBACK` | `0` | `0` makes a guest thread that reads GPU-written buffer memory wait until the render thread has copied the data back. The default starts the copy on the render thread and lets the guest thread wait for it on a separate readback slot, so the render thread continues. | `GuestBufferCache.cs` |
| `SHARPEMU_CAPTURE_PIXEL_EXEC_PCS` | list (`pc:register` pairs) | At each given instruction PC, the pixel shader writes the execution flag to the given VGPR register. The value is 1.0 when the flag is set and 0.0 when it is not. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_ADDRESS` | hex address | Selects the pixel shader for the image capture by its program address. The default is no capture. Use it with `SHARPEMU_CAPTURE_PIXEL_IMAGE_PC`. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_PC` | number (instruction PC) | Sets the image instruction whose result the SPIR-V translator copies to four VGPR registers. `SHARPEMU_CAPTURE_PIXEL_IMAGE_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_VGPR_BASE` | number (0 to 252) | Sets the first VGPR register that receives the captured image components. The default is 248. Values above 252 use the default. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` | hex address | Selects the pixel shader for the VGPR capture, execution capture and path marker functions by its program address. The default is no capture. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_DEST_BASE` | number (0 to 252) | Sets the first destination VGPR register for `SHARPEMU_CAPTURE_PIXEL_VGPR_SOURCES`. The default is 248. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_IGNORE_EXEC` | `1` | Makes the VGPR capture copies occur without the execution flag guard. The default applies the guard. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_PC` | number (instruction PC) | Sets the instruction PC at which the translator copies the source VGPR registers. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_POINTS` | list (`pc:source:destination` triples) | At each given instruction PC, the pixel shader copies the source VGPR register to the destination register. Register numbers must be less than 256. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_SOURCES` | list (one to four register numbers) | Gives the source VGPR registers that the translator copies at `SHARPEMU_CAPTURE_PIXEL_VGPR_PC`. The copies go to the registers that start at the destination base. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_DETILE` | `1` or `0` | Controls the detile step for tiled surfaces. The default detiles only the trusted swizzle modes. `1` also detiles the approximate modes and `0` uploads all surfaces as raw data. | `GnmTiling.cs` |
| `SHARPEMU_DISABLE_FILL_CLEAR` | `1` | Stops the adaptation that treats a transparent premultiplied fill draw as a target overwrite. The default keeps the adaptation on. | `ShaderPipelineCache.Adaptations.cs` |
| `SHARPEMU_ENABLE_CHUNKED_DRAWS` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT` | number (attribute location) | The Vulkan presenter reads the number into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_DEFAULT_RASTER_STATE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_FULLSCREEN_PIPELINE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_FULLSCREEN_VERTEX` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_PACKED_EXPORT_ONE` | `1` | The SPIR-V translator replaces each packed half-float export component with the constant 1.0. This applies to all shaders. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PACKED_EXPORT_STORE_ONE` | `1` | The SPIR-V translator writes (1.0, 1.0) to the packed half-float pair before the export reads it. This applies only to the pixel shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PACKED_STORE_EXEC_VALUES` | `1` | Each packed half-float store writes 1.0 when the EXEC mask is active and 0.5 when it is not. This shows the EXEC state as a color. This applies only to the pixel shader at guest address 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` | hex address | Sets the shader address filter for the pixel export debug overrides. The value is a hex address, and the `0x` prefix is optional. When unset, the overrides apply to all pixel shaders. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_EXPORT_PACK_VGPR_BASE` | number | Float pixel outputs read four vector registers that start at this base plus four times the export target. The translator packs the values to half-float and unpacks them again. The address filters select the shaders. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_EXPORT_VGPR_ADDRESS` | hex address | Sets the shader address filter for the two VGPR base overrides. The `0x` prefix is optional. When unset, the filter from `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` applies. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_EXPORT_VGPR_BASE` | number | Float pixel outputs read four vector registers that start at this base plus four times the export target. These values replace the decoded export sources. The address filters select the shaders. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_MAGENTA` | `1` | Each selected pixel shader export writes opaque magenta. The control flow, EXEC mask, geometry and raster state stay the same. Use `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` to select one shader. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_SOLID_FRAGMENT` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_TITLE_COMPARE_4D4` | `1` | The compare instruction at shader PC 0x4D4 always gives the result true. This applies only to the shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.Alu.cs` |
| `SHARPEMU_FORCE_TITLE_COMPARE_540` | `1` | The compare instruction at shader PC 0x540 always gives the result true. This applies only to the shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.Alu.cs` |
| `SHARPEMU_FORCE_TITLE_DEFAULT_RASTER_STATE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_TITLE_EARLY_COLOR` | `1` | The pixel shader at guest address 0x0000000500781200 writes a constant to its first output and returns at once. A float output gets opaque magenta. An integer output gets zero. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_TITLE_EXPORT_EXEC` | `1` | The translator sets the EXEC mask to true and writes 1 to 64-bit register 126 before each pixel export store. This applies only to the pixel shader at guest address 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_TITLE_FULLSCREEN_VERTEX` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_TITLE_SINGLE_MRT` | `1` | The translator declares only the first pixel output for the pixel shader at guest address 0x0000000500781200. It does not declare the other render target outputs. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_TITLE_VERTEX_OUTPUTS_ONE` | `1` | Vertex export targets 32 to 35 write the vector (1, 1, 1, 1). This applies only to the vertex shader at guest address 0x0000000500780000. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_GPU_BACKEND` | `vulkan` or `metal` | Selects the guest GPU backend. The default is Vulkan. The value `metal` works only on macOS, and other hosts show a warning and use Vulkan. | `GuestGpu.cs` |
| `SHARPEMU_GPU_WAIT_SPIN_US` | number (microseconds) | Sets how long a wait for GPU work polls before it blocks. A short poll saves the wake-up delay when the GPU finishes soon. `0` blocks at once. The default is 150. | `GpuWaitSpin.cs` |
| `SHARPEMU_GRAPHICS_SUBGROUPS` | `0` or `1` | Set `1` to use native Vulkan subgroup operations in graphics shaders. Set `0` to use the fallback path. When unset, the presenter uses them only if the subgroup size is 32 and the vertex and fragment stages support subgroups. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.Device.Setup.cs` |
| `SHARPEMU_IMAGE_LOOKUP_MEMO` | `0` | `0` turns off the image lookup memo. The default keeps the result of each image lookup that found an image with the same backing, and gives it again for the same request until an image is registered or removed. | `GuestImageCache.Lookup.cs` |
| `SHARPEMU_MARK_PIXEL_PCS` | list of `pc:register` pairs | Makes the SPIR-V pixel shader write the float value 1.0 to a shader register when execution reaches the given instruction PC. Use decimal numbers, a register below 256, and a comma between pairs. It has an effect only for the shader address in `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS`. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_MAX_GUEST_WORK_PER_RENDER` | number | Sets the maximum number of guest command stream slices that the Vulkan presenter runs in one render pass. The default is 1024, or 256 on macOS. The value must be more than 0. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.CommandStream.cs` |
| `SHARPEMU_RENDERDOC_CAPTURE_TIMEOUT_SECONDS` | number (seconds) | Set the maximum time for a RenderDoc frame capture. The emulator discards a capture that does not end in this time. The range is 1 to 120. The default is 15. | `RenderDocCapture.cs` |
| `SHARPEMU_RENDERDOC_DLL` | path | Set the path of the RenderDoc library file. The emulator tries this path first. When unset or when the load fails, it searches the default library name and the known install paths. | `RenderDocCapture.cs` |
| `SHARPEMU_RENDERDOC_WAIT` | number (seconds) or `enter` | Set to make the emulator wait before it starts Vulkan, so that you can attach RenderDoc. `enter` waits until you press Enter. A number waits that many seconds, from 1 to 300. Other text waits 15 seconds. | `VulkanVideoPresenter.Device.Setup.cs` |
| `SHARPEMU_RENDER_WORK_BUDGET_MS` | number (milliseconds) | Set the maximum time that one render call uses for queued guest work in the Vulkan presenter. Work that remains stays in the queue for the next frame. `0` removes the limit. The default is 12 on macOS and 0 on other hosts. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.RenderLoop.cs` |
| `SHARPEMU_RESOURCE_PREFETCH` | `0` | `0` makes the shader resource materialization read each guest word one at a time. The default reads adjacent words of descriptors and the flattened resource table in one read of resident guest memory. A range that the GPU may still own is read word by word as before. | `ShaderProgramCache.cs`, `RawReadPrefetch.cs` |
| `SHARPEMU_SHADER_MAX_STEPS` | number | Set the maximum number of dispatcher loop steps in each translated shader invocation. The limit makes sure that an incorrect loop stops. `0` removes the limit. The default is 100000. | `Gen5SpirvTranslator.cs`, `Gen5MslTranslator.cs` |
| `SHARPEMU_SHADER_PREWARM` | `0` | `0` turns off the shader prewarm list. By default, each compute program that a game compiles is added at once to `shader-prewarm.bin` in `user/pipeline_cache/<title id>/`, so a stopped game keeps the list. At the next launch after a change of the shader translator or of the GPU driver, background threads translate the listed programs again and create their pipelines, so that the driver compiles them before the game asks for them. The `shader-prewarm.stamp` file names the build and driver that last did this; with the same build and driver, the launch does no prewarm work. The log shows `Shader prewarm done` and `Shader prewarm hits`. On Windows it replaces the cache file by default; on Linux it works beside the cache file. It has no effect with the per-shader cache files of `SHARPEMU_VK_PIPELINE_CACHE_SHARDS`. | `VulkanVideoPresenter.ShaderPrewarm.cs`, `ShaderPrewarmList.cs`, `ShaderProgramCache.cs` |
| `SHARPEMU_SKIP_ALL_COMPUTE` | `1` | Set to `1` to make the Metal presenter skip all compute dispatches. The Vulkan presenter reads the value but does not use it. The default is off. | `MetalVideoPresenter.Compute.cs`, `VulkanVideoPresenter.cs` |
| `SHARPEMU_SKIP_TALL_COMPUTE_Z` | number | Effect not clear from the code. The Vulkan presenter reads the number into a field for a minimum Z group count. No code uses that field. The default is 0. | `VulkanVideoPresenter.cs` |
| `SHARPEMU_SRT_NATIVE` | `0` | `0` turns off the compiled shader resource (SRT) evaluator. By default, the resource plan of each shader is compiled once to .NET code (`DynamicMethod`) on a low-priority background thread named `SRT compiler`; until it is ready, the plan is evaluated by the interpreter, so the first use of a new shader does not wait for the compilation. With `0`, every plan always uses the interpreter. The compiled path is also off when the runtime does not support dynamic code, for example in a Native AOT build. | `CompiledResourceEvaluator.cs`, `ShaderResourcePlan.cs`, `RuntimeValueEvaluator.cs` |
| `SHARPEMU_SUSPEND_POINTS_IN_FLIGHT` | `0` | `0` makes `sceAgcSuspendPoint` wait until the frame boundary it queued is processed. The default lets one boundary wait behind the graphics commands, so the guest CPU can run one frame ahead. A wait that follows its own queue's store to the same label still sees that value when the title rewrites the label for the next frame (Dead Cells). | `CommandStreamQueue.cs`, `GpuCommandInterpreter.cs` |
| `SHARPEMU_VK_DEBUG_LABELS` | `1` | Turns on Vulkan object names and command labels for capture tools. `SHARPEMU_VK_VALIDATION=1` also turns them on. Default is off because the labels add overhead to each draw. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_VK_DEVICE` | text (part of a device name) | Selects the Vulkan device whose name contains this text. The comparison ignores case. When unset, the presenter gives a score to each device and prefers a discrete GPU. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE_PATH` | path | Sets the path of the Vulkan pipeline cache file. The path can contain environment variables. Default is `user/pipeline_cache/<title id>/` below the application directory. The shader prewarm list goes to the same directory. With the per-shader cache, the files go to a `.shards` directory next to this path. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanPipelineCacheStorage.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE` | `0`, `1` or `file` | Selects the Vulkan pipeline cache file, which keeps the driver's compiled pipelines. `1` or `file` saves and loads one cache file. `0` keeps the cache in memory only on every system. The default is memory only on Windows: the GPU driver keeps its own shader cache on disk, and the shader prewarm list (`SHARPEMU_SHADER_PREWARM`) rebuilds it after an update. Linux keeps a cache file by default, and macOS keeps per-shader files. | `VulkanVideoPresenter.Device.Setup.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE_SHARDS` | `1` or `0` | `1` keeps one Vulkan pipeline cache file for each compute shader and for each vertex and pixel shader pair, in `vulkan-pipeline-cache.bin.shards`, also on Windows. `0` keeps the whole cache in one file. The default is `1` on macOS and `0` on other systems: NVIDIA adds about 2 MB of driver data to every cache file, so a file for each shader uses much more disk. A shard file loads when its pipeline is first created, and a save writes only the files that got new pipelines. The shard mode removes the old single file when `SHARPEMU_VK_PIPELINE_CACHE_PATH` is not set. A cache file that grows over 256 MiB is emptied, so the next launch starts it again. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.PipelineCacheShards.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE_LEGACY_SHARDS` | `1` | `1` names the per-shader cache files by the guest shader hash. The default names them by a hash of the translated SPIR-V, so a translator change does not load records that no longer match. | `VulkanVideoPresenter.PipelineCacheShards.cs` |
## Metal
These variables apply to the Metal backend.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_GPU_DETILE` | `0` | The Metal backend decodes supported tiled texture data on the GPU by default. Set `0` to decode all tiled textures on the CPU. | `MetalTextureSnapshots.cs`, `GuestGpuTypes.cs` |
| `SHARPEMU_LOG_GPU_DETILE` | `1` | Writes each texture tile mode and each GPU detile decision one time to stderr. It applies to the Metal texture path only. The emulator reads the value one time at start. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_METAL_CAP_DRAWABLE` | `1` | Limits the long edge of the Metal drawable to 1920 pixels and keeps the aspect ratio. The default is off, so the drawable uses the full window pixel size. | `MetalVideoPresenter.cs` |
| `SHARPEMU_METAL_DBG` | `solid` or `uv` | Replaces the Metal present fragment shader with a test shader. `solid` shows a green frame. `uv` shows the value of vertex attribute 0. All other values use the normal present shader. | `MetalVideoPresenter.cs` |
| `SHARPEMU_METAL_FULL_DRAWABLE` | `1` | Cancels `SHARPEMU_METAL_CAP_DRAWABLE=1`, so the Metal drawable uses the full window pixel size. It has no effect when the cap is off. | `MetalVideoPresenter.cs` |
| `SHARPEMU_NO_TEXTURE_SKIP` | `1` | Stops the reuse of cached texture snapshots in the Metal backend, so each texture is copied again. Use it to make sure that a snapshot is not stale. The default is off. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_REUSE_GUEST_TEXTURE_SNAPSHOTS` | `0` | The default is on. The Metal backend uses a guest texture snapshot again when the snapshot key is the same. Set to `0` to make a new snapshot each time. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_TEXTURE_DUMP_DIR` | path | Set a directory to make the Metal backend write raw texture source bytes to `.bin` files. The file name contains the address, size, pitch, format and tile mode. The limit is 200 files. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_TEXTURE_LINEAR_DUMP_DIR` | path | Set a directory to make the Metal backend write the linear texture bytes to `.linear.bin` files. The file name contains the address, size, pitch, format and tile mode. The limit is 200 files. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_TRACE_STORAGE_IMAGE_INIT_ADDRESS` | hex address | The Metal backend writes an `agc.storage_initial_data` trace line when it makes the texture at this guest address. The line shows the upload state and the read result of the initial data. | `MetalTextureSnapshots.cs` |
## Audio and video
These variables apply to audio output and video playback.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_ALSA_DEVICE` | text (ALSA device name) | Sets the ALSA playback device on Linux. The default is `default`. | `PosixAlsaAudioStream.cs` |
| `SHARPEMU_AUDIO_LATENCY_MS` | number (milliseconds) | Sets the target depth of the SDL audio queue, which is the playback latency. The default is 60. Values of zero or less use the default. | `SdlHostAudio.cs` |
| `SHARPEMU_BINK_MODE` | text (`native`, `ffmpeg`, `dummy`, `skip`, `guest`) | Selects how the emulator handles Bink movies. `native` or `ffmpeg` decodes on the host and is the default. `dummy` shows a placeholder frame, `skip` skips the movie, and `guest` lets the guest decode. | `HostMovieBridge.cs` |
| `SHARPEMU_LOG_MOVIE_SYNC` | `1` | Records the difference between the movie clock and the guest audio position during movie playback. The emulator reads the value one time at start. | `MediaFramePlayback.cs` |
| `SHARPEMU_MOVIE_CLOCK` | `wall` | Sets the time base for host-decoded movie playback. By default, playback follows the guest audio clock and uses the wall clock when no guest audio flows. `wall` makes playback always use the wall clock. | `MediaFramePlayback.cs` |
| `SHARPEMU_TRACE_AVPLAYER_IMAGES` | `1` | Set to `1` to write `[AVPLAYER][TRACE]` lines for video buffer addresses and the first 16 video frame payloads. It also marks the video buffer ranges for image traces in other components. The default is off. | `AvPlayerExports.cs` |
## Input
These variables apply to the controller and input libraries.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_AUTO_CROSS` | list (seconds, for example `40,52,64`) | Presses the Cross button for 0.4 seconds at each time in the list. The times are seconds from process start. The default is no automatic press. | `PadExports.cs` |
| `SHARPEMU_BTHID_CB_FAIL` | text (`nf` or `ni`) | Makes `sceBluetoothHidRegisterCallback` return an error. `nf` returns NOT_FOUND and `ni` returns NOT_IMPLEMENTED. The default returns the usual result. | `BluetoothHidExports.cs` |
| `SHARPEMU_BTHID_EVENT_CODE` | number (decimal or `0x` hex) | Sets the event code of the synthetic Bluetooth HID callback event. The default is 0. It has an effect only with `SHARPEMU_BTHID_FIRE_CALLBACK=1`. | `BluetoothHidExports.cs` |
| `SHARPEMU_BTHID_EVENT_SIZE` | number (bytes, decimal or `0x` hex) | Sets the size of the zeroed event structure for the synthetic Bluetooth HID callback. The default is 256. It has an effect only with `SHARPEMU_BTHID_FIRE_CALLBACK=1`. | `BluetoothHidExports.cs` |
| `SHARPEMU_BTHID_FIRE_CALLBACK` | `1` | Calls the registered Bluetooth HID guest callback one time with a zeroed event structure. The call occurs at the first `sceBluetoothHidRegisterDevice`. The default is off. | `BluetoothHidExports.cs` |
## Memory and CPU
These variables apply to guest memory and guest code execution.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_DISABLE_GUEST_ALLOCATOR_HOLE_RECOVERY` | `1` | Stops the Windows fault recovery for a guest allocator that reads an empty pool node. The default recovers the fault and continues at the allocator fallback code. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_DISABLE_MITIGATION_RELAUNCH` | `1` | Stops the Windows relaunch of the emulator as a child process with Control Flow Guard and CET shadow stacks off. The default starts the child process. | `Program.cs`, `SelfLoader.cs` |
| `SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS` | `1` | Stops the use of native worker threads for guest thread execution on Windows. The default uses the native workers. | `DirectExecutionBackend.NativeWorker.cs` |
| `SHARPEMU_DISABLE_POSIX_SIGNALS` | `1` | Does not install the POSIX signal handlers on Linux and macOS. The emulator then cannot recover guest faults. | `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_DISABLE_RAW_HANDLER` | `1` | Does not install the raw vectored exception handler on Windows. On POSIX hosts it stops the raw sentinel recovery. The default keeps the two functions on. | `DirectExecutionBackend.Exceptions.cs`, `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_IGNORE_INT41` | `0` or `false` | The fault handler skips guest `int 0x41` instructions and continues at the next instruction by default. Set `0` or `false` to stop this recovery. The emulator shows a warning for the first 16 traps and then for each 65536th trap. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_IGNORE_STACK_CHK` | `1` | Recovers from a guest stack-protector failure call (NID `Ou3iL1abvng`) when a UD2 instruction follows the call. The emulator sets the return address to the function epilogue 20 bytes before. It does this only when the code bytes match the known pattern. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LAZY_RESERVE_PRIME_MB` | number | Sets the number of megabytes that the emulator commits at the start of each reserve-only memory region. The default is 64 and the maximum is 4096. The value 0 turns the initial commit off. | `PhysicalVirtualMemory.cs` |
| `SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT` | number | Sets the maximum number of native guest worker runs that execute at the same time. The default is 2. The emulator limits the value to the range 1 to 64. | `DirectExecutionBackend.NativeWorker.cs`, `DirectExecutionBackend.cs` |
| `SHARPEMU_GUEST_AFFINITY` | `1` | Applies the guest thread affinity to the host threads. The default leaves host threads unpinned, because pinning let spinning guest workers starve the render thread. | `DirectExecutionBackend.cs` |
| `SHARPEMU_RENDER_CORE` | `0` | `0` turns off the render core reservation. On Windows hosts with 8 to 64 logical processors, the default gives the render thread the last physical core of the fastest efficiency class with a high priority, and keeps guest threads off that core. | `HostLaneReservation.cs`, `VulkanVideoPresenter.Present.cs`, `DirectExecutionBackend.cs` |
| `SHARPEMU_RESERVED_HOST_LANES` | number | Set the number of host logical processors that guest thread affinity does not use. The emulator keeps these processors for its own threads. The default is three eighths of the processor count, with a minimum of 2. It has an effect only with `SHARPEMU_GUEST_AFFINITY=1`. | `DirectExecutionBackend.cs` |
| `SHARPEMU_TSC_FREQ_HZ` | number (Hz, minimum 1000000) | Sets the TSC frequency that the kernel library reports to the guest. When unset, the emulator uses the calibrated RDTSC frequency, then the CPUID frequency, then the host stopwatch frequency. | `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_WATCH_BULK_DEST_HI` | hex number | Limits the `SHARPEMU_WATCH_BULK_TORN` scan to writes whose upper 32 address bits equal this value. When unset, the scan uses the direct-memory address band. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_BULK_TORN` | `1` | Examines each aligned 8-byte value in managed bulk writes to guest memory. It reports torn values and shifted pointer values with a stack trace. It writes a maximum of 64 reports for each kind. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_POOL_HEADER` | `1` | Monitors the header slot at offset 0x40 of each pool mapping with size 0x10000 and protection 0xF2. It reports each managed write to a monitored slot with a stack trace. It monitors a maximum of 64 slots. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_VALUE1` | `1` | Reports managed writes of 1 to 8 bytes that have the value 1 and go to the direct-memory address band. Each report has a stack trace. It writes a maximum of 128 reports. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_VALUE_PATTERN` | `1` | Reports each managed 8-byte write whose low half is 1 and whose high half is between 1 and 0xFFFF. Each report has a stack trace. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_WRITE` | hex address | Monitors the 8 bytes at this guest address. It reports each managed write that touches these bytes with the address, the length, the first value, and a stack trace. Default is off. | `GuestWriteWatch.cs` |
## Loader
These variables apply to the program loader and the module system.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_AMPR_INDEX_CACHE` | path (directory) | Sets the directory for the AMPR file index cache. The default is `SharpEmu/ampr-index` in the local application data folder. | `AmprFileRegistry.cs` |
| `SHARPEMU_AMPR_REINDEX` | `1` | Ignores the AMPR index cache on disk and scans the application file tree again. Use it after manual changes to the application files. | `AmprFileRegistry.cs` |
| `SHARPEMU_APP0_DIR` | path (directory) | Sets the host directory for the guest `/app0` mount. The default is the directory of the loaded executable. The directory name also identifies the per-application data folders. | `SharpEmuRuntime.cs`, `KernelMemoryCompatExports.cs`, `AppContentExports.cs` |
| `SHARPEMU_DEVLOG_APP_DIR` | path (directory) | Sets the host directory for the guest devlog application path. The default is `user/game_logs/<title id>/devlog/app` below the emulator directory. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_DISABLE_RED_ZONE_PATCH` | `1` | Stops the load-time patch that protects the guest stack red zone on Windows and macOS. The default applies the patch. The vector store split for Rosetta stays on when it is necessary. | `GuestRedZonePatcher.cs` |
| `SHARPEMU_DOWNLOAD0_DIR` | path (directory) | Sets the host directory for the guest `/download0` mount. The default is a `download0` folder in the per-application writable directory. The emulator sets the variable to the default when it is not set. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_GUEST_ARGS` | text | Adds arguments to the guest process entry frame after the image name. White space divides the arguments. The emulator uses only the first two. | `CpuDispatcher.cs` |
| `SHARPEMU_HOSTAPP_DIR` | path | Sets the host directory for the guest `/hostapp` mount. The default is `user/game_logs/<title id>/hostapp` below the emulator directory. The emulator makes the directory if it is absent. | `KernelMemoryCompatExports.cs`, `KernelGameLogPathTests.cs` |
| `SHARPEMU_LLE_LIBC_ALL` | `1` | Sends all libc imports to the guest libc module and not to the HLE handlers. `SHARPEMU_DISABLE_LLE_LIBC=1` and `SHARPEMU_LLE_LIBC_SAFE_ONLY=off` have priority. Default is off. | `DirectExecutionBackend.cs` |
| `SHARPEMU_LLE_LIBC_SAFE_ONLY` | `1`, `0`, `off`, `false` or `none` | Controls which libc imports use the guest libc module. Unset or `1` uses guest code only for exports on the safe list. `0` uses guest code for all libc exports, and `off`, `false` or `none` uses HLE for all of them. | `DirectExecutionBackend.cs` |
| `SHARPEMU_PRELOAD_ALL_SCE_MODULES` | `1` | Set to `1` to preload all system modules. When unset, the runtime does not preload the modules in its internal skip list. | `SharpEmuRuntime.cs` |
## Debugger
These variables apply to the debug server.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_SENTINEL_PROBE` | `1` | Set to `1` to call the native entry stub with the unresolved sentinel address 0xFFFE before the guest entry. The emulator writes a log line before and after the call. The default is off. | `DirectExecutionBackend.cs`, `Gen5NativeReturnSmokeTests.cs` |
## Log channels
Each variable adds one group of messages to the log. The default is off.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_LOG_ACM` | `1` | Writes `acm.` trace lines from the Acm library handlers to stderr. Default is off. | `AcmExports.cs` |
| `SHARPEMU_LOG_AGC_SHADER` | `1` | Writes only the AGC shader trace lines and the Vulkan shader trace lines. `SHARPEMU_LOG_AGC=1` also turns this on. Default is off. | `AgcExports.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_LOG_AGC` | `1` | Starts the full AGC trace: command submissions, shader creation, draws and dispatches. It also turns on the AGC shader trace and the Vulkan shader trace. Default is off. | `AgcExports.cs`, `RenderExecutor.Trace.cs`, `AgcExports.ShaderState.cs` |
| `SHARPEMU_LOG_AJM` | `1` | Writes `ajm.` trace lines to stderr: context initialization, codec registration, split buffer descriptors and ATRAC9 decode state. Default is off. | `AjmExports.cs`, `Atrac9DecodeState.cs` |
| `SHARPEMU_LOG_ALLOC_IMPORTS` | `1` | Writes more trace lines for `sceKernelGetGPI` (the returned mask) and for the PRT aperture set call (the raw register values). Default is off. | `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_ALL_IMPORTS` | `1` | Writes a debug line for each import stub during setup. The lines show if the import uses a direct bridge, a trampoline, HLE or a runtime symbol. Default is off. | `DirectExecutionBackend.cs` |
| `SHARPEMU_LOG_AMPR_READS` | `1` | Writes only the AMPR file read trace lines and the pak directory tracker lines. `SHARPEMU_LOG_AMPR=1` also turns this on. Default is off. | `AmprExports.cs`, `PakDirectoryTracker.cs` |
| `SHARPEMU_LOG_AMPR` | `1` | Writes trace lines for AMPR command buffers, APR submissions and waits, and the pak directory tracker. It also turns on the AMPR read trace. Default is off. | `AmprExports.cs`, `KernelAprCompatExports.cs`, `PakDirectoryTracker.cs` |
| `SHARPEMU_LOG_APP_CONTENT` | `1` | Writes an `app_content` trace line to stderr for each AppContent call. The default is off. | `AppContentExports.cs` |
| `SHARPEMU_LOG_AUDIO_OUT2` | `1` | Writes an `audio_out2` trace line to stderr for each AudioOut2 operation. The default is off. | `AudioOut2Exports.cs` |
| `SHARPEMU_LOG_AUDIO_OUT` | `1` | Records `sceAudioOutOutput` calls and the peak amplitude of the submitted samples. It writes the first 8 calls and then every 200th call. The emulator reads the value one time at start. | `AudioOutExports.cs` |
| `SHARPEMU_LOG_BOOTSTRAP` | `1` | Writes a trace line for each call through the bootstrap bridge import. The line shows the handle, the symbol name and the output address. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_CONTEXT` | `1` | Writes `guest_context` trace lines to stderr. Each import dispatch shows the NID, the return address, the managed thread, the guest thread and the fiber. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_DATA_REBIND` | `1` | Writes one `[RUNTIME]` line for each imported data relocation. The line shows if the relocation is rebound, unresolved or write-failed. | `SharpEmuRuntime.cs` |
| `SHARPEMU_LOG_DISCMAP` | `1` | Writes a trace line for each DiscMap call. The line shows the export name, the path, the offset and the size. | `DiscMapExports.cs` |
| `SHARPEMU_LOG_DISCORD` | `1` | Writes `[DISCORD]` messages from the rich presence client of the GUI to stderr. The default is off. | `DiscordRichPresence.cs` |
| `SHARPEMU_LOG_DLSYM` | `1` | Writes a trace line for each successful `sceKernelDlsym` call. The line shows the module handle, the symbol name and the resolved address. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_EQUEUE` | `1` | Writes trace lines for kernel event queue operations. The emulator reads the value one time at start. | `KernelEventQueueCompatExports.cs` |
| `SHARPEMU_LOG_EVENT_FLAG` | `1` | Writes trace lines for kernel event flag operations. The emulator reads the value one time at start. | `KernelEventFlagCompatExports.cs` |
| `SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS` | `1` | Shows import error results that the emulator knows as usual, for example file-not-found probes, timeouts and busy trylocks. It shows the first 8 results for each NID and result, and then every 10000th. The default hides these results. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_FIBER` | `1` | Writes `fiber` trace lines for the Fiber library calls. It also writes a line for each fiber context transfer in the CPU backend. | `FiberExports.cs`, `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_FILE` | path | Writes all log entries to this file with timestamps and appends to an existing file. The file receives every log level. `SHARPEMU_LOG_LEVEL` then limits only the console output. | `SharpEmuLog.cs` |
| `SHARPEMU_LOG_FMOD` | `1` | Writes a trace line for the FMOD `system_set_output` compatibility call. The line shows the system address, the output type and the call count. | `FmodCompatExports.cs` |
| `SHARPEMU_LOG_GUARDS` | `1` | Writes a trace line for each C++ static guard operation. The line shows the guard address, the guard word, the state flags and the owner thread. | `CxxAbiExports.cs` |
| `SHARPEMU_LOG_GUEST_EXCEPTIONS` | `1` | Writes `guest_exception` trace lines when the emulator raises and delivers a guest exception. The lines show the target thread and the exception type. | `DirectExecutionBackend.cs`, `KernelExceptionCompatExports.cs` |
| `SHARPEMU_LOG_GUEST_THREADS` | `1` | Writes guest thread scheduler messages to stderr. The messages show thread state changes, yields, wakes, exits, nested callbacks and affinity failures. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_GUEST_THREAD_SNAPSHOTS` | `1` | Writes one snapshot line for each guest thread every second. The line shows the state, the import count, the last NID, the last return address and the block reason. The ready-dispatch thread then wakes every second. | `DirectExecutionBackend.cs` |
| `SHARPEMU_LOG_HTTP2` | `1` | Writes an `http2` trace line for each Http2 library call. The line shows the operation, the id and four arguments. | `Http2Exports.cs` |
| `SHARPEMU_LOG_HTTP` | `1` | Writes an `http` trace line for each Http library call. The line shows the operation, the id and four arguments. | `HttpExports.cs` |
| `SHARPEMU_LOG_IO_FILTER` | text | Limits the `SHARPEMU_LOG_IO` output to paths that contain this text. The comparison ignores case. When unset, the emulator shows all paths. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_JSON` | `1` | Writes a `json` trace line for each Json library call. Text values show a maximum of 128 characters. | `JsonExports.cs` |
| `SHARPEMU_LOG_LAZY_COMMIT` | `1` | Writes a trace line for every lazy memory commit fault. The default shows only the first 16 faults and then every 256th fault. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_LEVEL` | text | Sets the minimum log level. The values are `Trace`, `Debug`, `Info`, `Warning` or `Warn`, `Error`, `Critical` or `Fatal`, and `None`. The default is `Info`, and the comparison ignores case. | `SharpEmuLog.cs` |
| `SHARPEMU_LOG_LIBC_ALLOC` | `1` | Writes a trace line for each guest libc allocation call. The line shows the size, the alignment, the result address and the return address of the caller. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_NET` | `1` | Writes `net` trace lines for Net library calls and debug lines for kernel socket operations. It also writes each payload that the guest sends on a connected socket to stdout as text. | `NetExports.cs`, `KernelSocketCompatExports.cs` |
| `SHARPEMU_LOG_NGS2` | `1` | Writes trace lines for Ngs2 audio library calls. The default is off. | `Ngs2Exports.cs` |
| `SHARPEMU_LOG_NO_COLOR` | `1` | Stops the use of colors in the console log. The values `1`, `true`, `yes` and `on` are accepted. The console log has no colors when the output is redirected. | `SharpEmuLog.cs` |
| `SHARPEMU_LOG_NP_WEB_API2` | `1` | Writes an `npwebapi2` trace line for each NpWebApi2 call. The line shows the operation, the id, the first argument and the initialized state. | `NpWebApi2Exports.cs` |
| `SHARPEMU_LOG_OPEN` | `1` | Writes trace lines for guest file open operations to stderr. The default is off. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_PLAYGO` | `1` | Writes `playgo` trace lines for PlayGo calls. For single-entry locus queries it shows the first 32 calls and then every 1000th call. | `PlayGoExports.cs` |
| `SHARPEMU_LOG_POINTER_WINDOW_SIZE` | hex number | Sets the byte size of each memory window that `SHARPEMU_LOG_POINTER_WINDOWS` shows after a guest fault. The value is hexadecimal, with or without `0x`. The default is 0x80. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_POSIX_SIGNALS` | `1` | Writes a trace line for every POSIX signal that the fault handler receives. When unset, the log shows only the first 16 signals and then every 1024th signal. | `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_LOG_PROC_PARAM_PTRS` | `1` | Shows the pointer fields in the process parameter block. This dump is part of the `SHARPEMU_LOG_PROC_PARAM` trace, so set that variable also. | `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_PROC_PARAM` | `1` | Shows the address and the contents of the process parameter block of the guest program. The default is off. | `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_PS5_USER_SLOTS` | `1` | Adds four 0x60-byte memory dumps to the IL2CPP exception diagnostic. The dumps start at the fixed guest address 0x801A73110 and have a step of 0x51C8 bytes. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_PSML` | `1` | Writes `psml.` trace lines for the PSML library calls to stderr. The default is off. | `PsmlExports.cs` |
| `SHARPEMU_LOG_PTHREADS` | `1` | Writes trace lines for the pthread calls of the guest. It also starts the condition variable trace of `SHARPEMU_LOG_PTHREAD_CONDS`. The default is off. | `KernelPthreadCompatExports.cs`, `KernelExports.cs` |
| `SHARPEMU_LOG_PTHREAD_CONDS` | `1` | Writes a `pthread_cond_` trace line for each condition variable operation. The line shows the waiter count, the signal epoch, and the result. | `KernelPthreadCompatExports.cs` |
| `SHARPEMU_LOG_PTHREAD_FASTPATH` | `1` | Writes trace lines for the mutex fast path. It shows the first 16 fast-path unlocks with the mutex object words. It shows one busy result for each mutex address. | `KernelPthreadCompatExports.cs` |
| `SHARPEMU_LOG_PTHREAD_MUTEX_FILTER` | list of hex addresses | Limits the mutex trace to the given mutex addresses. Use a comma, a semicolon, or a space between the hexadecimal addresses. The listed mutexes are in the trace even if `SHARPEMU_LOG_PTHREADS` is not set. | `KernelPthreadCompatExports.cs` |
| `SHARPEMU_LOG_REFSCAN_ADDRS` | list of hex addresses | After a guest fault, scans executable guest memory from 0x800000000 to 0x810000000 for instructions that refer to the given addresses. It shows a maximum of 24 hits for each address. Use a comma between the hexadecimal addresses. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_REGISTER_WINDOWS` | `1` | After a guest fault, shows a 0x80-byte memory window at each general register value that is 0x10000 or more. The default is off. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_SAVEDATA` | `1` | Writes `savedata.` and `save_data_dialog.` trace lines for the save data calls to stderr. The default is off. | `SaveDataExports.cs`, `SaveDataDialogExports.cs` |
| `SHARPEMU_LOG_SEMA` | `1` | Writes trace lines for the kernel semaphore operations. The emulator reads the value one time at start. The default is off. | `KernelSemaphoreCompatExports.cs` |
| `SHARPEMU_LOG_SHARE` | `1` | Writes `share.` trace lines for the Share library calls to stderr. The default is off. | `ShareExports.cs` |
| `SHARPEMU_LOG_SSL` | `1` | Writes an `ssl.` trace line for each SSL library call. The line shows the operation, the identifier, and the first argument. | `SslExports.cs` |
| `SHARPEMU_LOG_STACK_CHK` | `1` | Writes diagnostic lines when the guest calls the stack check failure import. The lines show the return address, the guard registers, and the stack slots from rbp-0x10 to rbp-0x80. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_STDIO` | `1` | Writes trace lines for the libc stdio calls of the guest. The emulator reads the value one time at start. The default is off. | `LibcStdioExports.cs` |
| `SHARPEMU_LOG_STRLEN_BURSTS` | `1` | Writes a warning when the guest calls `strlen` 24 times in sequence. The warning shows the return address and the last five different imports before the burst. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_STRLEN` | `1` | Keeps `strlen` calls in the import trace. When unset, the import trace does not show `strlen` calls. It also starts the burst detection of `SHARPEMU_LOG_STRLEN_BURSTS`. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_THREAD_MODE` | `1` | Writes `[THREADMODE]` lines when a native guest worker thread is created, starts a guest run, and stops a guest run. Each line shows the host thread identifiers. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.NativeWorker.cs` |
| `SHARPEMU_LOG_USER_SERVICE` | `1` | Writes trace lines for the user service library calls. Each line includes the guest return address of the call. | `UserServiceExports.cs` |
| `SHARPEMU_LOG_USLEEP` | `1` | Writes trace lines for `usleep` calls: the first 32 calls and then every 10000th call. It also stops the native inline `usleep` stub and the leaf import path, so each call goes through the traced handler. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_VIDEOOUT_FPS` | `1` | Writes periodic frame rate lines with flip, present, draw, pipeline, and buffer pool counts. `SHARPEMU_PROFILE_PERFORMANCE=1` starts the same output. | `VideoOutExports.cs` |
| `SHARPEMU_LOG_VIDEOOUT` | `1` | Writes `videoout.` trace lines for the video output calls, such as flip events and frame dumps. The default is off. | `VideoOutExports.cs` |
| `SHARPEMU_LOG_VK_RESOURCES` | `1` | Writes a `vk.global_buffer` trace line one time for each new guest buffer that the Vulkan presenter binds. The line shows the base address and the size. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.GuestBuffers.cs` |
| `SHARPEMU_LOG_VMEM` | `1` | Writes debug lines for guest virtual memory operations and `[HOSTMEM]` lines for host memory operations. The default is off. | `PhysicalVirtualMemory.cs`, `HostMemory.cs`, `PosixHostMemory.cs` |
| `SHARPEMU_LOG_WIDE_PRINTF_ARGS` | `1` | Writes trace lines for the arguments of printf format calls, such as string arguments. Use `SHARPEMU_LOG_WIDE_PRINTF_FILTER` to limit the output. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_WIDE_PRINTF_FILTER` | text | Limits the `SHARPEMU_LOG_WIDE_PRINTF_ARGS` trace to format strings that contain this text. The comparison is case-sensitive. When unset, the trace includes all format strings. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_WIDE_PRINTF` | `1` | Writes a trace line for each wide-character printf call. The line shows the first 160 characters of the format and of the result. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_WIDE` | `1` | Shows the first 32 bytes at the string address for each `wcslen` call. The default is off. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_PERIODIC_SNAPSHOT_SECONDS` | number (seconds) | Set a number of seconds to write the stall watchdog snapshot at that interval. The default is 0, which writes no periodic snapshot. The stall watchdog must be on. | `DirectExecutionBackend.cs` |
## Traces
Each variable records a detailed sequence of events. Traces can be large.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_LOG_IMPORT_FILTER` | text | Writes a trace line for each import that contains this text. The emulator compares the text with the library name, the export name and the NID, and ignores case. When unset, the filter selects no imports. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_IMPORT_FRAMES` | `1` | Adds the guest frame chain to each traced import. It applies only to imports that `SHARPEMU_LOG_ALL_IMPORTS` or `SHARPEMU_LOG_IMPORT_FILTER` selects. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_IMPORT_PERIODIC` | `1` | Starts the periodic import trace. It shows the early import bands and every 100000th import dispatch. The default is off because the stderr output decreases the speed. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_IMPORT_RECENT` | `1` | Writes the recent import history after each traced import. It applies only to imports that `SHARPEMU_LOG_ALL_IMPORTS` or `SHARPEMU_LOG_IMPORT_FILTER` selects. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_PROBE_IMPORT_RET_ADDRESS` | hex address | Set a guest return address. When an import call returns to this address, the emulator writes an `import-return-address-probe` line with the thread, NID, stack and frame values. The limit is 2048 lines. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_PROBE_IMPORT_RET` | text (NID or `*`) | Set an import NID, or `*` for all imports. For the first 8 applicable import calls, the emulator writes the bytes and the disassembly before the return address. The default is off. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `DirectExecutionBackend.Diagnostics.cs` |
| `SHARPEMU_RTC_PROBE_RANGE` | hex address range (`start-end`) | Set a range of guest addresses. When a caller of `sceRtcGetCurrentTick` returns into this range, the emulator writes 0x100 bytes of code near that return address. It does this one time only. | `RtcExports.cs` |
| `SHARPEMU_TRACE_AGC_EQ_ACCESSORS` | `1` | Set to `1` to write an `agc.eq_accessor` line for calls to the AGC event accessor functions. Each line shows the accessor, the event address, the result and the event bytes. After 64 lines, the emulator writes only at power-of-two counts. | `AgcExports.Events.cs` |
| `SHARPEMU_TRACE_DCC_WRITERS` | `1` | Writes a `[GPU][DCC_WRITER]` line with the shader program and a call stack for each GPU buffer write that overlaps DCC metadata. It also writes `[GPU][BOUNDED_FILL_REFUSED]` lines when a fill shader is not changed into a buffer fill. Each kind writes a maximum of 64 lines. | `DccWriterTrace.cs` |
| `SHARPEMU_TRACE_FOCUSED_CONTINUATION` | `1` | Set to `1` to write `focused_continuation` lines for guest thread continuations. The emulator writes a line only when the stack pointer is in the range 0x6FFFAC000000 to 0x6FFFAC200000. | `DirectExecutionBackend.cs` |
| `SHARPEMU_TRACE_FRAME_PACKETS` | `1` | Writes a `[FRAMEPKT]` line at a flip with the draw count and the dispatch count of the frame. It shows the first 8 flips, each 60th flip, and each flip that has no draws. Default is off. | `AgcExports.CommandStream.cs` |
| `SHARPEMU_TRACE_GPU_MEMORY_ADDRESS` | hex address or `auto` | Writes `[GPU][MEMORY_TRACE]` lines for GPU memory events that touch the page of the given address. The value `auto` selects the page of the first device-address fault. Default is off. | `GuestGpuMemoryHook.cs` |
| `SHARPEMU_TRACE_GPU_READS` | `1` | Writes a `[GPU][SYNC_READ]` line with the shader hash, the stage and the address when a shader resource read must wait for GPU-written memory. It writes one line for each shader and page, and a maximum of 64 lines. | `GpuReadTrace.cs`, `VulkanVideoPresenter.Pipelines.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGES` | `1`, `present` or `every:N[@M]` | `1` and `present` write Vulkan present trace lines (`vk.present_taken`, `vk.present_dropped`, `vk.present_sample`) and up to 64 Metal `agc.texture_fallback` lines. The code reads the `every:N[@M]` form, but no code uses the result. Default is off. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.Present.cs`, `MetalTextureSnapshots.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGE_ADDRS` | list of hex addresses or `*` | The guest image write tracker writes `[WT][LIFETIME]` lines for each tracked range that contains a listed address. `*` selects all ranges. The Vulkan presenter also reads the list, but no code uses that filter. | `GuestImageWriteTracker.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGE_FORMAT` | text (Vulkan format name) | Effect not clear from the code. The Vulkan presenter keeps the name for an image filter that compares the image format, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGE_HEIGHT` | number | Effect not clear from the code. The Vulkan presenter keeps the number for an image filter that compares the image height, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGE_WIDTH` | number | Effect not clear from the code. The Vulkan presenter keeps the number for an image filter that compares the image width, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_GUEST_MEMORY_LIFETIME` | list of source names or `*` | The guest image write tracker writes `[WT][LIFETIME]` lines for each tracked range that has a listed source name. Use a comma or a semicolon between names. `*` selects all sources. | `GuestImageWriteTracker.cs` |
| `SHARPEMU_TRACE_GUEST_WRITES` | list of hex addresses or `*` | Effect not clear from the code. The Vulkan presenter reads the list in an address filter for guest image writes, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_PACKED_EXPORT` | `1` | The SPIR-V translator writes `[AGC][PACKED-EXPORT]` and `[AGC][TITLE-IR]` lines for packed export analysis. It does this only for the shader program at address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_ADDRS` | list of hex addresses or `*` | Writes a `vk.present_sample` trace line when the presenter shows a guest image at a listed address. It records the first presentation of each address unless `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_OCCURRENCE` is set. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.RenderLoop.cs` |
| `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_OCCURRENCE` | number | Selects which presentation of an address in `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_ADDRS` the presenter records. Default is 0, which selects the first presentation. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_TITLE_INTERFACE` | `1` | The SPIR-V translator writes `[AGC][TITLE-INTERFACE]` lines with the export and interpolation instructions of a shader. It does this only for the programs at addresses 0x0000000500780000 and 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_TRACE_WRITER_ADDRESS` | hex address | Writes a `[GPU][TARGET_WRITER]` line with the shader program and a call stack for each GPU buffer write that covers this guest address. It writes a maximum of 64 lines. Default is off. | `DccWriterTrace.cs` |
## Data dumps
Each variable writes data to files.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_DUMP_FAULT_STACK_WINDOW` | `1` | Writes the full stack window from RSP-0x300 to RSP+0x100 to the error output when a guest fault report occurs. The default writes only the short stack list. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_DUMP_FIXED_SOLID_FRAGMENT` | path | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_DUMP_SPIRV_ADDRESS` | hex address | Limits the shader dump of `SHARPEMU_DUMP_SPIRV=1` to the shader at this guest address. The default writes all shaders. | `ShaderProgramCache.cs` |
| `SHARPEMU_DUMP_VIDEOOUT` | `1` | Writes submitted VideoOut frame buffers as BMP files with a text metadata file. It writes a maximum of 8 frames and skips a frame that did not change. | `VideoOutExports.cs`, `VideoOutExports.FlipRequests.cs` |
| `SHARPEMU_GUEST_IMAGE_DUMP_CONTINUOUS` | `1` | Writes a swapchain dump for each presented frame, not only for the first one. It needs `SHARPEMU_GUEST_IMAGE_DUMP_DIR` and an active presented-image trace. Each dump waits for the GPU, so the frame rate decreases. | `VulkanVideoPresenter.Present.Frames.cs` |
| `SHARPEMU_GUEST_IMAGE_DUMP_DIR` | path | Sets the directory for raw swapchain dumps. The presenter writes `present-NNNN-WxH-format.bgra` files there when `SHARPEMU_TRACE_GUEST_IMAGES` is `1` or `present`. The presenter makes the directory if it is absent. | `VulkanVideoPresenter.Present.Frames.cs` |
| `SHARPEMU_LOG_DISASM_ADDRS` | list of hex addresses | Adds more addresses to the fault disassembly dump. Use a comma between the addresses. The emulator uses this list only when `SHARPEMU_LOG_DISASM` is `1`, and it shows 48 instructions for each address. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_DISASM` | `1` | Writes guest disassembly to stderr when a guest fault, an abort or an illegal instruction occurs. It shows the code before the fault address, before the stack return address and before three frame return addresses. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_IL2CPP_EXCEPTION` | `1` | Writes a diagnostic dump when the guest calls the unresolved import with NID `cfwBSQyr5Ys`. The dump shows the registers, the code pointers on the stack and the frame pointer chain. The emulator writes a maximum of 4 dumps. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_POINTER_WINDOWS` | list of hex addresses | Writes a memory window for each address when a guest access fault occurs. Use a comma between the addresses. The window size is 0x80 bytes unless `SHARPEMU_LOG_POINTER_WINDOW_SIZE` sets a different size. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_SHADER_SPIRV_DUMP_DIR` | path | Set a directory for shader dumps. When set, the Vulkan presenter writes each SPIR-V module to a numbered `.spv` file. The `SHARPEMU_DUMP_SPIRV=1` dumps also use this directory. Their default is `shader-dumps` next to the executable. | `VulkanVideoPresenter.Pipelines.cs`, `ShaderProgramCache.cs`, `VulkanVideoPresenter.Device.Setup.cs` |
| `SHARPEMU_SWAPCHAIN_DUMP_EVERY` | number (frames) | Set a number N to trace the presented swapchain image at each Nth present. This operates only when `SHARPEMU_TRACE_GUEST_IMAGES` is `1` or `present`. The default is 0, which traces only the first presented image. | `VulkanVideoPresenter.Present.Frames.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
## Performance measurement
Each variable records timings or counts.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_LOG_AUDIO_QUEUE` | `1` | Writes one `[PERF][AUDIO]` line per second for each SDL audio stream. The line shows the queue depth in milliseconds, the submit rate, the fill percentage and the drops. The emulator reads the value one time at start. | `SdlHostAudio.cs` |
| `SHARPEMU_PERF_HLE_NODICT` | `1` | Use with `SHARPEMU_PERF_HLE=1`. Set to `1` to stop the call count dictionary and the top call count report. The dispatch time report stays on. | `DirectExecutionBackend.Diagnostics.cs` |
| `SHARPEMU_PERF_HLE` | `1` | Set to `1` to measure the time of each HLE import dispatch. After each 500000 calls, the emulator writes `[PERF][HLE]` lines with the call counts and the cost of the top exports. The default is off. | `DirectExecutionBackend.Diagnostics.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_PERF_MEM` | `1` | Set to `1` to count the POSIX fault signals that the signal handler receives. The emulator writes a `[PERF][MEM] posix_faults` line after each 100000 signals. This has an effect only on POSIX hosts. | `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_PROFILE_DCB_PARSE` | `1` | Set to `1` to measure the time to parse draw command buffers. The emulator writes `[PERF][DCB_PARSE]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DcbParseProfile.cs` |
| `SHARPEMU_PROFILE_DCB_SUBMISSION` | `1` | Set to `1` to measure command buffer submission and geometry snapshot work. The emulator writes `[PERF][DCB_SUBMIT]`, `[PERF][VERTEX_SNAPSHOT]` and `[PERF][SNAPSHOT_PREPASS]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DcbSubmissionProfile.cs` |
| `SHARPEMU_PROFILE_GPU_DRAWS` | `1` | Use with the render profile. Writes a GPU timestamp after each draw and dispatch, so that the `[PERF][GPU_INTERVAL]` lines give the GPU time of each pipeline. The default writes only two timestamps for each submission for the `[PERF][GPU_BUSY]` lines, because one timestamp for each draw can change the GPU time. | `VulkanCommandProfile.cs` |
| `SHARPEMU_PROFILE_GUEST_IMAGE_TRACKER` | `1` | Set to `1` to record counters for the guest image write tracker. The emulator writes `[PERF][GUEST_IMAGE_TRACKER]` and `[PERF][GUEST_IMAGE_TRACKER_RANGE]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `GuestImageWriteTracker.cs` |
| `SHARPEMU_PROFILE_GUEST_RIP_INTERVAL_MS` | number (milliseconds) | Set the time between samples of the guest code profiler. The value must be more than 0. The default is 2. | `DirectExecutionBackend.GuestSampler.cs` |
| `SHARPEMU_PROFILE_GUEST_RIP_REPORT_S` | number (seconds) | Set the time between reports of the guest code profiler. The value must be more than 0. The default is 15. | `DirectExecutionBackend.GuestSampler.cs` |
| `SHARPEMU_PROFILE_GUEST_RIP_THREAD` | text | Set a text filter for the guest code profiler. The profiler samples only guest threads whose name contains this text. The comparison ignores case. When unset, the profiler samples all guest threads. | `DirectExecutionBackend.GuestSampler.cs` |
| `SHARPEMU_PROFILE_GUEST_RIP` | `1` | Set to `1` to start a sample profiler for guest code. The profiler records the instruction address of each guest thread and writes `[PERF][GUEST]` reports. The default is off. | `DirectExecutionBackend.GuestSampler.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_PROFILE_GUEST_SCHEDULER` | `1` | Set to `1` to record guest thread scheduler statistics. The emulator writes `[PERF][GUEST_SCHED]` lines. With `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE=1`, it also records guest thread flow events. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DirectExecutionBackend.GuestSchedulerProfile.cs`, `GuestThreadFlowProfile.cs` |
| `SHARPEMU_PROFILE_RENDER_REPORT_S` | number (seconds) | Set the time between render profile reports. The value can be a decimal number and must be more than 0. The default is 5. | `RenderPhaseProfile.cs` |
| `SHARPEMU_PROFILE_RENDER` | `1` | Set to `1` to measure the time of each render thread phase. The emulator writes `[PERF][RENDER]`, `[PERF][RENDER_MS]` and related lines. With `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE=1`, it also records GPU memory access counters. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `RenderPhaseProfile.cs`, `GpuMemoryAccessProfile.cs` |
| `SHARPEMU_PROFILE_SYNC_ON_ADDRESS` | `1` | Set to `1` to start the detailed sync-on-address profile. The emulator also records the guest call stack for each wait and wake call. It writes `[PERF][SYNC_ADDR]` and `[PERF][SYNC_THREAD]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` starts the profile without the call stack. | `KernelSyncOnAddressProfile.cs`, `KernelSyncOnAddressCompatExports.cs` |
| `SHARPEMU_PROFILE_TEXTURE_PREPARATION` | `1` | Set to `1` to measure texture preparation work. The emulator writes `[PERF][TEXTURE_PREP]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `TexturePreparationProfile.cs` |
## Internal
The emulator sets these variables for its own processes. Do not set them manually.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_MITIGATED_CHILD` | `1` | The Windows launcher sets this variable to 1 when it starts the child process with CFG and CET mitigations off. The child accepts the `--sharpemu-mitigated-child` argument only when the value is 1. Do not set it manually. | `Program.cs`, `EmulatorProcess.cs` |
## Tests and tools
Only the tests, the scripts or the build tools read these variables.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_BACKING_VIEW_FAULT_WORKER` | `1` | The backing view fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `BackingViewFaultTests.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_BUFFER_FAULT_WORKER` | `1` | The guest buffer fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `GuestBufferFaultTests.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_FAULT_WORKER_REPORT` | path | The fault test parent sets this to a temporary file path for its child process. The child writes its result (`completed` or `skipped`) to this file. When unset, the child writes the result to the error stream. | `GuestFaultWorker.cs` |
| `SHARPEMU_IMAGE_FAULT_WORKER` | `1` | The guest image fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `GuestImageFaultTests.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_NATIVE_RETURN_SMOKE_WORKER` | `1` | The native return smoke test sets this for its child process. The value `1` tells the child to run the synthetic guest entry. Do not set it manually. | `Gen5NativeReturnSmokeTests.cs` |
| `SHARPEMU_SOCKET_CONNECTION_TEST_WORKER` | `1` | The kernel socket connection test sets this for its child process. The value `1` tells the child to do the connect sequence. Do not set it manually. | `KernelSocketConnectionTests.cs` |
| `SHARPEMU_TEST_REFERENCE_SPIRV_DIR` | path | Sets the directory that contains the reference compiled shader files for the GPU image tests. The tests make sure that each file has the recorded SHA-256 hash. When unset, these tests skip the reference comparison. | `ImageTestHarness.cs` |
| `SHARPEMU_TEST_REQUIRE_DEVICE` | `1` | Makes a GPU test fail when a Vulkan or Metal device or a device feature is missing. Default is to skip the test. | `ImageTestHarness.cs`, `MetalRuntimeTests.cs` |
| `SHARPEMU_TEST_REQUIRE_REFERENCE_SPIRV` | `1` | Makes a test fail when a reference shader file, its directory, or a necessary device is missing. Default is to skip the test. | `ImageTestHarness.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_TEST_VK_VALIDATION` | `1` | Loads the Vulkan validation layer in the headless test device and prints the validation messages. Default is off. | `HeadlessVulkan.cs` |
| `SHARPEMU_THREAD_CONTEXT_TEST_WORKER` | `1` | The Windows thread context capture test sets this for its child process. The value `1` tells the child to do the capture sequence. Do not set it manually. | `WindowsThreadContextCaptureTests.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_UPDATE_GOLDENS` | `1` | Makes the Metal golden tests write the generated shader text to the golden files in the source tree. Default is to compare the generated text with the golden files. | `MslGoldenTests.cs` |
## Variables that the code reads but does not use
The code reads these variables into a field or a filter. No other code uses that field or filter.
Thus they have no effect, or only a part of their function operates. The rows above give the details.
- `SHARPEMU_DUMP_FIXED_SOLID_FRAGMENT`
- `SHARPEMU_ENABLE_CHUNKED_DRAWS`
- `SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT`
- `SHARPEMU_FORCE_DEFAULT_RASTER_STATE`
- `SHARPEMU_FORCE_FULLSCREEN_PIPELINE`
- `SHARPEMU_FORCE_FULLSCREEN_VERTEX`
- `SHARPEMU_FORCE_SOLID_FRAGMENT`
- `SHARPEMU_FORCE_TITLE_DEFAULT_RASTER_STATE`
- `SHARPEMU_FORCE_TITLE_FULLSCREEN_VERTEX`
- `SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT`
- `SHARPEMU_SKIP_TALL_COMPUTE_Z`
- `SHARPEMU_TRACE_GUEST_IMAGE_FORMAT`
- `SHARPEMU_TRACE_GUEST_IMAGE_HEIGHT`
- `SHARPEMU_TRACE_GUEST_IMAGE_WIDTH`
- `SHARPEMU_TRACE_GUEST_WRITES`

View File

@ -0,0 +1,48 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Windows crash capture
In the launcher, open **Options > Logging** and enable **Crash dumps** before
you start a game. The same toggle is available in each game's Logging settings.
It sets `SHARPEMU_CRASH_CAPTURE` and is off by default.
For a diagnostic run on Windows x64, you can also set it from PowerShell:
```powershell
$env:SHARPEMU_CRASH_CAPTURE = '1'
.\SharpEmu.exe
```
Start a new launcher from that PowerShell window. The game process starts a
hidden helper before runtime initialization. No SDK or debugger install is
required. Look for `[CRASH][INFO] Crash capture ready` in the game log.
The helper writes a full dump beside the run log. Without a log path, it uses
`user/logs` beside the executable. Names include the game process ID:
- `<run>.crash-<pid>.dmp`: the memory dump.
- `<run>.crash-<pid>.dmp.capture.log`: capture status and errors.
Full dumps can use many GB of disk space and contain private process data.
Keep them local until you choose to share them. There is no automatic upload.
The helper uses Windows debugger events. It passes first-chance exceptions to
the application and captures only second-chance, unhandled exceptions. Normal
handled page faults do not create dumps. A normal exit or a forced process
termination does not create a dump either. The original crash is not suppressed.
This option changes debugger presence and can change timing. Do not use it for
FPS comparisons. Do not attach WinDbg, ProcDump, or another native debugger at
the same time. Capture failures are recorded when possible; no capture method
can cover every process termination. A failed write can leave an incomplete dump.
This is separate from the `DOTNET_Dbg*` settings. It does not change them.
To disable a GUI selection, turn off **Crash dumps** before the next game launch.
If you set the option in PowerShell, close the launcher and run:
```powershell
Remove-Item Env:SHARPEMU_CRASH_CAPTURE -ErrorAction SilentlyContinue
```

View File

@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "disable"
"rollForward": "latestFeature"
}
}

198
scripts/aerolib_catalog.py Normal file
View File

@ -0,0 +1,198 @@
#!/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 base64
import hashlib
import re
import sys
from pathlib import Path
NID_SUFFIX = bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")
NID_PATTERN = re.compile(r"^[A-Za-z0-9+-]{11}$")
DEFAULT_NAMES_FILE = Path(__file__).resolve().with_name("ps5_names.txt")
DEFAULT_EXPORT_FILE = Path(__file__).resolve().parents[1] / "artifacts" / "aerolib.txt"
def compute_nid(export_name: str) -> str:
digest = hashlib.sha1(export_name.encode("utf-8") + NID_SUFFIX).digest()
encoded = base64.b64encode(digest[:8][::-1]).decode("ascii")
return encoded.rstrip("=").replace("/", "-")
def read_names(path: Path) -> list[str]:
try:
return [
line.strip()
for line in path.read_text(encoding="utf-8").splitlines()
if line.strip()
]
except OSError as error:
raise SystemExit(f"Unable to read catalog '{path}': {error}") from error
def write_pair(nid: str, export_name: str) -> None:
print(f"{nid}\t{export_name}")
def lookup(args: argparse.Namespace) -> int:
values = [value.strip() for value in args.value.split(",") if value.strip()]
names = read_names(args.names)
name_set = set(names)
result = 0
for value in values:
if NID_PATTERN.fullmatch(value):
found = False
for export_name in names:
if compute_nid(export_name) == value:
write_pair(value, export_name)
found = True
break
if not found:
print(f"NID not found in catalog: {value}", file=sys.stderr)
result = 1
continue
write_pair(compute_nid(value), value)
if value not in name_set:
print(
f"Warning: export name is not present in the catalog: {value}",
file=sys.stderr,
)
return result
def search(args: argparse.Namespace) -> int:
names = read_names(args.names)
if args.regex:
try:
pattern = re.compile(args.query, 0 if args.case_sensitive else re.IGNORECASE)
except re.error as error:
print(f"Invalid regular expression: {error}", file=sys.stderr)
return 2
matches = (name for name in names if pattern.search(name))
elif args.case_sensitive:
matches = (name for name in names if args.query in name)
else:
query = args.query.casefold()
matches = (name for name in names if query in name.casefold())
count = 0
for export_name in matches:
write_pair(compute_nid(export_name), export_name)
count += 1
if args.limit and count >= args.limit:
break
if count == 0:
print(f"No catalog names matched: {args.query}", file=sys.stderr)
return 1
return 0
def export_catalog(args: argparse.Namespace) -> int:
pairs = [(compute_nid(name), name) for name in read_names(args.names)]
if args.sort == "nid":
pairs.sort(key=lambda pair: (pair[0], pair[1]))
elif args.sort == "name":
pairs.sort(key=lambda pair: pair[1])
args.output.parent.mkdir(parents=True, exist_ok=True)
try:
with args.output.open("w", encoding="utf-8", newline="\n") as output:
output.write("# NID\tExportName\n")
for nid, export_name in pairs:
output.write(f"{nid}\t{export_name}\n")
except OSError as error:
print(f"Unable to write catalog '{args.output}': {error}", file=sys.stderr)
return 1
print(f"Wrote {len(pairs)} entries to {args.output}")
return 0
def create_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Inspect the SharpEmu PS5 export-name/NID catalog.",
epilog=(
"Examples:\n"
" python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk\n"
" python scripts/aerolib_catalog.py lookup sceKernelWaitSema\n"
" python scripts/aerolib_catalog.py search VideoOut --limit 20\n"
" python scripts/aerolib_catalog.py export"
),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--names",
type=Path,
default=DEFAULT_NAMES_FILE,
help=f"source name list (default: {DEFAULT_NAMES_FILE})",
)
subparsers = parser.add_subparsers(dest="command", required=True)
lookup_parser = subparsers.add_parser(
"lookup", help="resolve a NID or calculate the NID for an export name"
)
lookup_parser.add_argument(
"value", help="11-character NID or exact export name; comma-separated values are supported"
)
lookup_parser.set_defaults(handler=lookup)
search_parser = subparsers.add_parser(
"search", help="find export names and print matching NID/name pairs"
)
search_parser.add_argument("query", help="name substring or regular expression")
search_parser.add_argument(
"--limit", type=int, default=50, help="maximum matches; 0 means unlimited"
)
search_parser.add_argument(
"--case-sensitive", action="store_true", help="match case exactly"
)
search_parser.add_argument(
"--regex", action="store_true", help="treat the query as a regular expression"
)
search_parser.set_defaults(handler=search)
export_parser = subparsers.add_parser(
"export", help="write every NID/name pair to a tab-separated text file"
)
export_parser.add_argument(
"output",
type=Path,
nargs="?",
default=DEFAULT_EXPORT_FILE,
help=f"output file (default: {DEFAULT_EXPORT_FILE})",
)
export_parser.add_argument(
"--sort",
choices=("source", "nid", "name"),
default="nid",
help="output ordering (default: nid)",
)
export_parser.set_defaults(handler=export_catalog)
return parser
def main() -> int:
parser = create_parser()
args = parser.parse_args()
return args.handler(args)
if __name__ == "__main__":
raise SystemExit(main())

37
scripts/fetch-macos-moltenvk.sh Executable file
View File

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Downloads the official (universal x86_64+arm64) MoltenVK dylib and stages
# it next to a SharpEmu build as libvulkan.1.dylib. The macOS build runs as
# an x86-64 process under Rosetta 2, so Homebrew's arm64-only Vulkan
# libraries cannot be used; the presenter looks for this app-local copy.
#
# Usage: scripts/fetch-macos-moltenvk.sh [output-dir]
# (default output: artifacts/bin/Debug/net10.0/osx-x64)
set -euo pipefail
MVK_VERSION="${MVK_VERSION:-v1.4.0}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT_DIR="${1:-$REPO_ROOT/artifacts/bin/Debug/net10.0/osx-x64}"
if [[ ! -d "$OUT_DIR" ]]; then
echo "output directory does not exist: $OUT_DIR (build first?)" >&2
exit 2
fi
WORK_DIR="$(mktemp -d)"
trap 'rm -rf "$WORK_DIR"' EXIT
echo ">> Downloading MoltenVK $MVK_VERSION..."
curl -sL -o "$WORK_DIR/mvk.tar" \
"https://github.com/KhronosGroup/MoltenVK/releases/download/$MVK_VERSION/MoltenVK-macos.tar"
tar -xf "$WORK_DIR/mvk.tar" -C "$WORK_DIR" \
MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib
DYLIB="$WORK_DIR/MoltenVK/MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib"
file "$DYLIB" | grep -q x86_64 || { echo "downloaded dylib lacks x86_64 slice" >&2; exit 3; }
cp "$DYLIB" "$OUT_DIR/libMoltenVK.dylib"
cp "$DYLIB" "$OUT_DIR/libvulkan.1.dylib"
echo ">> Staged libMoltenVK.dylib + libvulkan.1.dylib in $OUT_DIR"

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()

View File

@ -133845,6 +133845,7 @@ sceKernelWriteRemapIntoPrtCommand
sceKernelWriteRemapWithGpuMaskIdCommand
sceKernelWriteSdkEventLog
sceKernelWriteSdkEventLogV
sceKernelWriteThrottlingStatus
sceKernelWriteUnmapCommand
sceKernelWriteUnmapToPrtCommand
sceKernelWriteWaitOnAddressCommand
@ -153133,6 +153134,7 @@ scePsmlMfsrGetContextBufferRequirement800M3_2
scePsmlMfsrGetDispatchMfsrPacket1000
scePsmlMfsrGetDispatchMfsrPacket1100
scePsmlMfsrGetDispatchMfsrPacketSizeInDwords
scePsmlMfsrGetDispatchMfsrPacket900
scePsmlMfsrGetMipmapBias
scePsmlMfsrGetSharedResourcesInitRequirement
scePsmlMfsrInit
@ -154449,3 +154451,12 @@ vector_str_substr
WTFAnnotateBenignRaceSized
WTFAnnotateHappensAfter
WTFAnnotateHappensBefore
_sceUlobjmgrRegisterObject
_sceUlobjmgrUnregisterObject
sceNpEAAccessInitialize
sceNpEAAccessTerminate
sceNpHasEAAccessSubscription
sceNpHasEAAccessSubscriptionAbortRequest
sceAgcAddPrimStateRegisters
sceVoiceQoSSetMode
sceVoiceQoSTerminate

466
scripts/release.py Normal file
View File

@ -0,0 +1,466 @@
#!/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 get_previous_tag(repository_root: Path) -> str:
try:
return run_git(
"describe",
"--tags",
"--abbrev=0",
"--match",
"v*",
"HEAD",
cwd=repository_root,
capture_output=True,
)
except ReleaseError:
return ""
def get_release_commits(
repository_root: Path,
previous_tag: str,
) -> list[str]:
revision_range = f"{previous_tag}..HEAD" if previous_tag else "HEAD"
output = run_git(
"log",
"--no-merges",
"--reverse",
"--pretty=tformat:%h %s",
revision_range,
cwd=repository_root,
capture_output=True,
)
return output.splitlines()
def print_release_notes_preview(repository_root: Path) -> None:
previous_tag = get_previous_tag(repository_root)
commits = get_release_commits(repository_root, previous_tag)
print()
if previous_tag:
print(f"Commits since {previous_tag} ({len(commits)}):")
else:
print(f"Commits in this release ({len(commits)}):")
if not commits:
print(" (none)")
for commit in commits:
print(f" {commit}")
print()
print("These commits go into the generated release notes.")
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,
)
print_release_notes_preview(repository_root)
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())

32
scripts/test-linux-docker.sh Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
#
# Smoke-tests the linux-x64 build inside an amd64 container. Useful from any
# host (including Apple Silicon, where Docker runs the amd64 image under
# emulation) to confirm the cross-platform layer keeps working on Linux.
#
# Usage: scripts/test-linux-docker.sh /path/to/eboot.bin
set -euo pipefail
GAME_PATH="${1:-}"
if [[ -z "$GAME_PATH" || ! -f "$GAME_PATH" ]]; then
echo "usage: $0 <path-to-eboot.bin>" >&2
exit 2
fi
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GAME_DIR="$(cd "$(dirname "$GAME_PATH")" && pwd)"
GAME_FILE="$(basename "$GAME_PATH")"
PUBLISH_DIR="$REPO_ROOT/artifacts/publish/SharpEmu.CLI/Debug/net10.0/linux-x64"
echo ">> Publishing linux-x64 self-contained build..."
dotnet publish "$REPO_ROOT/src/SharpEmu.CLI" \
-c Debug -r linux-x64 --self-contained -p:PublishSingleFile=false
echo ">> Running inside linux/amd64 container..."
docker run --rm --platform linux/amd64 \
-v "$PUBLISH_DIR":/app:ro \
-v "$GAME_DIR":/game:ro \
mcr.microsoft.com/dotnet/runtime-deps:10.0 \
/app/SharpEmu --log-level=info "/game/$GAME_FILE"

View File

@ -0,0 +1,55 @@
#!/usr/bin/env bash
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
set -euo pipefail
if [ "$#" -ne 4 ]; then
echo "usage: $0 <spirv-val> <expected-version> <target-env> <module-directory>" >&2
exit 2
fi
validator=$1
expected_version=$2
target_env=$3
module_directory=$4
if [ ! -x "$validator" ]; then
echo "SPIR-V validator is not executable: $validator" >&2
exit 2
fi
if [ ! -d "$module_directory" ]; then
echo "SPIR-V module directory does not exist: $module_directory" >&2
exit 2
fi
validator_version="$("$validator" --version | head -n 1)"
if [[ "$validator_version" != *"SPIRV-Tools $expected_version"* ]]; then
echo "unexpected SPIRV-Tools version: $validator_version (expected $expected_version)" >&2
exit 2
fi
echo "Validator: $validator_version"
echo "Target environment: $target_env"
mapfile -d '' modules < <(find "$module_directory" -type f -name '*.spv' -print0 | sort -z)
if [ "${#modules[@]}" -eq 0 ]; then
echo "no SPIR-V modules found in $module_directory" >&2
exit 1
fi
failures=0
for module in "${modules[@]}"; do
echo "Validating module: $module"
if ! "$validator" --target-env "$target_env" "$module"; then
echo "SPIR-V validation failed: $module" >&2
failures=1
fi
done
if [ "$failures" -ne 0 ]; then
exit 1
fi
echo "Validated ${#modules[@]} synthetic SPIR-V modules."

View File

@ -5,8 +5,10 @@ using SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu;
using SharpEmu.GUI;
using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Logging;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Text.Json;
@ -30,7 +32,6 @@ internal static partial class Program
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const int ATTACH_PARENT_PROCESS = -1;
private const int STD_INPUT_HANDLE = -10;
private const int STD_OUTPUT_HANDLE = -11;
@ -44,6 +45,14 @@ internal static partial class Program
[STAThread]
private static int Main(string[] args)
{
ConfigureManagedPluginResolution();
if (args.Length > 0 && args[0] == SharpEmu.Core.Diagnostics.WindowsCrashCapture.HelperArgument)
return SharpEmu.Core.Diagnostics.WindowsCrashCapture.RunHelper(args);
RenderDocCapture.ApplyVulkanLoaderEnvironment();
SharpEmu.Libs.VideoOut.RenderDocCapture.Initialize();
try
{
return Run(args);
@ -55,13 +64,36 @@ internal static partial class Program
}
}
private static void ConfigureManagedPluginResolution()
{
AssemblyLoadContext.Default.Resolving += static (loadContext, assemblyName) =>
{
if (string.IsNullOrWhiteSpace(assemblyName.Name))
{
return null;
}
var assemblyPath = Path.Combine(
AppContext.BaseDirectory,
"plugins",
assemblyName.Name + ".dll");
return File.Exists(assemblyPath)
? loadContext.LoadFromAssemblyPath(assemblyPath)
: null;
};
}
private static int Run(string[] args)
{
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (args.Length == 0 && !isMitigatedChild)
if (Updater.TryApply(args, out var updateExitCode))
{
return updateExitCode;
}
args = NormalizeInternalArguments(args, out var isMitigatedChild);
if (args.Length == 0)
{
// No arguments: open the desktop frontend. Any argument selects
// the classic CLI behavior below.
return GuiLauncher.Run();
}
@ -74,6 +106,136 @@ internal static partial class Program
TryEnableConsoleFileMirror(earlyLogFilePath);
}
if (!CheckHostArchitecture())
{
return 5;
}
if (OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())
{
if (OperatingSystem.IsMacOS())
{
ConfigureMoltenVkDefaults();
PreloadMacVulkanLoader();
}
// SDL/AppKit window work belongs on the process main thread on
// macOS. Linux uses the same model for consistent X11/Wayland
// event ownership. Emulation remains on a worker thread.
var exitCode = 0;
HostMainThread.Enable();
var emulation = new Thread(() =>
{
try
{
exitCode = RunEmulator(args, isMitigatedChild);
}
finally
{
HostMainThread.Shutdown();
}
}, 32 * 1024 * 1024)
{
Name = "SharpEmu Emulation",
};
emulation.Start();
HostMainThread.Pump();
emulation.Join();
return exitCode;
}
return RunEmulator(args, isMitigatedChild);
}
/// <summary>
/// The supported host execution model, checked before any emulation
/// starts: the CPU backend executes guest x86-64 code natively, so the
/// host process must be x86-64 — win-x64/linux-x64 on x64 hardware, or
/// osx-x64 under Rosetta 2 on Apple Silicon (Rosetta translates the
/// whole process, so it still reports as X64 here). Failing up front on
/// any other process architecture distinguishes that from MoltenVK,
/// signal-handler, or guest-memory startup problems.
/// </summary>
private static bool CheckHostArchitecture()
{
if (RuntimeInformation.ProcessArchitecture == Architecture.X64)
{
return true;
}
Console.Error.WriteLine(
$"[LOADER][ERROR] Unsupported process architecture " +
$"{RuntimeInformation.ProcessArchitecture}: guest code executes " +
"natively, so SharpEmu must run as an x86-64 process.");
if (OperatingSystem.IsMacOS())
{
Console.Error.WriteLine(
"[LOADER][ERROR] On Apple Silicon, use the osx-x64 build under " +
"Rosetta 2 (install with: softwareupdate --install-rosetta).");
}
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 before SDL creates its Vulkan surface.
/// Homebrew's Vulkan libraries are arm64-only and cannot load into this
/// x86-64 (Rosetta 2) process, so a universal libMoltenVK.dylib placed
/// next to the executable (named libvulkan.1.dylib) is preloaded here;
/// dyld can then resolve the loader for SDL and Silk.NET.
/// </summary>
private static void PreloadMacVulkanLoader()
{
var candidates = new[]
{
Path.Combine(AppContext.BaseDirectory, "libvulkan.1.dylib"),
Path.Combine(AppContext.BaseDirectory, "libMoltenVK.dylib"),
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".sharpemu", "x64lib", "libvulkan.1.dylib"),
};
foreach (var candidate in candidates)
{
if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out _))
{
Console.Error.WriteLine($"[LOADER][INFO] Vulkan loader preloaded: {candidate}");
return;
}
}
if (NativeLibrary.TryLoad("libvulkan.1.dylib", out _))
{
return;
}
Console.Error.WriteLine(
"[LOADER][WARN] No x86-64 Vulkan loader found; video output will be unavailable. " +
"Place a universal libMoltenVK.dylib (from the MoltenVK releases) next to SharpEmu " +
"as libvulkan.1.dylib.");
}
private static int RunEmulator(string[] args, bool isMitigatedChild)
{
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
@ -81,7 +243,13 @@ internal static partial class Program
return childExitCode;
}
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
if (!TryParseArguments(
args,
out var ebootPath,
out var runtimeOptions,
out var videoOptions,
out var logLevel,
out var logFilePath))
{
PrintUsage();
return 1;
@ -93,6 +261,11 @@ internal static partial class Program
}
SharpEmuLog.MinimumLevel = logLevel;
if (!HostVideoHost.TryConfigureVideo(videoOptions))
{
Console.Error.WriteLine("[LOADER][ERROR] Video options cannot change while a presenter is active.");
return 3;
}
Log.Info(BuildInfo.Banner);
Log.Info(HostSystemInfo.Summary);
@ -106,53 +279,118 @@ internal static partial class Program
return 2;
}
if (!TryGetDebugServerOptions(args, out var debugServerEnabled, out var debugServerOptions, out var debugServerError))
{
Log.Error($"Invalid --debug-server endpoint: {debugServerError}");
return 1;
}
SharpEmu.Debugger.DebuggerServerHost? debugHost = null;
if (debugServerEnabled)
{
debugHost = new SharpEmu.Debugger.DebuggerServerHost(debugServerOptions);
try
{
debugHost.Start();
Log.Info($"Live debug server listening on {debugHost.Endpoint}. Attach with SharpEmu.DebugClient.");
// With StopAtEntry, the guest parks at its first frame until a
// client connects and continues.
runtimeOptions = runtimeOptions with { DebugHook = debugHost.Hook };
}
catch (Exception ex)
{
Log.Error("Failed to start the debug server.", ex);
debugHost.DisposeAsync().AsTask().GetAwaiter().GetResult();
return 6;
}
}
Console.Error.WriteLine("[DEBUG] Creating runtime...");
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
SharpEmu.Core.Diagnostics.WindowsCrashCapture.StartIfEnabled(logFilePath);
OrbisGen2Result result;
try
{
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
result = runtime.Run(ebootPath);
Console.Error.WriteLine($"[DEBUG] Result: {result}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DEBUG] Exception: {ex}");
Log.Error("SharpEmu failed to run.", ex);
return 3;
}
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
{
Log.Info(runtime.LastSessionSummary);
}
OrbisGen2Result result;
ConsoleCancelEventHandler? cancelHandler = null;
try
{
cancelHandler = (_, eventArgs) =>
{
eventArgs.Cancel = true;
VideoOutExports.NotifyHostInterrupt();
};
Console.CancelKeyPress += cancelHandler;
// SIGTERM (launchd, process managers, `kill`) takes the same cooperative shutdown: the
// runtime's default exits with GPU work in flight, which leaves the host driver to reclaim
// resources the device still owns.
using var terminateRegistration = OperatingSystem.IsWindows()
? null
: PosixSignalRegistration.Create(PosixSignal.SIGTERM, context =>
{
context.Cancel = true;
VideoOutExports.NotifyHostInterrupt();
});
if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace))
{
Log.Info("BB trace:");
Log.Info(runtime.LastBasicBlockTrace);
}
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
result = runtime.Run(ebootPath);
Console.Error.WriteLine($"[DEBUG] Result: {result}");
}
catch (Exception ex)
{
Console.Error.WriteLine($"[DEBUG] Exception: {ex}");
Log.Error("SharpEmu failed to run.", ex);
return 3;
}
finally
{
if (cancelHandler is not null)
{
Console.CancelKeyPress -= cancelHandler;
}
}
if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog))
{
Log.Info(runtime.LastMilestoneLog);
}
Log.Info($"SharpEmu execution completed. Result={result} (0x{(int)result:X8})");
if (!string.IsNullOrWhiteSpace(runtime.LastSessionSummary))
{
Log.Info(runtime.LastSessionSummary);
}
if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics))
{
Log.Warn(runtime.LastExecutionDiagnostics);
}
if (!string.IsNullOrWhiteSpace(runtime.LastBasicBlockTrace))
{
Log.Info("BB trace:");
Log.Info(runtime.LastBasicBlockTrace);
}
if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace))
{
Log.Info("Import trace:");
Log.Info(runtime.LastExecutionTrace);
}
if (!string.IsNullOrWhiteSpace(runtime.LastMilestoneLog))
{
Log.Info(runtime.LastMilestoneLog);
}
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
if (result != OrbisGen2Result.ORBIS_GEN2_OK && !string.IsNullOrWhiteSpace(runtime.LastExecutionDiagnostics))
{
Log.Warn(runtime.LastExecutionDiagnostics);
}
if (runtimeOptions.ImportTraceLimit > 0 && !string.IsNullOrWhiteSpace(runtime.LastExecutionTrace))
{
Log.Info("Import trace:");
Log.Info(runtime.LastExecutionTrace);
}
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
}
finally
{
if (debugHost is not null)
{
debugHost.NotifyRunCompleted();
debugHost.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
}
}
private static void EnsureCliConsole()
@ -249,7 +487,9 @@ internal static partial class Program
return handle != 0 && handle != -1;
}
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
private static string[] NormalizeInternalArguments(
string[] args,
out bool isMitigatedChild)
{
isMitigatedChild = false;
var trustedMitigatedChild = string.Equals(
@ -295,14 +535,15 @@ internal static partial class Program
return false;
}
var childArgs = new string[args.Length + 1];
childArgs[0] = MitigatedChildFlag;
for (var i = 0; i < args.Length; i++)
{
childArgs[i + 1] = args[i];
}
var commandLineArguments = Environment.GetCommandLineArgs();
var entryAssemblyPath = commandLineArguments.Length > 0 ? commandLineArguments[0] : null;
string[] childArguments =
Path.GetFileNameWithoutExtension(processPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrWhiteSpace(entryAssemblyPath)
? [entryAssemblyPath, MitigatedChildFlag, .. args]
: [MitigatedChildFlag, .. args];
var commandLine = BuildCommandLine(processPath, childArgs);
var commandLine = BuildCommandLine(processPath, childArguments);
var startupInfoEx = new STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
@ -351,7 +592,7 @@ internal static partial class Program
nint jobHandle = 0;
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
var created = CreateProcessW(
processPath,
null,
cmdLineBuilder,
0,
0,
@ -769,14 +1010,52 @@ internal static partial class Program
private static void PrintUsage()
{
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
Log.Info("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
}
/// <summary>
/// Detects the <c>--debug-server</c> flag and parses its optional
/// <c>host:port</c> endpoint. Returns false only when the flag is present but
/// its endpoint is malformed, so the caller can abort with a clear error.
/// </summary>
private static bool TryGetDebugServerOptions(
string[] args,
out bool enabled,
out SharpEmu.Debugger.Server.DebuggerServerOptions options,
out string error)
{
enabled = false;
options = new SharpEmu.Debugger.Server.DebuggerServerOptions();
error = string.Empty;
foreach (var argument in args)
{
if (string.Equals(argument, "--debug-server", StringComparison.OrdinalIgnoreCase))
{
enabled = true;
continue;
}
const string prefix = "--debug-server=";
if (argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
enabled = true;
if (!SharpEmu.Debugger.Server.DebuggerServerOptions.TryParseEndpoint(argument[prefix.Length..], out options, out error))
{
return false;
}
}
}
return true;
}
private static bool TryParseArguments(
string[] args,
out string ebootPath,
out SharpEmuRuntimeOptions runtimeOptions,
out HostVideoOptions videoOptions,
out LogLevel logLevel,
out string? logFilePath)
{
@ -784,6 +1063,7 @@ internal static partial class Program
{
ebootPath = string.Empty;
runtimeOptions = default;
videoOptions = HostVideoOptions.Default;
logLevel = SharpEmuLog.MinimumLevel;
logFilePath = null;
return false;
@ -792,18 +1072,173 @@ internal static partial class Program
var strictDynlibResolution = false;
var importTraceLimit = 0;
var cpuEngine = CpuExecutionEngine.NativeOnly;
var systemLanguage = 1;
HostWindowMode? windowModeOverride = null;
HostScalingMode? scalingModeOverride = null;
int? windowWidthOverride = null;
int? windowHeightOverride = null;
int? displayIndexOverride = null;
int? refreshRateOverride = null;
bool? vsyncOverride = null;
HostHdrMode? hdrModeOverride = null;
bool? overlayEnabledOverride = null;
PerformanceOverlayMode? overlayModeOverride = null;
PerformanceOverlayCorner? overlayCornerOverride = null;
videoOptions = HostVideoOptions.Default;
logFilePath = null;
logLevel = SharpEmuLog.MinimumLevel;
var pathTokens = new List<string>(args.Length);
for (var i = 0; i < args.Length; i++)
{
var argument = args[i];
if (TrySplitOption(argument, "--console", out var consoleText))
{
if (!string.Equals(consoleText, "ps5", StringComparison.OrdinalIgnoreCase))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
continue;
}
if (TrySplitOption(argument, "--console-language", out var consoleLanguageText))
{
if (!TryParseSystemLanguage(consoleLanguageText, out systemLanguage))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
continue;
}
if (TrySplitOption(argument, "--window-mode", out var windowModeText))
{
if (!TryParseWindowMode(windowModeText, out var windowMode))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
windowModeOverride = windowMode;
continue;
}
if (TrySplitOption(argument, "--resolution", out var resolutionText))
{
if (!TryParseResolution(resolutionText, out var windowWidth, out var windowHeight))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
windowWidthOverride = windowWidth;
windowHeightOverride = windowHeight;
continue;
}
if (TrySplitOption(argument, "--display", out var displayText))
{
if (!int.TryParse(displayText, out var displayIndex) || displayIndex < 0)
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
displayIndexOverride = displayIndex;
continue;
}
if (TrySplitOption(argument, "--refresh-rate", out var refreshText))
{
if (!int.TryParse(refreshText, out var refreshRate) || refreshRate < 0)
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
refreshRateOverride = refreshRate;
continue;
}
if (TrySplitOption(argument, "--scaling", out var scalingText))
{
if (!TryParseScalingMode(scalingText, out var scalingMode))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
scalingModeOverride = scalingMode;
continue;
}
if (TrySplitOption(argument, "--vsync", out var vsyncText))
{
if (!TryParseSwitch(vsyncText, out var vsync))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
vsyncOverride = vsync;
continue;
}
if (TrySplitOption(argument, "--overlay", out var overlayText))
{
if (!TryParseSwitch(overlayText, out var overlayEnabled))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
overlayEnabledOverride = overlayEnabled;
continue;
}
if (TrySplitOption(argument, "--overlay-mode", out var overlayModeText))
{
if (!Enum.TryParse<PerformanceOverlayMode>(overlayModeText, true, out var overlayMode) ||
!Enum.IsDefined(overlayMode))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
overlayModeOverride = overlayMode;
continue;
}
if (TrySplitOption(argument, "--overlay-corner", out var overlayCornerText))
{
if (!Enum.TryParse<PerformanceOverlayCorner>(overlayCornerText, true, out var overlayCorner) ||
!Enum.IsDefined(overlayCorner))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
overlayCornerOverride = overlayCorner;
continue;
}
if (TrySplitOption(argument, "--hdr", out var hdrText))
{
if (!TryParseHdrMode(hdrText, out var hdrMode))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
hdrModeOverride = hdrMode;
continue;
}
if (string.Equals(argument, "--strict", StringComparison.OrdinalIgnoreCase))
{
strictDynlibResolution = true;
continue;
}
// The debug-server endpoint is parsed separately (see
// TryGetDebugServerOptions); accept the flag here so it is not
// rejected as an unknown option or mistaken for the eboot path.
if (string.Equals(argument, "--debug-server", StringComparison.OrdinalIgnoreCase) ||
argument.StartsWith("--debug-server=", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (string.Equals(argument, "--trace-imports", StringComparison.OrdinalIgnoreCase))
{
importTraceLimit = DefaultImportTraceLimit;
@ -947,12 +1382,199 @@ internal static partial class Program
runtimeOptions = new SharpEmuRuntimeOptions
{
CpuEngine = cpuEngine,
SystemLanguage = systemLanguage,
StrictDynlibResolution = strictDynlibResolution,
ImportTraceLimit = importTraceLimit,
};
var configuredVideoOptions = LoadConfiguredVideoOptions(ebootPath);
videoOptions = (configuredVideoOptions with
{
WindowMode = windowModeOverride ?? configuredVideoOptions.WindowMode,
ScalingMode = scalingModeOverride ?? configuredVideoOptions.ScalingMode,
Width = windowWidthOverride ?? configuredVideoOptions.Width,
Height = windowHeightOverride ?? configuredVideoOptions.Height,
DisplayIndex = displayIndexOverride ?? configuredVideoOptions.DisplayIndex,
RefreshRate = refreshRateOverride ?? configuredVideoOptions.RefreshRate,
VSync = vsyncOverride ?? configuredVideoOptions.VSync,
HdrMode = hdrModeOverride ?? configuredVideoOptions.HdrMode,
OverlayEnabled = overlayEnabledOverride ?? configuredVideoOptions.OverlayEnabled,
OverlayMode = overlayModeOverride ?? configuredVideoOptions.OverlayMode,
OverlayCorner = overlayCornerOverride ?? configuredVideoOptions.OverlayCorner,
}).Normalize();
return true;
}
private static HostVideoOptions LoadConfiguredVideoOptions(string ebootPath)
{
var defaults = HostVideoOptions.Default;
try
{
var effective = EffectiveLaunchSettings.Resolve(
GuiSettings.Load(),
PerGameSettings.Load(TryReadTitleId(ebootPath)));
var windowMode = TryParseWindowMode(effective.WindowMode, out var parsedWindowMode)
? parsedWindowMode
: defaults.WindowMode;
var scalingMode = TryParseScalingMode(effective.ScalingMode, out var parsedScalingMode)
? parsedScalingMode
: defaults.ScalingMode;
var hasResolution = TryParseResolution(
effective.Resolution,
out var configuredWidth,
out var configuredHeight);
var hdrMode = TryParseHdrMode(effective.HdrMode, out var parsedHdrMode)
? parsedHdrMode
: defaults.HdrMode;
return new HostVideoOptions
{
WindowMode = windowMode,
ScalingMode = scalingMode,
Width = hasResolution ? configuredWidth : defaults.Width,
Height = hasResolution ? configuredHeight : defaults.Height,
DisplayIndex = effective.DisplayIndex,
RefreshRate = effective.RefreshRate,
VSync = effective.VSync,
HdrMode = hdrMode,
OverlayEnabled = effective.OverlayEnabled,
OverlayCorner = Enum.TryParse<PerformanceOverlayCorner>(effective.OverlayCorner, true, out var corner)
? corner : defaults.OverlayCorner,
OverlayMode = Enum.TryParse<PerformanceOverlayMode>(effective.OverlayMode, true, out var mode)
? mode : defaults.OverlayMode,
}.Normalize();
}
catch (Exception exception)
{
Console.Error.WriteLine(
$"[LOADER][WARN] GUI video settings could not be loaded; using defaults: {exception.Message}");
return defaults;
}
}
private static bool TrySplitOption(string argument, string name, out string value)
{
var prefix = name + "=";
if (argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
value = argument[prefix.Length..];
return true;
}
value = string.Empty;
return false;
}
private static bool TryParseWindowMode(string value, out HostWindowMode mode)
{
mode = value.ToLowerInvariant() switch
{
"windowed" => HostWindowMode.Windowed,
"borderless" => HostWindowMode.Borderless,
"exclusive" or "fullscreen" => HostWindowMode.ExclusiveFullscreen,
_ => (HostWindowMode)(-1),
};
return Enum.IsDefined(mode);
}
private static bool TryParseSystemLanguage(string value, out int language)
{
language = value.ToLowerInvariant() switch
{
"japanese" => 0,
"englishus" => 1,
"french" => 2,
"spanishspain" => 3,
"german" => 4,
"italian" => 5,
"dutch" => 6,
"portugueseportugal" => 7,
"russian" => 8,
"korean" => 9,
"chinesetraditional" => 10,
"chinesesimplified" => 11,
"finnish" => 12,
"swedish" => 13,
"danish" => 14,
"norwegian" => 15,
"polish" => 16,
"portuguesebrazil" => 17,
"englishuk" => 18,
"turkish" => 19,
"spanishlatinamerica" => 20,
"arabic" => 21,
"frenchcanada" => 22,
"czech" => 23,
"hungarian" => 24,
"greek" => 25,
"romanian" => 26,
"thai" => 27,
"vietnamese" => 28,
"indonesian" => 29,
"ukrainian" => 30,
_ => -1,
};
return language >= 0;
}
private static bool TryParseScalingMode(string value, out HostScalingMode mode)
{
mode = value.ToLowerInvariant() switch
{
"fit" => HostScalingMode.Fit,
"cover" => HostScalingMode.Cover,
"stretch" => HostScalingMode.Stretch,
"integer" => HostScalingMode.Integer,
_ => (HostScalingMode)(-1),
};
return Enum.IsDefined(mode);
}
private static bool TryParseHdrMode(string value, out HostHdrMode mode)
{
mode = value.ToLowerInvariant() switch
{
"auto" => HostHdrMode.Auto,
"on" or "true" or "1" => HostHdrMode.On,
"off" or "false" or "0" => HostHdrMode.Off,
_ => (HostHdrMode)(-1),
};
return Enum.IsDefined(mode);
}
private static bool TryParseResolution(string value, out int width, out int height)
{
var parts = value.Split('x', 'X');
if (parts.Length == 2 && int.TryParse(parts[0], out width) && int.TryParse(parts[1], out height) &&
width >= 640 && height >= 360)
{
return true;
}
width = 0;
height = 0;
return false;
}
private static bool TryParseSwitch(string value, out bool enabled)
{
if (value is "1" || value.Equals("on", StringComparison.OrdinalIgnoreCase) ||
value.Equals("true", StringComparison.OrdinalIgnoreCase))
{
enabled = true;
return true;
}
if (value is "0" || value.Equals("off", StringComparison.OrdinalIgnoreCase) ||
value.Equals("false", StringComparison.OrdinalIgnoreCase))
{
enabled = false;
return true;
}
enabled = false;
return false;
}
private static bool TryParseCpuEngine(string valueText, out CpuExecutionEngine engine)
{
if (string.Equals(valueText, "native", StringComparison.OrdinalIgnoreCase) ||
@ -1131,7 +1753,7 @@ internal static partial class Program
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CreateProcessW(
string applicationName,
string? applicationName,
StringBuilder commandLine,
nint processAttributes,
nint threadAttributes,
@ -1188,4 +1810,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);
}

View File

@ -7,6 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\SharpEmu.Debugger\SharpEmu.Debugger.csproj" />
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
@ -16,35 +17,55 @@ SPDX-License-Identifier: GPL-2.0-or-later
console window; CLI mode re-attaches to the parent terminal's console. -->
<OutputType>WinExe</OutputType>
<AssemblyName>SharpEmu</AssemblyName>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
<!-- osx-x64 is the macOS target: the CPU backend executes guest x86-64
natively, so on Apple Silicon it runs under Rosetta 2. -->
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers>
<!-- A plain "dotnet publish" with no -r defaults $(RuntimeIdentifier) to
the host's own RID; see Directory.Build.props, which is where that
default actually has to live (PublishDir's RID suffix is decided
there, evaluated before this file, so a default set only here would
be too late for it). -->
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<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>
<!-- Background GC's write-watch revisit calls FlushProcessWriteBuffers,
which on macOS uses thread_get_register_pointer_values; under Rosetta 2
that Mach call can stall indefinitely on threads executing translated
guest code, wedging the whole runtime (every allocating thread then
blocks behind the never-finishing GC). Non-concurrent GC never takes
that path. Windows and Linux keep concurrent GC. -->
<PropertyGroup Condition="$([System.String]::Copy('$(RuntimeIdentifier)').StartsWith('osx'))">
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\LICENSE.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
@ -56,19 +77,107 @@ SPDX-License-Identifier: GPL-2.0-or-later
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
<Visible>False</Visible>
</Content>
<Content Include="..\SharpEmu.LibAtrac9\LICENSE.txt">
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<TargetPath>licenses\LibAtrac9.txt</TargetPath>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<Visible>False</Visible>
</Content>
</ItemGroup>
<!-- Keep glfw as a loose file next to the executable; every other native
library is embedded into the single-file bundle. -->
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
<Target Name="KeepLibAtrac9External" BeforeTargets="_ComputeFilesToBundle">
<ItemGroup>
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw'))" />
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
<ResolvedFileToPublish Update="@(ResolvedFileToPublish)"
Condition="'%(Filename)%(Extension)' == 'SharpEmu.LibAtrac9.dll'">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<RelativePath>plugins\SharpEmu.LibAtrac9.dll</RelativePath>
</ResolvedFileToPublish>
</ItemGroup>
</Target>
<!-- These are native debug symbols emitted by Skia/HarfBuzz, not managed
symbols that single-file publish can bundle. They are not needed at
runtime and would otherwise add more than 100 MB to every release. -->
<Target Name="RemoveNativeDebugSymbols" AfterTargets="Publish">
<ItemGroup>
<_NativeDebugSymbols Include="$(PublishDir)**\*.pdb" />
</ItemGroup>
<Delete Files="@(_NativeDebugSymbols)" />
</Target>
<!-- Native FFmpeg libraries publish into a subfolder next to the
executable instead of sitting loose beside it, so the publish
directory stays uncluttered as more native deps get added. The folder
name is a fixed constant, not derived from the RID/architecture: each
publish output only ever holds one architecture's binaries anyway, so
varying the name added a class of bugs (RID resolution timing, host-OS
vs. target-RID mixups) for no benefit. Runtime code (FfmpegRuntime)
uses the same literal "plugins" folder name. -->
<PropertyGroup>
<NativeLibraryFolderName>plugins</NativeLibraryFolderName>
</PropertyGroup>
<PropertyGroup>
<FfmpegRuntimeTag>3b502d4</FfmpegRuntimeTag>
<FfmpegRuntimeDir>
$(BaseIntermediateOutputPath)ffmpeg-runtime/$(FfmpegRuntimeTag)/$(RuntimeIdentifier)</FfmpegRuntimeDir>
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'win-x64'">ffmpeg-windows-x64.zip</FfmpegRuntimePackage>
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'linux-x64'">ffmpeg-linux-x64.zip</FfmpegRuntimePackage>
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-x64'">ffmpeg-macos-x64.zip</FfmpegRuntimePackage>
<FfmpegRuntimePackage Condition="'$(RuntimeIdentifier)' == 'osx-arm64'">ffmpeg-macos-arm64.zip</FfmpegRuntimePackage>
<FfmpegRuntimeArchive>$(FfmpegRuntimeDir)/$(FfmpegRuntimePackage)</FfmpegRuntimeArchive>
<FfmpegRuntimeExtractDir>$(FfmpegRuntimeDir)/extracted</FfmpegRuntimeExtractDir>
</PropertyGroup>
<Target Name="FetchFfmpegRuntime"
BeforeTargets="Publish;Build"
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
<DownloadFile
SourceUrl="https://github.com/sharpemu/ffmpeg-core/releases/download/$(FfmpegRuntimeTag)/$(FfmpegRuntimePackage)"
DestinationFolder="$(FfmpegRuntimeDir)"
Condition="!Exists('$(FfmpegRuntimeArchive)')" />
<Unzip
SourceFiles="$(FfmpegRuntimeArchive)"
DestinationFolder="$(FfmpegRuntimeExtractDir)"
Condition="!Exists('$(FfmpegRuntimeExtractDir)')" />
</Target>
<Target Name="PublishFfmpegRuntime"
AfterTargets="Publish"
DependsOnTargets="FetchFfmpegRuntime"
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
<!-- Keyed off the target $(RuntimeIdentifier), not the host OS: publishing
e.g. linux-x64 from a Windows machine is a supported cross-publish,
and the extracted archive's own layout (bin/*.dll vs lib/*.so*) only
depends on which platform's ffmpeg-core package was fetched. -->
<ItemGroup>
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
</ItemGroup>
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
DestinationFolder="$(PublishDir)$(NativeLibraryFolderName)"
SkipUnchangedFiles="true" />
</Target>
<!-- Mirrors PublishFfmpegRuntime for plain `dotnet build`: devs running
straight out of the build output directory (no publish step) still
need the FFmpeg plugins present, otherwise AvPlayer/Bink video probing
throws NotSupportedException the first time a guest opens a movie. -->
<Target Name="BuildFfmpegRuntime"
AfterTargets="Build"
DependsOnTargets="FetchFfmpegRuntime"
Condition="'$(RuntimeIdentifier)' != '' And '$(FfmpegRuntimePackage)' != ''">
<ItemGroup>
<_FfmpegRuntimeFiles Condition="$(RuntimeIdentifier.StartsWith('win'))"
Include="$(FfmpegRuntimeExtractDir)/bin/*.dll" />
<_FfmpegRuntimeFiles Condition="!$(RuntimeIdentifier.StartsWith('win'))"
Include="$(FfmpegRuntimeExtractDir)/lib/*.so;$(FfmpegRuntimeExtractDir)/lib/*.so.*;$(FfmpegRuntimeExtractDir)/lib/*.dylib" />
</ItemGroup>
<Copy SourceFiles="@(_FfmpegRuntimeFiles)"
DestinationFolder="$(OutDir)$(NativeLibraryFolderName)"
SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="SharpEmu" />
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Required by Avalonia NativeControlHost on Windows 10 and 11. -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>

View File

@ -1,491 +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.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.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
"Silk.NET.Windowing": "[2.23.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"Avalonia": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
"dependencies": {
"Avalonia.BuildServices": "11.3.2",
"Avalonia.Remote.Protocol": "11.3.18",
"MicroCom.Runtime": "0.11.0"
}
},
"Avalonia.Desktop": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
"dependencies": {
"Avalonia": "11.3.18",
"Avalonia.Native": "11.3.18",
"Avalonia.Skia": "11.3.18",
"Avalonia.Win32": "11.3.18",
"Avalonia.X11": "11.3.18"
}
},
"Avalonia.Fonts.Inter": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Avalonia.Themes.Fluent": {
"type": "CentralTransitive",
"requested": "[11.3.18, )",
"resolved": "11.3.18",
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"Iced": {
"type": "CentralTransitive",
"requested": "[1.21.0, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"Silk.NET.Core": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.EXT": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Vulkan.Extensions.KHR": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Tmds.DBus.Protocol": {
"type": "CentralTransitive",
"requested": "[0.21.3, )",
"resolved": "0.21.3",
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
}
},
"net10.0/linux-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-arm64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/win-x64": {
"Avalonia.Angle.Windows.Natives": {
"type": "Transitive",
"resolved": "2.1.25547.20250602",
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
},
"Avalonia.Native": {
"type": "Transitive",
"resolved": "11.3.18",
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
"dependencies": {
"Avalonia": "11.3.18"
}
},
"HarfBuzzSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
},
"HarfBuzzSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
},
"HarfBuzzSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "8.3.1.1",
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
}
}
}

View File

@ -3,33 +3,38 @@
using System.Buffers.Binary;
using System.Text;
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Core.Cpu;
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
private enum EntryFrameKind
{
ProcessEntry,
ModuleInitializer,
}
private const ulong StackBaseAddress = 0x7FFF_F000_0000UL;
// The top of the x86-64 user address space (0x7FFD..0x7FFF) is only
// freely mappable on Windows; on macOS/Linux it hosts the dyld shared
// cache / vdso and (under Rosetta 2) the translator runtime, so POSIX
// hosts use the equivalent layout one slot lower at 0x6FFx.
private static readonly ulong StackBaseAddress = OperatingSystem.IsWindows() ? 0x7FFF_F000_0000UL : 0x6FFF_F000_0000UL;
private const ulong StackSize = 0x0020_0000UL;
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
private static readonly ulong TlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
private const ulong TlsSize = 0x0001_0000UL;
private const ulong TlsPrefixSize = 0x0000_1000UL;
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
private const ulong ReturnToHostStubBaseAddress = 0x7FFD_C000_0000UL;
// The static TLS blocks live at negative offsets from the TCB (FreeBSD
// 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;
private static readonly ulong ReturnToHostStubBaseAddress = OperatingSystem.IsWindows() ? 0x7FFD_C000_0000UL : 0x6FFD_C000_0000UL;
private const ulong BootstrapRegionSize = 0x0000_1000UL;
private const ulong ReturnToHostStubStride = 0x0100_0000UL;
private const ulong BootstrapPayloadResultOffset = 0x28UL;
@ -83,8 +88,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
{
@ -92,7 +97,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;
}
}
@ -105,8 +111,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
{
@ -121,11 +127,26 @@ 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;
}
}
public bool TryInstallAdditionalModule(
IReadOnlyDictionary<ulong, string> importStubs,
IReadOnlyDictionary<string, ulong> runtimeSymbols,
out string? error)
{
if (_nativeCpuBackend is not DirectExecutionBackend backend)
{
error = "the active CPU backend does not support runtime modules";
return false;
}
return backend.TryInstallAdditionalModule(importStubs, runtimeSymbols, out error);
}
private OrbisGen2Result DispatchEntryCore(
ulong entryPoint,
Generation generation,
@ -135,7 +156,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;
@ -266,7 +287,23 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
entryFrameDiagnostic,
Environment.NewLine,
"CpuEngine: native-only");
// Frame boundaries an attached debugger observes; null hook = a branch.
var debugHook = executionOptions.DebugHook;
var debugFrame = debugHook is null
? null
: new CpuContextDebugFrame(
frameKind == EntryFrameKind.ProcessEntry
? CpuDebugFrameKind.ProcessEntry
: CpuDebugFrameKind.ModuleInitializer,
entryPoint,
processImageName,
context,
effectiveImportStubs);
debugHook?.OnFrameEnter(debugFrame!);
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
// Let backend stall reports reference the same frame as entry.
(_nativeCpuBackend as DirectExecutionBackend)?.SetActiveDebugFrame(debugFrame);
if (_nativeCpuBackend.TryExecute(
context,
entryPoint,
@ -276,6 +313,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
executionOptions,
out var nativeResult))
{
debugHook?.OnFrameExit(debugFrame!, nativeResult);
LastSessionSummary = new CpuSessionSummary(
nativeResult,
nativeResult == OrbisGen2Result.ORBIS_GEN2_OK
@ -290,6 +328,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return nativeResult;
}
debugHook?.OnFrameExit(debugFrame!, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
var backendName = string.IsNullOrWhiteSpace(_nativeCpuBackend.BackendName)
? "native-backend"
: _nativeCpuBackend.BackendName;
@ -307,7 +347,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);
@ -366,11 +406,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)
@ -396,35 +444,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))
{
@ -457,6 +524,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,
@ -653,6 +727,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return _virtualMemory.TryWrite(address, buffer);
}
/// <summary>
/// True when the disposed native backend left its session state alive
/// because guest workers were still executing guest code. The guest
/// address space must then stay mapped as well.
/// </summary>
internal bool NativeSessionLeaked { get; private set; }
public void Dispose()
{
if (_nativeCpuBackend is IDisposable disposableBackend)
@ -660,6 +741,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
disposableBackend.Dispose();
}
NativeSessionLeaked = _nativeCpuBackend is DirectExecutionBackend { GuestSessionLeaked: true };
_nativeCpuBackend = null;
}
}

View File

@ -1,15 +1,26 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Debugging;
namespace SharpEmu.Core.Cpu;
public readonly struct CpuExecutionOptions
{
public bool EnableDisasmDiagnostics { get; init; }
public CpuExecutionEngine CpuEngine { get; init; }
public bool StrictDynlibResolution { get; init; }
public int ImportTraceLimit { get; init; }
/// <summary>
/// An optional debugger attached to this execution session. When set, the
/// dispatcher notifies it at each frame boundary via
/// <see cref="ICpuDebugHook.OnFrameEnter"/> / <see cref="ICpuDebugHook.OnFrameExit"/>.
/// Null when no debugger is attached, which is the default and imposes no
/// runtime cost.
/// </summary>
public ICpuDebugHook? DebugHook { get; init; }
}

View File

@ -0,0 +1,66 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>
/// Adapts a live <see cref="CpuContext"/> to <see cref="ICpuDebugFrame"/>. The
/// dispatcher creates one of these around the guest context it is about to run
/// and passes it to the attached <see cref="ICpuDebugHook"/>; every accessor
/// forwards directly to the underlying context.
/// </summary>
internal sealed class CpuContextDebugFrame : ICpuDebugFrame
{
private readonly CpuContext _context;
internal CpuContextDebugFrame(
CpuDebugFrameKind kind,
ulong entryPoint,
string label,
CpuContext context,
IReadOnlyDictionary<ulong, string> importStubs)
{
Kind = kind;
EntryPoint = entryPoint;
Label = label ?? string.Empty;
_context = context ?? throw new ArgumentNullException(nameof(context));
ImportStubs = importStubs ?? new Dictionary<ulong, string>();
}
public CpuDebugFrameKind Kind { get; }
public Generation Generation => _context.TargetGeneration;
public ulong EntryPoint { get; }
public string Label { get; }
public ICpuMemory Memory => _context.Memory;
public ulong GetRegister(CpuRegister register) => _context[register];
public void SetRegister(CpuRegister register, ulong value) => _context[register] = value;
public ulong Rip
{
get => _context.Rip;
set => _context.Rip = value;
}
public ulong Rflags
{
get => _context.Rflags;
set => _context.Rflags = value;
}
public ulong FsBase => _context.FsBase;
public ulong GsBase => _context.GsBase;
public void GetXmm(int registerIndex, out ulong low, out ulong high)
=> _context.GetXmmRegister(registerIndex, out low, out high);
public IReadOnlyDictionary<ulong, string> ImportStubs { get; }
}

View File

@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>
/// Identifies the kind of guest entry frame a debugger is observing. The
/// dispatcher enters a fresh frame for the process entry point and for every
/// module initializer, so the debug layer can label stops accordingly.
/// </summary>
public enum CpuDebugFrameKind
{
/// <summary>The guest process entry point (<c>eboot.bin</c> start).</summary>
ProcessEntry,
/// <summary>A module DT_INIT / initializer routine.</summary>
ModuleInitializer,
}

View File

@ -0,0 +1,70 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>The kind of execution stall the backend detected.</summary>
public enum CpuStallKind
{
/// <summary>
/// The guest is repeatedly re-dispatching the same import with no forward
/// progress — most commonly a spin on a mutex lock/unlock pair.
/// </summary>
ImportLoop,
}
/// <summary>
/// Details of a detected stall handed to <see cref="ICpuDebugHook.OnStall"/>.
/// Reported from the emulation thread at the point the backend recognises the
/// livelock, before it forces the guest out of the loop.
/// </summary>
public readonly struct CpuStallInfo
{
public CpuStallInfo(
CpuStallKind kind,
string? nid,
ulong instructionPointer,
long dispatchIndex,
ulong argument0,
ulong argument1,
string detail,
string? libraryName = null,
string? functionName = null)
{
Kind = kind;
Nid = nid;
InstructionPointer = instructionPointer;
DispatchIndex = dispatchIndex;
Argument0 = argument0;
Argument1 = argument1;
Detail = detail ?? string.Empty;
LibraryName = libraryName;
FunctionName = functionName;
}
public CpuStallKind Kind { get; }
/// <summary>The NID of the import being spun on, when known.</summary>
public string? Nid { get; }
/// <summary>The guest return address of the looping import dispatch.</summary>
public ulong InstructionPointer { get; }
/// <summary>The import dispatch counter at detection time.</summary>
public long DispatchIndex { get; }
/// <summary>The first two guest ABI arguments at stall detection.</summary>
public ulong Argument0 { get; }
public ulong Argument1 { get; }
/// <summary>The resolved HLE export, when the NID is registered.</summary>
public string? LibraryName { get; }
public string? FunctionName { get; }
public bool IsResolved => !string.IsNullOrWhiteSpace(FunctionName);
/// <summary>A human-readable one-line summary of the stall.</summary>
public string Detail { get; }
}

View File

@ -0,0 +1,66 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>
/// A live view of the guest CPU state at a dispatch boundary, handed to an
/// <see cref="ICpuDebugHook"/> so a debugger can read and mutate registers and
/// guest memory without taking a dependency on the concrete
/// <c>CpuContext</c>/<c>CpuDispatcher</c> types.
/// </summary>
/// <remarks>
/// The frame instance is only valid for the duration of the hook call that
/// receives it (between <see cref="ICpuDebugHook.OnFrameEnter"/> and the
/// matching <see cref="ICpuDebugHook.OnFrameExit"/>). Reads and writes are
/// forwarded straight to the underlying guest context, so mutations made from
/// a hook are observed by the CPU backend when it resumes the frame.
/// </remarks>
public interface ICpuDebugFrame
{
/// <summary>The kind of frame being executed.</summary>
CpuDebugFrameKind Kind { get; }
/// <summary>The guest ABI generation this frame targets.</summary>
Generation Generation { get; }
/// <summary>The guest virtual address the frame begins executing at.</summary>
ulong EntryPoint { get; }
/// <summary>
/// A human-readable label for the frame (process image name or module name).
/// </summary>
string Label { get; }
/// <summary>Guest-addressable memory for this frame.</summary>
ICpuMemory Memory { get; }
/// <summary>Reads a general-purpose register.</summary>
ulong GetRegister(CpuRegister register);
/// <summary>Overwrites a general-purpose register.</summary>
void SetRegister(CpuRegister register, ulong value);
/// <summary>The instruction pointer.</summary>
ulong Rip { get; set; }
/// <summary>The flags register.</summary>
ulong Rflags { get; set; }
/// <summary>The FS segment base (guest TLS pointer).</summary>
ulong FsBase { get; }
/// <summary>The GS segment base.</summary>
ulong GsBase { get; }
/// <summary>Reads the 128-bit value of an XMM register.</summary>
void GetXmm(int registerIndex, out ulong low, out ulong high);
/// <summary>
/// The import stubs (guest address to NID) resolved for this frame, so a
/// debugger can annotate calls into HLE exports.
/// </summary>
IReadOnlyDictionary<ulong, string> ImportStubs { get; }
}

View File

@ -0,0 +1,49 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>
/// The seam the CPU dispatcher uses to notify an attached debugger when guest
/// execution crosses a frame boundary. Implemented outside of Core (for
/// example by <c>SharpEmu.Debugger</c>) and supplied through
/// <see cref="CpuExecutionOptions.DebugHook"/>.
/// </summary>
/// <remarks>
/// This is intentionally coarse-grained: it exposes the entry and exit of each
/// dispatched frame rather than per-instruction stepping. Per-instruction
/// control requires cooperation from the native execution backend and is layered
/// on top of this seam as the backend gains support; keeping the dispatcher-level
/// contract stable lets the debugger infrastructure exist independently of that
/// work. Implementations must be thread-safe: frames may be dispatched from the
/// dedicated emulation thread while a debug server services clients on its own
/// threads.
/// </remarks>
public interface ICpuDebugHook
{
/// <summary>
/// Invoked immediately before the native backend begins executing a frame.
/// The debugger may inspect or mutate <paramref name="frame"/> and may block
/// the calling thread (for example, to honour a pause request) before
/// returning to allow execution to proceed.
/// </summary>
void OnFrameEnter(ICpuDebugFrame frame);
/// <summary>
/// Invoked after a frame completes, whether it returned to the host or
/// terminated with an error. <paramref name="frame"/> reflects the final
/// guest state.
/// </summary>
void OnFrameExit(ICpuDebugFrame frame, OrbisGen2Result result);
/// <summary>
/// Invoked from the emulation thread when the backend detects an execution
/// stall (for example a mutex spin loop) in the running frame, before it
/// forces the guest out of the loop. As with <see cref="OnFrameEnter"/>, the
/// implementation may inspect <paramref name="frame"/> and block to honour a
/// break before returning to let the backend proceed.
/// </summary>
void OnStall(ICpuDebugFrame frame, CpuStallInfo info);
}

View File

@ -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);
}
}

View File

@ -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,
}

View File

@ -0,0 +1,149 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Numerics;
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// Calculates the results of the SHA extension instructions for instruction recovery.
/// Dword 0 of a value is bits 31:0 and dword 3 is bits 127:96, as in the Intel SDM.
/// </summary>
public static class ShaInstructionEmulator
{
public static UInt128 Sha1Rnds4(UInt128 source1, UInt128 source2, int function)
{
var a = Dword(source1, 3);
var b = Dword(source1, 2);
var c = Dword(source1, 1);
var d = Dword(source1, 0);
var e = 0u;
var k = (function & 3) switch
{
0 => 0x5A827999u,
1 => 0x6ED9EBA1u,
2 => 0x8F1BBCDCu,
_ => 0xCA62C1D6u,
};
// The first message dword already carries E (SHA1NEXTE adds it).
for (var round = 0; round < 4; round++)
{
var next = Sha1Function(function, b, c, d) + BitOperations.RotateLeft(a, 5) + Dword(source2, 3 - round) + e + k;
e = d;
d = c;
c = BitOperations.RotateLeft(b, 30);
b = a;
a = next;
}
return FromDwords(d, c, b, a);
}
public static UInt128 Sha1Nexte(UInt128 source1, UInt128 source2)
{
var e = BitOperations.RotateLeft(Dword(source1, 3), 30);
return FromDwords(Dword(source2, 0), Dword(source2, 1), Dword(source2, 2), Dword(source2, 3) + e);
}
public static UInt128 Sha1Msg1(UInt128 source1, UInt128 source2)
{
var w0 = Dword(source1, 3);
var w1 = Dword(source1, 2);
var w2 = Dword(source1, 1);
var w3 = Dword(source1, 0);
var w4 = Dword(source2, 3);
var w5 = Dword(source2, 2);
return FromDwords(w5 ^ w3, w4 ^ w2, w3 ^ w1, w2 ^ w0);
}
public static UInt128 Sha1Msg2(UInt128 source1, UInt128 source2)
{
var w13 = Dword(source2, 2);
var w14 = Dword(source2, 1);
var w15 = Dword(source2, 0);
var w16 = BitOperations.RotateLeft(Dword(source1, 3) ^ w13, 1);
var w17 = BitOperations.RotateLeft(Dword(source1, 2) ^ w14, 1);
var w18 = BitOperations.RotateLeft(Dword(source1, 1) ^ w15, 1);
var w19 = BitOperations.RotateLeft(Dword(source1, 0) ^ w16, 1);
return FromDwords(w19, w18, w17, w16);
}
// SHA256RNDS2 takes its two round-constant-plus-message dwords from XMM0[63:0].
public static UInt128 Sha256Rnds2(UInt128 source1, UInt128 source2, UInt128 xmm0)
{
var a = Dword(source2, 3);
var b = Dword(source2, 2);
var c = Dword(source1, 3);
var d = Dword(source1, 2);
var e = Dword(source2, 1);
var f = Dword(source2, 0);
var g = Dword(source1, 1);
var h = Dword(source1, 0);
for (var round = 0; round < 2; round++)
{
var t1 = Ch(e, f, g) + BigSigma1(e) + Dword(xmm0, round) + h;
var t2 = Maj(a, b, c) + BigSigma0(a);
h = g;
g = f;
f = e;
e = d + t1;
d = c;
c = b;
b = a;
a = t1 + t2;
}
return FromDwords(f, e, b, a);
}
public static UInt128 Sha256Msg1(UInt128 source1, UInt128 source2)
{
var w4 = Dword(source2, 0);
var w3 = Dword(source1, 3);
var w2 = Dword(source1, 2);
var w1 = Dword(source1, 1);
var w0 = Dword(source1, 0);
return FromDwords(w0 + SmallSigma0(w1), w1 + SmallSigma0(w2), w2 + SmallSigma0(w3), w3 + SmallSigma0(w4));
}
public static UInt128 Sha256Msg2(UInt128 source1, UInt128 source2)
{
var w14 = Dword(source2, 2);
var w15 = Dword(source2, 3);
var w16 = Dword(source1, 0) + SmallSigma1(w14);
var w17 = Dword(source1, 1) + SmallSigma1(w15);
var w18 = Dword(source1, 2) + SmallSigma1(w16);
var w19 = Dword(source1, 3) + SmallSigma1(w17);
return FromDwords(w16, w17, w18, w19);
}
public static uint Dword(UInt128 value, int index) => (uint)(value >> (32 * index));
public static UInt128 FromDwords(uint dword0, uint dword1, uint dword2, uint dword3) =>
((UInt128)dword3 << 96) | ((UInt128)dword2 << 64) | ((UInt128)dword1 << 32) | dword0;
private static uint Sha1Function(int function, uint b, uint c, uint d) => (function & 3) switch
{
0 => (b & c) ^ (~b & d),
2 => (b & c) ^ (b & d) ^ (c & d),
_ => b ^ c ^ d,
};
private static uint Ch(uint e, uint f, uint g) => (e & f) ^ (~e & g);
private static uint Maj(uint a, uint b, uint c) => (a & b) ^ (a & c) ^ (b & c);
private static uint BigSigma0(uint x) =>
BitOperations.RotateRight(x, 2) ^ BitOperations.RotateRight(x, 13) ^ BitOperations.RotateRight(x, 22);
private static uint BigSigma1(uint x) =>
BitOperations.RotateRight(x, 6) ^ BitOperations.RotateRight(x, 11) ^ BitOperations.RotateRight(x, 25);
private static uint SmallSigma0(uint x) =>
BitOperations.RotateRight(x, 7) ^ BitOperations.RotateRight(x, 18) ^ (x >> 3);
private static uint SmallSigma1(uint x) =>
BitOperations.RotateRight(x, 17) ^ BitOperations.RotateRight(x, 19) ^ (x >> 10);
}

View File

@ -0,0 +1,48 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// Calculates the bit-field results for SSE4a instruction recovery.
/// </summary>
public static class Sse4aBitFieldEmulator
{
public static bool IsValidBitField(int length, int index)
{
var len = length & 0x3F;
var idx = index & 0x3F;
return (len != 0 || idx == 0) && (len == 0 ? idx == 0 : idx + len <= 64);
}
public static ulong ExtractBitField(ulong value, int length, int index)
{
var len = length & 0x3F;
var idx = index & 0x3F;
if (len == 0)
{
len = 64;
}
// Results past bit 63 are undefined; limit the field to the remaining bits.
len = Math.Min(len, 64 - idx);
var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
return (value >> idx) & mask;
}
public static ulong InsertBitField(ulong destination, ulong source, int length, int index)
{
var len = length & 0x3F;
var idx = index & 0x3F;
if (len == 0)
{
len = 64;
}
len = Math.Min(len, 64 - idx);
var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
var destinationClearMask = fieldMask << idx;
var sourceField = (source & fieldMask) << idx;
return (destination & ~destinationClearMask) | sourceField;
}
}

View File

@ -26,22 +26,21 @@ public sealed unsafe class CpuPatcher : IDisposable
internal unsafe class UnsafeCodeReader : Iced.Intel.CodeReader
{
private byte* _start;
private byte* _current;
private int _length;
private readonly byte* _start;
private readonly ulong _length;
public ulong Position { get; private set; }
public UnsafeCodeReader(byte* start, int length)
public UnsafeCodeReader(byte* start, ulong length)
{
_start = start;
_current = start;
_length = length;
}
public override int ReadByte()
{
if (_current >= _start + _length)
if (Position >= _length)
return -1;
return *_current++;
return _start[Position++];
}
}
}

View File

@ -0,0 +1,189 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Threading;
using Iced.Intel;
using SharpEmu.Core.Cpu.Emulation;
namespace SharpEmu.Core.Cpu.Native;
// Recover supported instructions only after an illegal-instruction fault.
// Leave native instructions and unrelated register values unchanged.
public sealed partial class DirectExecutionBackend
{
// Byte offset of Xmm0 within the Win64 CONTEXT record: FltSave (the XMM_SAVE_AREA32/FXSAVE
// image) starts right after Rip at offset 256, and XmmRegisters[0] sits 160 bytes into that
// area (32-byte header + 8 legacy x87/MMX slots x 16 bytes). 256 + 160 = 416 (0x1A0). Cross-
// checked against this file's own Win64ContextSize (0x4D0): rebuilding the whole CONTEXT
// layout field-by-field from offset 0 lands on the same 0x4D0 total, which would not happen
// if this offset (or anything before it) were wrong.
private const int Win64ContextXmm0Offset = 0x1A0;
private static int _sse4aSoftwareFallbackAnnounced;
private static long _sse4aInstructionsEmulated;
private static int _monitorxSoftwareFallbackAnnounced;
private static long _monitorxInstructionsEmulated;
private unsafe bool TryRecoverAmdCompatInstruction(void* contextRecord, ulong rip)
{
if (TryRecoverMonitorxMwaitx(contextRecord, rip))
{
return true;
}
// EXTRQ and INSERTQ need the current vector register values.
// Use recovery only when the signal context contains these values.
return (OperatingSystem.IsWindows() || _posixXmmContextBridged) &&
TryRecoverSse4aExtractInsert(contextRecord, rip);
}
private unsafe bool TryRecoverMonitorxMwaitx(void* contextRecord, ulong rip)
{
// MONITORX (0F 01 FA) and MWAITX (0F 01 FB) are fixed 3-byte encodings with no
// ModRM/SIB/displacement/immediate, so a raw byte compare is sufficient and unambiguous.
var opcode = new byte[3];
if (!TryReadExecutableBytes(rip, opcode) ||
opcode[0] != 0x0F || opcode[1] != 0x01 || (opcode[2] != 0xFA && opcode[2] != 0xFB))
{
return false;
}
// PS5 titles use this pair in idle/wait loops: MONITORX arms a monitor on a cache line
// and MWAITX blocks until that line is written (or a timeout elapses). Hosts without
// the extension raise #UD on either one. We do not model the monitor itself, only its
// observable effect on guest forward progress: MONITORX becomes a no-op (arming a
// watch we never honour has no side effect of its own) and MWAITX becomes a plain
// thread yield, i.e. treat the awaited condition as already satisfied so the guest
// loop keeps making progress instead of executing an illegal opcode forever.
if (opcode[2] == 0xFB)
{
Thread.Yield();
}
WriteCtxU64(contextRecord, CTX_RIP, rip + 3);
Interlocked.Increment(ref _monitorxInstructionsEmulated);
if (Interlocked.Exchange(ref _monitorxSoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks AMD MONITORX/MWAITX used by the guest; " +
"emulating those instructions in software.");
}
return true;
}
private unsafe bool TryRecoverSse4aExtractInsert(void* contextRecord, ulong rip)
{
if (!OperatingSystem.IsWindows() && !_posixXmmContextBridged ||
!TryReadFaultingInstruction(rip, out var instruction))
{
return false;
}
var isExtrq = instruction.Mnemonic == Mnemonic.Extrq;
var isInsertq = instruction.Mnemonic == Mnemonic.Insertq;
if (!isExtrq && !isInsertq)
{
return false;
}
var isImmediateExtrq = isExtrq && instruction.OpCount == 3;
var isRegisterExtrq = isExtrq && instruction.OpCount == 2;
var isImmediateInsertq = isInsertq && instruction.OpCount == 4;
if (!isImmediateExtrq && !isRegisterExtrq && !isImmediateInsertq)
{
return false;
}
if (instruction.GetOpKind(0) != OpKind.Register ||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset))
{
return false;
}
var destLow = ReadCtxU64(contextRecord, destOffset);
if (isExtrq)
{
int length;
int index;
if (isRegisterExtrq)
{
if (instruction.GetOpKind(1) != OpKind.Register ||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var controlOffset))
{
return false;
}
// AMD defines the register form's field length in xmm2[5:0] and its start
// index in xmm2[13:8]. Other control bits do not affect the instruction.
var control = ReadCtxU64(contextRecord, controlOffset);
length = (int)(control & 0x3F);
index = (int)((control >> 8) & 0x3F);
}
else
{
length = (int)instruction.GetImmediate(1);
index = (int)instruction.GetImmediate(2);
}
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.ExtractBitField(destLow, length, index));
WriteCtxU64(contextRecord, destOffset + 8, 0);
}
else
{
if (instruction.GetOpKind(1) != OpKind.Register ||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var srcOffset))
{
return false;
}
var length = (int)instruction.GetImmediate(2);
var index = (int)instruction.GetImmediate(3);
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.InsertBitField(
destLow, ReadCtxU64(contextRecord, srcOffset), length, index));
WriteCtxU64(contextRecord, destOffset + 8, 0);
}
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
Interlocked.Increment(ref _sse4aInstructionsEmulated);
if (Interlocked.Exchange(ref _sse4aSoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks SSE4a EXTRQ/INSERTQ used by the guest; " +
"emulating those instructions in software.");
}
return true;
}
// Maps an Iced XMM register to its byte offset in the Win64 CONTEXT record. Written as an
// explicit switch (rather than arithmetic on the Register enum) to match the style already
// used by TryGetGprSlot/TryGetGpr64Offset in DirectExecutionBackend.IllegalInstruction.cs.
private static bool TryGetXmmOffset(Register register, out int offset)
{
switch (register)
{
case Register.XMM0: offset = Win64ContextXmm0Offset + 16 * 0; return true;
case Register.XMM1: offset = Win64ContextXmm0Offset + 16 * 1; return true;
case Register.XMM2: offset = Win64ContextXmm0Offset + 16 * 2; return true;
case Register.XMM3: offset = Win64ContextXmm0Offset + 16 * 3; return true;
case Register.XMM4: offset = Win64ContextXmm0Offset + 16 * 4; return true;
case Register.XMM5: offset = Win64ContextXmm0Offset + 16 * 5; return true;
case Register.XMM6: offset = Win64ContextXmm0Offset + 16 * 6; return true;
case Register.XMM7: offset = Win64ContextXmm0Offset + 16 * 7; return true;
case Register.XMM8: offset = Win64ContextXmm0Offset + 16 * 8; return true;
case Register.XMM9: offset = Win64ContextXmm0Offset + 16 * 9; return true;
case Register.XMM10: offset = Win64ContextXmm0Offset + 16 * 10; return true;
case Register.XMM11: offset = Win64ContextXmm0Offset + 16 * 11; return true;
case Register.XMM12: offset = Win64ContextXmm0Offset + 16 * 12; return true;
case Register.XMM13: offset = Win64ContextXmm0Offset + 16 * 13; return true;
case Register.XMM14: offset = Win64ContextXmm0Offset + 16 * 14; return true;
case Register.XMM15: offset = Win64ContextXmm0Offset + 16 * 15; return true;
default:
offset = 0;
return false;
}
}
}

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.HLE;
using SharpEmu.Logging;
@ -16,6 +17,129 @@ 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 sealed class PerfHleExportCost
{
public long Calls;
public long Ticks;
}
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, PerfHleExportCost> _perfHleCosts = new();
/// <summary>
/// Name of the export currently being dispatched on this thread, so the
/// gateway can attribute its elapsed time once the call returns. Answering
/// "which export is worth optimising" needs cost per export, not just call
/// counts — a rare expensive call and a hot cheap one look identical in a
/// frequency histogram.
/// </summary>
[System.ThreadStatic]
private static string? _perfHleCurrentExport;
private static long _perfHleFirstTimestamp;
private static void RecordPerfHleDispatchTime(long ticks)
{
var total = System.Threading.Interlocked.Add(ref _perfHleDispatchTicks, ticks);
var calls = System.Threading.Interlocked.Read(ref _perfHleTotal);
var name = _perfHleCurrentExport;
if (name is not null)
{
var cost = _perfHleCosts.GetOrAdd(name, static _ => new PerfHleExportCost());
System.Threading.Interlocked.Increment(ref cost.Calls);
System.Threading.Interlocked.Add(ref cost.Ticks, ticks);
}
if (calls > 0 && calls % 500000 == 0)
{
var frequency = (double)System.Diagnostics.Stopwatch.Frequency;
var avgUs = (double)total / frequency * 1_000_000.0 / calls;
var first = System.Threading.Interlocked.CompareExchange(ref _perfHleFirstTimestamp, 0, 0);
var wallSeconds = first == 0
? 0
: (double)(System.Diagnostics.Stopwatch.GetTimestamp() - first) / frequency;
System.Console.Error.WriteLine(
$"[PERF][HLE] managed_dispatch_avg={avgUs:F3}us " +
$"total_managed_s={(double)total / frequency:F2} " +
$"wall_s={wallSeconds:F2} " +
$"cores={(wallSeconds > 0 ? total / frequency / wallSeconds : 0):F2}");
var snapshot = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<string, PerfHleExportCost>>(_perfHleCosts.Count + 16);
foreach (var kvp in _perfHleCosts)
{
snapshot.Add(kvp);
}
var top = snapshot
.OrderByDescending(kvp => System.Threading.Interlocked.Read(ref kvp.Value.Ticks))
.Take(12)
.Select(kvp =>
{
var seconds = System.Threading.Interlocked.Read(ref kvp.Value.Ticks) / frequency;
var callCount = System.Threading.Interlocked.Read(ref kvp.Value.Calls);
var cores = wallSeconds > 0 ? seconds / wallSeconds : 0;
var perCallUs = callCount > 0 ? seconds * 1_000_000.0 / callCount : 0;
return $"{kvp.Key}: {cores:F2}cores {seconds:F1}s n={callCount} {perCallUs:F2}us/call";
});
System.Console.Error.WriteLine($"[PERF][HLE] cost: {string.Join(" | ", top)}");
if (SharpEmu.HLE.GuestFastPath.Enabled)
{
var fastPath = SharpEmu.HLE.GuestFastPath.SnapshotCounters();
System.Console.Error.WriteLine(
$"[PERF][HLE] fast_path: self_hits={fastPath.SelfHits} " +
$"getspecific_hits={fastPath.GetspecificHits} blocks={fastPath.Blocks}");
}
}
}
private static readonly bool _perfHleNoDict =
string.Equals(System.Environment.GetEnvironmentVariable("SHARPEMU_PERF_HLE_NODICT"), "1", System.StringComparison.Ordinal);
private static void RecordPerfHleCall(string name)
{
_perfHleCurrentExport = name;
var total = System.Threading.Interlocked.Increment(ref _perfHleTotal);
if (total == 1)
{
System.Threading.Interlocked.CompareExchange(
ref _perfHleFirstTimestamp,
System.Diagnostics.Stopwatch.GetTimestamp(),
0);
}
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,
@ -24,101 +148,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}");
}
}
@ -184,6 +247,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))
{
@ -235,15 +342,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(
@ -270,14 +376,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(
@ -301,6 +407,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);
@ -343,6 +471,29 @@ public sealed partial class DirectExecutionBackend
return false;
}
// The committed readable run of host pages that contains address.
private unsafe static bool TryQueryReadableRange(ulong address, out ulong start, out ulong end)
{
start = 0;
end = 0;
if (address <= 65536 || address >= 140737488355328L ||
VirtualQuery((void*)address, out var info, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
var regionEnd = info.BaseAddress + info.RegionSize;
if (regionEnd < info.BaseAddress || address > regionEnd - 8 ||
info.State != 4096 || !IsReadableProtection(info.Protect))
{
return false;
}
start = info.BaseAddress;
end = regionEnd;
return true;
}
private unsafe static bool TryReadStackU64(ulong address, out ulong value)
{
value = 0uL;

View File

@ -0,0 +1,286 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
/// <summary>
/// Native fast-path stubs for the trivial per-thread imports that dominate the
/// HLE call count. Each stub answers from <see cref="GuestFastPath"/>'s
/// per-host-thread block and jumps to the normal import trampoline whenever it
/// cannot, so the managed implementation stays the single source of truth for
/// every case the stub does not handle.
/// </summary>
public sealed unsafe partial class DirectExecutionBackend
{
internal enum GuestFastPathStub
{
None,
PthreadSelf,
PthreadGetspecific,
MemoryCopy,
}
// scePthreadSelf / pthread_self.
private const string PthreadSelfNid = "aI+OeCz8xrQ";
private const string PosixPthreadSelfNid = "EotR8a3ASf4";
// scePthreadGetspecific / pthread_getspecific.
private const string PthreadGetspecificNid = "eoht7mQOCmo";
private const string PosixPthreadGetspecificNid = "0-KXaS70xy4";
// memcpy / memmove.
private const string MemcpyNid = "Q3VBxCXhUHs";
private const string MemmoveNid = "+P6FRGH4LfA";
// Guest memory is identity-mapped and every fault the copy can take (write tracking, lazy
// commit) is resolved from the fault address alone, so the copy may run as guest-side code.
// SHARPEMU_HLE_FAST_MEMCPY=0 keeps it on the managed export.
private static readonly bool GuestMemoryCopyStubEnabled =
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_HLE_FAST_MEMCPY"), "0", StringComparison.Ordinal);
private uint _fastPathBlockTlsIndex = uint.MaxValue;
private bool _guestFastPathEnabled;
private int _guestFastPathStubCount;
/// <summary>
/// Decides whether native fast-path stubs may be installed for this run and
/// arms <see cref="GuestFastPath"/> if so. Any diagnostic mode that must
/// observe every import call keeps the fast path off, because a stub answers
/// without ever reaching the managed gateway that does the tracing.
/// </summary>
private void ConfigureGuestFastPath()
{
_guestFastPathEnabled = false;
_guestFastPathStubCount = 0;
// Emission uses the macOS pthread gs-indexed key array. Windows and
// Linux keep the managed gateway until their TLS read is emitted too.
if (!OperatingSystem.IsMacOS() ||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
return;
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_HLE_FAST_PATH"), "0", StringComparison.Ordinal))
{
Console.Error.WriteLine("[LOADER][INFO] HLE native fast path disabled by SHARPEMU_HLE_FAST_PATH=0.");
return;
}
if (_logAllImports ||
!string.IsNullOrEmpty(_importFilter) ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
"[LOADER][INFO] HLE native fast path disabled: import/pthread tracing needs every call in the gateway.");
return;
}
if (_fastPathBlockTlsIndex == uint.MaxValue)
{
_fastPathBlockTlsIndex = TlsAlloc();
if (_fastPathBlockTlsIndex == uint.MaxValue)
{
return;
}
}
var tlsIndex = _fastPathBlockTlsIndex;
GuestFastPath.Enable(block => TlsSetValue(tlsIndex, block));
_guestFastPathEnabled = true;
}
private static GuestFastPathStub ClassifyGuestFastPathStub(string nid) => nid switch
{
PthreadSelfNid or PosixPthreadSelfNid => GuestFastPathStub.PthreadSelf,
PthreadGetspecificNid or PosixPthreadGetspecificNid => GuestFastPathStub.PthreadGetspecific,
MemcpyNid or MemmoveNid => GuestFastPathStub.MemoryCopy,
_ => GuestFastPathStub.None,
};
/// <summary>
/// Builds the native stub for <paramref name="nid"/>, falling back to
/// <paramref name="fallbackTrampoline"/> (the import's managed gateway
/// trampoline) for every case the stub declines.
/// </summary>
private unsafe bool TryCreateGuestFastPathStub(string nid, nint fallbackTrampoline, out nint address)
{
address = 0;
if (!_guestFastPathEnabled || fallbackTrampoline == 0)
{
return false;
}
var kind = ClassifyGuestFastPathStub(nid);
if (kind == GuestFastPathStub.None ||
// The write watch inspects every managed guest write, which a native copy never reaches.
(kind == GuestFastPathStub.MemoryCopy && (!GuestMemoryCopyStubEnabled || GuestWriteWatch.Armed)))
{
return false;
}
var code = EmitGuestFastPathStub(kind, _fastPathBlockTlsIndex, fallbackTrampoline);
const uint stubAllocationSize = 128u;
if (code.Count > stubAllocationSize)
{
return false;
}
void* memory = VirtualAlloc(null, stubAllocationSize, 12288u, 64u);
if (memory == null)
{
return false;
}
for (var i = 0; i < code.Count; i++)
{
((byte*)memory)[i] = code[i];
}
uint oldProtect = 0;
if (!VirtualProtect(memory, stubAllocationSize, 32u, &oldProtect))
{
VirtualFree(memory, 0u, 32768u);
return false;
}
FlushInstructionCache(GetCurrentProcess(), memory, (nuint)code.Count);
address = (nint)memory;
_importHandlerTrampolines.Add(address);
_guestFastPathStubCount++;
return true;
}
/// <summary>
/// Emits one fast-path stub. The stubs obey the SysV ABI the guest calls
/// them with: the answer goes in RAX, only RAX and the arithmetic flags are
/// clobbered (both caller-saved across a call), and on the slow path every
/// register - including the incoming RAX that carries the variadic vector
/// count - is restored before jumping to the trampoline.
/// </summary>
internal static List<byte> EmitGuestFastPathStub(
GuestFastPathStub kind,
uint blockTlsIndex,
nint fallbackTrampoline)
{
if (kind == GuestFastPathStub.MemoryCopy)
{
return EmitGuestMemoryCopyStub(fallbackTrampoline);
}
var code = new List<byte>(48);
var slowPathFixups = new List<int>();
void Emit(params byte[] bytes) => code.AddRange(bytes);
// Emits a short conditional branch to the slow path; 0x74 is JE, 0x73 is JAE.
void EmitBranchToSlowPath(byte opcode)
{
Emit(opcode, 0x00); // rel8, patched once the slow-path offset is known
slowPathFixups.Add(code.Count - 1);
}
void EmitJumpToSlowPath() => EmitBranchToSlowPath(0x74);
// push rax: the guest's incoming RAX/AL must survive a slow-path bail.
Emit(0x50);
// mov rax, gs:[blockTlsIndex * 8] - the macOS pthread key array is the
// gs-based thread specific data array, so a key read is a single load.
Emit(0x65, 0x48, 0x8B, 0x04, 0x25);
code.AddRange(BitConverter.GetBytes(checked(blockTlsIndex * 8u)));
// test rax, rax / je slow: no block published on this host thread.
Emit(0x48, 0x85, 0xC0);
EmitJumpToSlowPath();
switch (kind)
{
case GuestFastPathStub.PthreadSelf:
// cmp qword [rax + SelfHandleOffset], 0 / je slow: no guest
// thread bound here, and a host thread's pthread_self has to
// reach the managed export for its scheduler registration.
Emit(0x48, 0x83, 0x78, checked((byte)GuestFastPath.BlockSelfHandleOffset), 0x00);
EmitJumpToSlowPath();
// inc qword [rax + SelfHitsOffset]
Emit(0x48, 0xFF, 0x40, checked((byte)GuestFastPath.BlockSelfHitsOffset));
// mov rax, [rax + SelfHandleOffset]
Emit(0x48, 0x8B, 0x40, checked((byte)GuestFastPath.BlockSelfHandleOffset));
break;
case GuestFastPathStub.PthreadGetspecific:
// cmp qword [rax + TlsValuesOffset], 0 / je slow: this thread
// has no value table published.
Emit(0x48, 0x83, 0x78, checked((byte)GuestFastPath.BlockTlsValuesOffset), 0x00);
EmitJumpToSlowPath();
// cmp rdi, TlsSlotCount / jae slow. An out-of-range or
// sign-extended negative key is answered by the managed path,
// which truncates it to int and consults the key registry.
Emit(0x48, 0x81, 0xFF);
code.AddRange(BitConverter.GetBytes(GuestFastPath.TlsSlotCount));
EmitBranchToSlowPath(0x73);
// inc qword [rax + GetspecificHitsOffset]
Emit(0x48, 0xFF, 0x40, checked((byte)GuestFastPath.BlockGetspecificHitsOffset));
// mov rax, [rax + TlsValuesOffset] / mov rax, [rax + rdi*8]
Emit(0x48, 0x8B, 0x40, checked((byte)GuestFastPath.BlockTlsValuesOffset));
Emit(0x48, 0x8B, 0x04, 0xF8);
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown fast-path stub kind.");
}
// add rsp, 8 (drop the saved RAX - the result replaces it) / ret
Emit(0x48, 0x83, 0xC4, 0x08);
Emit(0xC3);
var slowPathOffset = code.Count;
// pop rax / jmp qword [rip+0] - the absolute target follows the
// instruction, so nothing else is clobbered on the way out.
Emit(0x58);
Emit(0xFF, 0x25, 0x00, 0x00, 0x00, 0x00);
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
foreach (var fixup in slowPathFixups)
{
var delta = slowPathOffset - (fixup + 1);
if (delta is < 0 or > 127)
{
throw new InvalidOperationException("Fast-path stub slow-path branch is out of rel8 range.");
}
code[fixup] = (byte)delta;
}
return code;
}
/// <summary>
/// memcpy/memmove(dst, src, n) as a forward <c>rep movsb</c>, returning dst. The libc
/// export copies with memmove semantics, so a destination that starts inside the source
/// (where a forward copy would read bytes it already overwrote) takes the managed path.
/// Only RAX, RCX, RSI, RDI and the flags change - all caller-saved - and the direction
/// flag is clear on entry per the SysV ABI.
/// </summary>
internal static List<byte> EmitGuestMemoryCopyStub(nint fallbackTrampoline)
{
var code = new List<byte>(40);
// mov rcx, rdi / sub rcx, rsi / cmp rcx, rdx / jb slow: dst - src < n (unsigned) is
// exactly a destination in (src, src + n) or equal to src with a non-empty copy.
code.AddRange([0x48, 0x89, 0xF9, 0x48, 0x29, 0xF1, 0x48, 0x39, 0xD1, 0x72, 0x00]);
var slowPathFixup = code.Count - 1;
// mov rax, rdi / mov rcx, rdx / rep movsb / ret
code.AddRange([0x48, 0x89, 0xF8, 0x48, 0x89, 0xD1, 0xF3, 0xA4, 0xC3]);
code[slowPathFixup] = checked((byte)(code.Count - (slowPathFixup + 1)));
// jmp qword [rip+0] with the absolute trampoline address after it.
code.AddRange([0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
return code;
}
}

View File

@ -0,0 +1,387 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
/// <summary>
/// Sampling profiler for guest code. Managed profilers only see the emulator's
/// own frames — once a guest thread is running translated code it is opaque to
/// them, so a title that burns its cores inside its own spin loops looks like
/// unattributed native time. This walks the guest thread registry and samples
/// each thread's host RIP, which lands directly on the guest instruction being
/// executed.
/// </summary>
public sealed partial class DirectExecutionBackend
{
private static readonly bool _profileGuestRip =
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP"),
"1",
StringComparison.Ordinal);
private static readonly int _profileGuestRipIntervalMs =
int.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP_INTERVAL_MS"),
out var interval) && interval > 0
? interval
: 2;
private static readonly int _profileGuestRipReportSeconds =
int.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP_REPORT_S"),
out var report) && report > 0
? report
: 15;
private static readonly string? _profileGuestRipThreadFilter =
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP_THREAD");
private const ulong GuestImageBase = 0x0000_0008_0000_0000UL;
private const ulong GuestImageLimit = 0x0000_0009_0000_0000UL;
private int _guestRipSamplerStarted;
private readonly ConcurrentDictionary<ulong, long> _guestRipSamples = new();
private readonly ConcurrentDictionary<string, long> _guestRipThreadSamples = new();
private readonly ConcurrentDictionary<string, long> _guestWaitSamples = new();
private readonly ConcurrentDictionary<string, long> _guestThreadWaitSamples = new();
private readonly ConcurrentDictionary<string, long> _guestThreadWaitReasonSamples = new();
private long _guestRipTotalSamples;
private long _guestWaitTotalSamples;
private long _guestRipCaptureFailures;
private long _guestRipSamplerErrors;
private readonly record struct GuestRipSampleTarget(
string Name,
CpuContext Context,
int HostThreadId,
string? BlockReason);
/// <summary>
/// Names the HLE call a thread is parked in, using the guest RIP the import
/// dispatcher left on its context.
/// </summary>
private string ResolveWaitLabel(CpuContext context, string? blockReason)
{
var importIndex = context.ActiveImportIndex;
if ((uint)importIndex >= (uint)_importEntries.Length)
{
// Host code with no import in flight: the thread is parked by the
// emulator's own scheduler. The cooperative block records why, which
// is the part that actually identifies what the frame is waiting on.
return string.IsNullOrEmpty(blockReason)
? "<idle-or-scheduler>"
: $"blocked:{blockReason}";
}
var entry = _importEntries[importIndex];
return entry.Export?.Name ?? entry.Nid;
}
internal void ClearActiveImportIndex()
{
if (!_profileGuestRip)
{
return;
}
var context = ActiveCpuContext;
if (context is not null)
{
context.ActiveImportIndex = -1;
}
}
private void EnsureGuestRipSampler()
{
if (!_profileGuestRip ||
!OperatingSystem.IsWindows() ||
Interlocked.Exchange(ref _guestRipSamplerStarted, 1) != 0)
{
return;
}
var sampler = new Thread(GuestRipSampleLoop)
{
IsBackground = true,
Name = "SharpEmu guest RIP sampler",
// Sampling suspends guest threads briefly. Keep this diagnostic below
// the title workers so it observes them without becoming the bottleneck.
Priority = ThreadPriority.BelowNormal,
};
sampler.Start();
Console.Error.WriteLine(
$"[PERF][GUEST] RIP sampler started: interval={_profileGuestRipIntervalMs}ms " +
$"report={_profileGuestRipReportSeconds}s " +
$"thread={_profileGuestRipThreadFilter ?? "<all>"}");
}
private void GuestRipSampleLoop()
{
var clock = Stopwatch.StartNew();
var lastReportMs = 0L;
var lastReportSamples = 0L;
while (true)
{
try
{
var guestThreads = SnapshotGuestRipTargets();
if (!string.IsNullOrWhiteSpace(_profileGuestRipThreadFilter))
{
guestThreads = guestThreads
.Where(thread => thread.Name.Contains(
_profileGuestRipThreadFilter,
StringComparison.OrdinalIgnoreCase))
.ToArray();
}
var sampledHostThreads = new HashSet<int>();
foreach (var thread in guestThreads)
{
var hostThreadId = thread.HostThreadId;
if (hostThreadId == 0 || !sampledHostThreads.Add(hostThreadId))
{
continue;
}
if (!TryCaptureHostThreadContext(hostThreadId, out var snapshot) ||
!snapshot.IsValid)
{
Interlocked.Increment(ref _guestRipCaptureFailures);
continue;
}
_guestRipSamples.AddOrUpdate(snapshot.Rip, 1, static (_, value) => value + 1);
_guestRipThreadSamples.AddOrUpdate(
string.IsNullOrEmpty(thread.Name) ? "<unnamed>" : thread.Name,
1,
static (_, value) => value + 1);
Interlocked.Increment(ref _guestRipTotalSamples);
// A host RIP means the thread is inside the emulator rather
// than running translated code. DispatchImport parks the
// guest RIP on the import stub for the call being serviced,
// so the stub address names what the thread is waiting on —
// no hot-path bookkeeping needed to find out.
if (snapshot.Rip >= GuestImageBase && snapshot.Rip < GuestImageLimit)
{
continue;
}
var threadName = string.IsNullOrEmpty(thread.Name) ? "<unnamed>" : thread.Name;
var waitLabel = ResolveWaitLabel(thread.Context, thread.BlockReason);
_guestWaitSamples.AddOrUpdate(
waitLabel,
1,
static (_, value) => value + 1);
_guestThreadWaitSamples.AddOrUpdate(
threadName,
1,
static (_, value) => value + 1);
_guestThreadWaitReasonSamples.AddOrUpdate(
$"{threadName}\u001F{waitLabel}",
1,
static (_, value) => value + 1);
Interlocked.Increment(ref _guestWaitTotalSamples);
}
Thread.Sleep(_profileGuestRipIntervalMs);
var elapsedMs = clock.ElapsedMilliseconds;
if (elapsedMs - lastReportMs < _profileGuestRipReportSeconds * 1000L)
{
continue;
}
var samples = Interlocked.Read(ref _guestRipTotalSamples);
ReportGuestRipSamples(samples - lastReportSamples, (elapsedMs - lastReportMs) / 1000.0);
lastReportMs = elapsedMs;
lastReportSamples = samples;
}
catch (Exception exception)
{
// A title can tear down a thread or its context during a capture.
// The profiler must never silently die or affect guest execution.
if (Interlocked.Increment(ref _guestRipSamplerErrors) == 1)
{
Console.Error.WriteLine($"[PERF][GUEST] sampler recovery: {exception.GetType().Name}: {exception.Message}");
}
}
}
}
private GuestRipSampleTarget[] SnapshotGuestRipTargets()
{
using (LockGate("SnapshotGuestRipTargets"))
{
var targets = new GuestRipSampleTarget[_guestThreads.Count + _externalGuestThreads.Count];
var index = 0;
foreach (var thread in _guestThreads.Values)
{
targets[index++] = new GuestRipSampleTarget(
thread.Name,
thread.Context,
Volatile.Read(ref thread.HostThreadId),
thread.BlockReason);
}
foreach (var pair in _externalGuestThreads)
{
var thread = pair.Value;
targets[index++] = new GuestRipSampleTarget(
thread.Name,
thread.Context,
Volatile.Read(ref thread.HostThreadId),
null);
}
return targets;
}
}
private void ReportGuestRipSamples(long windowSamples, double windowSeconds)
{
if (windowSamples == 0)
{
return;
}
var byRip = new List<KeyValuePair<ulong, long>>(_guestRipSamples.Count + 16);
foreach (var pair in _guestRipSamples)
{
byRip.Add(pair);
}
// A tight spin lands on a handful of instructions; grouping by 4 KB page
// as well shows which routine those instructions belong to.
var byPage = new Dictionary<ulong, long>();
foreach (var pair in byRip)
{
var page = pair.Key & ~0xFFFUL;
byPage[page] = byPage.TryGetValue(page, out var existing)
? existing + pair.Value
: pair.Value;
}
var byThread = new List<KeyValuePair<string, long>>(_guestRipThreadSamples.Count + 16);
foreach (var pair in _guestRipThreadSamples)
{
byThread.Add(pair);
}
Console.Error.WriteLine(
$"[PERF][GUEST] window={windowSamples} in {windowSeconds:F1}s " +
$"capture_failures={Interlocked.Read(ref _guestRipCaptureFailures)}");
Console.Error.WriteLine(
"[PERF][GUEST] top_rip: " +
string.Join(
" | ",
byRip.OrderByDescending(pair => pair.Value)
.Take(12)
.Select(pair =>
$"0x{pair.Key:X}{DescribeGuestAddress(pair.Key)}={pair.Value * 100.0 / windowSamples:F1}%")));
Console.Error.WriteLine(
"[PERF][GUEST] top_page: " +
string.Join(
" | ",
byPage.OrderByDescending(pair => pair.Value)
.Take(8)
.Select(pair =>
$"0x{pair.Key:X}{DescribeGuestAddress(pair.Key)}={pair.Value * 100.0 / windowSamples:F1}%")));
var byWait = new List<KeyValuePair<string, long>>(_guestWaitSamples.Count + 16);
foreach (var pair in _guestWaitSamples)
{
byWait.Add(pair);
}
var waitTotal = Interlocked.Read(ref _guestWaitTotalSamples);
Console.Error.WriteLine(
$"[PERF][GUEST] waiting={waitTotal * 100.0 / windowSamples:F1}% of guest thread-time; top_wait: " +
string.Join(
" | ",
byWait.OrderByDescending(pair => pair.Value)
.Take(12)
.Select(pair => $"{pair.Key}={pair.Value * 100.0 / windowSamples:F1}%")));
Console.Error.WriteLine(
"[PERF][GUEST] thread_wait: " +
string.Join(
" | ",
_guestThreadWaitReasonSamples.OrderByDescending(pair => pair.Value)
.Take(16)
.Select(pair =>
{
var separator = pair.Key.IndexOf('\u001F');
var threadName = separator >= 0 ? pair.Key[..separator] : pair.Key;
var waitLabel = separator >= 0 ? pair.Key[(separator + 1)..] : "<unknown>";
return $"{threadName}->{waitLabel}={pair.Value * 100.0 / windowSamples:F1}%";
})));
// Per-thread spin/park split. The global wait share mixes the job pool in
// with a dozen dormant threads, which hides the number that matters:
// how much of a core each worker actually burns.
Console.Error.WriteLine(
"[PERF][GUEST] thread_split (running/parked): " +
string.Join(
" | ",
byThread.OrderByDescending(pair => pair.Value)
.Take(10)
.Select(pair =>
{
var parked = _guestThreadWaitSamples.TryGetValue(pair.Key, out var wait) ? wait : 0;
var running = pair.Value - parked;
return $"{pair.Key}={running * 100.0 / pair.Value:F0}%/{parked * 100.0 / pair.Value:F0}%";
})));
Console.Error.WriteLine(
"[PERF][GUEST] top_thread: " +
string.Join(
" | ",
byThread.OrderByDescending(pair => pair.Value)
.Take(10)
.Select(pair => $"{pair.Key}={pair.Value * 100.0 / windowSamples:F1}%")));
_guestRipSamples.Clear();
_guestRipThreadSamples.Clear();
_guestWaitSamples.Clear();
_guestThreadWaitSamples.Clear();
_guestThreadWaitReasonSamples.Clear();
Interlocked.Exchange(ref _guestWaitTotalSamples, 0);
}
/// <summary>
/// Tags a sampled address with the region it belongs to. Guest module code
/// lives above the image base; anything else is emulator or system code that
/// the managed profiler already covers.
/// </summary>
private string DescribeGuestAddress(ulong address)
{
if (address >= GuestImageBase && address < GuestImageLimit)
{
return $"(app+0x{address - GuestImageBase:X})";
}
for (var index = 0; index < _importEntries.Length; index++)
{
if (_importEntries[index].Address == (address & ~0xFUL))
{
return $"(stub:{_importEntries[index].Nid})";
}
}
return "(host)";
}
}

View File

@ -0,0 +1,309 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Diagnostics;
namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe partial class DirectExecutionBackend
{
private sealed class GuestSchedulerThreadStats
{
public string Name = string.Empty;
public long ReadyTimestamp;
public long BlockTimestamp;
public long ScheduledTimestamp;
public long RunTimestamp;
public long ReadyCount;
public long ClaimCount;
public long ScheduleCount;
public long RunCount;
public long BlockCount;
public long ResumeCount;
public long ReadyTicks;
public long BlockTicks;
public long ScheduleTicks;
public long RunTicks;
public long MaxReadyTicks;
public long MaxBlockTicks;
public long MaxScheduleTicks;
public long MaxRunTicks;
public string? LastBlockReason;
public int Completed;
}
private static readonly bool _profileGuestScheduler =
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_SCHEDULER"),
"1",
StringComparison.Ordinal) ||
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE"),
"1",
StringComparison.Ordinal);
private readonly ConcurrentDictionary<ulong, GuestSchedulerThreadStats> _guestSchedulerStats = new();
private readonly GuestThreadFlowProfile _guestThreadFlowProfile = new();
private readonly object _guestTraceOutputGate = new();
private long _guestSchedulerProfileWindowStart = Stopwatch.GetTimestamp();
private long _guestSchedulerProfileNextReport =
Stopwatch.GetTimestamp() + (Stopwatch.Frequency * 5L);
private int _guestSchedulerProfileReporting;
private void StartGuestFlowTraces()
{
lock (_guestTraceOutputGate)
{
_guestThreadFlowProfile.StartSession();
SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.StartSession();
SharpEmu.Libs.Diagnostics.MutexHandoffProfile.StartSession();
SharpEmu.Libs.VideoOut.SubmissionFlowProfile.StartSession();
}
}
private void WriteGuestFlowTraces(TextWriter? output = null)
{
output ??= Console.Error;
lock (_guestTraceOutputGate)
{
var signals = SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.Close();
var mutexes = SharpEmu.Libs.Diagnostics.MutexHandoffProfile.Close();
SharpEmu.Libs.VideoOut.SubmissionFlowProfile.WriteTrace(output);
try
{
_guestThreadFlowProfile.WriteTrace(output);
SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.WriteTrace(output, signals);
SharpEmu.Libs.Diagnostics.MutexHandoffProfile.WriteTrace(output, mutexes);
}
catch (Exception exception) when (exception is IOException or ObjectDisposedException)
{
// A closed trace output must not prevent native session cleanup.
}
}
}
private void RecordGuestThreadFlow(GuestThreadState thread, GuestThreadFlowProfile.EventKind kind, string? reason = null)
{
if (!GuestThreadFlowProfile.Enabled) return;
_guestThreadFlowProfile.Record(new GuestThreadFlowProfile.TraceEvent(
Stopwatch.GetTimestamp(), Environment.CurrentManagedThreadId, thread.ThreadHandle,
thread.Name, kind, reason, thread.BlockWakeKey, Volatile.Read(ref thread.LastImportNid),
Volatile.Read(ref thread.LastImportRdi), Volatile.Read(ref thread.LastImportRsi), thread.BlockDeadlineTimestamp));
}
private GuestSchedulerThreadStats GetGuestSchedulerStats(GuestThreadState thread)
{
var stats = _guestSchedulerStats.GetOrAdd(
thread.ThreadHandle,
static _ => new GuestSchedulerThreadStats());
stats.Name = thread.Name;
return stats;
}
private void ProfileGuestThreadReady(GuestThreadState thread)
{
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Ready);
if (!_profileGuestScheduler)
{
return;
}
var stats = GetGuestSchedulerStats(thread);
var now = Stopwatch.GetTimestamp();
Volatile.Write(ref stats.ReadyTimestamp, now);
Interlocked.Increment(ref stats.ReadyCount);
var blockTimestamp = Interlocked.Exchange(ref stats.BlockTimestamp, 0);
if (blockTimestamp != 0 && now >= blockTimestamp)
{
var ticks = now - blockTimestamp;
Interlocked.Increment(ref stats.ResumeCount);
Interlocked.Add(ref stats.BlockTicks, ticks);
UpdateGuestSchedulerMaximum(ref stats.MaxBlockTicks, ticks);
}
}
private void ProfileGuestThreadClaimed(GuestThreadState thread)
{
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Claimed);
if (!_profileGuestScheduler)
{
return;
}
var now = Stopwatch.GetTimestamp();
var stats = GetGuestSchedulerStats(thread);
var readyTimestamp = Interlocked.Exchange(ref stats.ReadyTimestamp, 0);
Interlocked.Increment(ref stats.ClaimCount);
if (readyTimestamp != 0 && now >= readyTimestamp)
{
var ticks = now - readyTimestamp;
Interlocked.Add(ref stats.ReadyTicks, ticks);
UpdateGuestSchedulerMaximum(ref stats.MaxReadyTicks, ticks);
}
}
private void ProfileGuestThreadScheduled(GuestThreadState thread)
{
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Scheduled);
if (!_profileGuestScheduler)
{
return;
}
var stats = GetGuestSchedulerStats(thread);
Volatile.Write(ref stats.ScheduledTimestamp, Stopwatch.GetTimestamp());
Interlocked.Increment(ref stats.ScheduleCount);
}
private void ProfileGuestThreadRunStarted(GuestThreadState thread)
{
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.RunStarted);
if (!_profileGuestScheduler)
{
return;
}
var now = Stopwatch.GetTimestamp();
var stats = GetGuestSchedulerStats(thread);
var scheduledTimestamp = Interlocked.Exchange(ref stats.ScheduledTimestamp, 0);
Volatile.Write(ref stats.RunTimestamp, now);
Interlocked.Increment(ref stats.RunCount);
if (scheduledTimestamp != 0 && now >= scheduledTimestamp)
{
var ticks = now - scheduledTimestamp;
Interlocked.Add(ref stats.ScheduleTicks, ticks);
UpdateGuestSchedulerMaximum(ref stats.MaxScheduleTicks, ticks);
}
}
private void ProfileGuestThreadRunStopped(GuestThreadState thread)
{
if (!_profileGuestScheduler)
{
return;
}
var now = Stopwatch.GetTimestamp();
var stats = GetGuestSchedulerStats(thread);
var runTimestamp = Interlocked.Exchange(ref stats.RunTimestamp, 0);
if (runTimestamp != 0 && now >= runTimestamp)
{
var ticks = now - runTimestamp;
Interlocked.Add(ref stats.RunTicks, ticks);
UpdateGuestSchedulerMaximum(ref stats.MaxRunTicks, ticks);
}
if (thread.State == GuestThreadRunState.Blocked)
{
Interlocked.Increment(ref stats.BlockCount);
Volatile.Write(ref stats.BlockTimestamp, now);
Volatile.Write(ref stats.LastBlockReason, thread.BlockReason);
}
else if (thread.State is GuestThreadRunState.Exited or GuestThreadRunState.Faulted)
{
Volatile.Write(ref stats.Completed, 1);
}
TryReportGuestSchedulerProfile(now);
}
private void TryReportGuestSchedulerProfile(long now)
{
var deadline = Volatile.Read(ref _guestSchedulerProfileNextReport);
if (now < deadline ||
Interlocked.CompareExchange(ref _guestSchedulerProfileReporting, 1, 0) != 0)
{
return;
}
try
{
Volatile.Write(
ref _guestSchedulerProfileNextReport,
now + (Stopwatch.Frequency * 5L));
var windowStart = Interlocked.Exchange(ref _guestSchedulerProfileWindowStart, now);
var seconds = Math.Max(
(now - windowStart) / (double)Stopwatch.Frequency,
double.Epsilon);
var rows = new List<(long Rank, string Text)>();
foreach (var pair in _guestSchedulerStats)
{
var stats = pair.Value;
var readyCount = Interlocked.Exchange(ref stats.ReadyCount, 0);
var claimCount = Interlocked.Exchange(ref stats.ClaimCount, 0);
var scheduleCount = Interlocked.Exchange(ref stats.ScheduleCount, 0);
var runCount = Interlocked.Exchange(ref stats.RunCount, 0);
var blockCount = Interlocked.Exchange(ref stats.BlockCount, 0);
var resumeCount = Interlocked.Exchange(ref stats.ResumeCount, 0);
var readyTicks = Interlocked.Exchange(ref stats.ReadyTicks, 0);
var blockTicks = Interlocked.Exchange(ref stats.BlockTicks, 0);
var scheduleTicks = Interlocked.Exchange(ref stats.ScheduleTicks, 0);
var runTicks = Interlocked.Exchange(ref stats.RunTicks, 0);
var maxReadyTicks = Interlocked.Exchange(ref stats.MaxReadyTicks, 0);
var maxBlockTicks = Interlocked.Exchange(ref stats.MaxBlockTicks, 0);
var maxScheduleTicks = Interlocked.Exchange(ref stats.MaxScheduleTicks, 0);
var maxRunTicks = Interlocked.Exchange(ref stats.MaxRunTicks, 0);
if (readyCount == 0 && claimCount == 0 && scheduleCount == 0 && runCount == 0)
{
continue;
}
rows.Add((Math.Max(blockTicks, Math.Max(readyTicks, runTicks)),
$"name='{stats.Name}' handle=0x{pair.Key:X16} " +
$"ready={readyCount} claimed={claimCount} scheduled={scheduleCount} " +
$"runs={runCount} blocked={blockCount} resumed={resumeCount} " +
$"blocked_ms={GuestSchedulerMilliseconds(blockTicks):F2} " +
$"blocked_avg_ms={(resumeCount == 0 ? 0.0 : GuestSchedulerMilliseconds(blockTicks) / resumeCount):F3} " +
$"blocked_max_ms={GuestSchedulerMilliseconds(maxBlockTicks):F3} " +
$"ready_ms={GuestSchedulerMilliseconds(readyTicks):F2} " +
$"ready_avg_ms={(claimCount == 0 ? 0.0 : GuestSchedulerMilliseconds(readyTicks) / claimCount):F3} " +
$"ready_max_ms={GuestSchedulerMilliseconds(maxReadyTicks):F3} " +
$"dispatch_ms={GuestSchedulerMilliseconds(scheduleTicks):F2} " +
$"dispatch_avg_ms={(runCount == 0 ? 0.0 : GuestSchedulerMilliseconds(scheduleTicks) / runCount):F3} " +
$"dispatch_max_ms={GuestSchedulerMilliseconds(maxScheduleTicks):F3} " +
$"run_ms={GuestSchedulerMilliseconds(runTicks):F2} " +
$"run_avg_ms={(runCount == 0 ? 0.0 : GuestSchedulerMilliseconds(runTicks) / runCount):F3} " +
$"run_max_ms={GuestSchedulerMilliseconds(maxRunTicks):F3} " +
$"last_block={Volatile.Read(ref stats.LastBlockReason) ?? "none"}"));
if (Volatile.Read(ref stats.Completed) != 0)
{
_guestSchedulerStats.TryRemove(pair.Key, out _);
}
}
Console.Error.WriteLine(
$"[PERF][GUEST_SCHED] window={seconds:F1}s active_threads={rows.Count}");
foreach (var row in rows
.OrderByDescending(static item => item.Rank)
.Take(32))
{
Console.Error.WriteLine($"[PERF][GUEST_SCHED] {row.Text}");
}
}
finally
{
Volatile.Write(ref _guestSchedulerProfileReporting, 0);
}
}
private static double GuestSchedulerMilliseconds(long ticks) =>
ticks * 1000.0 / Stopwatch.Frequency;
private static void UpdateGuestSchedulerMaximum(ref long target, long candidate)
{
var current = Volatile.Read(ref target);
while (candidate > current)
{
var observed = Interlocked.CompareExchange(ref target, candidate, current);
if (observed == current)
{
return;
}
current = observed;
}
}
}

View File

@ -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 (!TryReadExecutableBytes(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

View File

@ -0,0 +1,51 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Runtime.InteropServices;
namespace SharpEmu.Core.Cpu.Native;
// On macOS the runtime receives EXC_BAD_ACCESS on each of its threads through a Mach exception
// port before the kernel raises SIGSEGV or SIGBUS. It calls any fault in the page holding RSP, or
// the page below it, a stack overflow and aborts, assuming RSP is on a stack it owns. Guest code
// runs on guest stacks, and those pages fault whenever GPU or image write tracking has protected
// them, so a thread running guest code must leave its access faults to the signal handler. The
// handler resolves guest faults and chains every other one to the runtime's own signal handler,
// the same path the runtime uses on Linux.
public sealed unsafe partial class DirectExecutionBackend
{
private const uint ExceptionMaskBadAccess = 1u << 1;
private const int ExceptionDefault = 1;
private const int ThreadStateNone = 13;
[ThreadStatic]
private static bool _guestFaultsUseSignals;
private static int _machExceptionPortWarning;
[DllImport("libSystem.dylib", EntryPoint = "mach_thread_self")]
private static extern uint MachThreadSelf();
[DllImport("libSystem.dylib", EntryPoint = "thread_set_exception_ports")]
private static extern int ThreadSetExceptionPorts(uint thread, uint exceptionMask, uint newPort, int behavior, int newFlavor);
[DllImport("libSystem.dylib", EntryPoint = "mach_port_deallocate")]
private static extern int MachPortDeallocate(uint task, uint name);
private static readonly uint _machTaskSelf = OperatingSystem.IsMacOS()
? (uint)Marshal.ReadInt32(NativeLibrary.GetExport(NativeLibrary.Load("libSystem.dylib"), "mach_task_self_"))
: 0;
private static void RouteGuestAccessFaultsToSignals()
{
if (_guestFaultsUseSignals || !OperatingSystem.IsMacOS())
return;
_guestFaultsUseSignals = true;
var thread = MachThreadSelf();
var result = ThreadSetExceptionPorts(thread, ExceptionMaskBadAccess, 0, ExceptionDefault, ThreadStateNone);
_ = MachPortDeallocate(_machTaskSelf, thread);
if (result != 0 && System.Threading.Interlocked.Exchange(ref _machExceptionPortWarning, 1) == 0)
Console.Error.WriteLine($"[LOADER][WARN] Cannot hand guest access faults to the signal handler: thread_set_exception_ports={result}.");
}
}

View File

@ -29,9 +29,29 @@ public sealed partial class DirectExecutionBackend
private static readonly bool NativeGuestWorkersDisabled =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
// Cap concurrent native-worker Runs. Astro's tbb_thead burst overlaps many
// UnmanagedCallersOnly prologues; a large prewarm + unbounded concurrency
// FailFasts (0xC0000409) mid-storm with no VEH breadcrumb. Pool size and
// in-flight Runs are separate knobs.
private static readonly int NativeWorkerMaxConcurrent = ReadNativeWorkerMaxConcurrent();
private static int ReadNativeWorkerMaxConcurrent()
{
if (int.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT"),
out var parsed) &&
parsed > 0)
{
return Math.Clamp(parsed, 1, 64);
}
return 2;
}
private readonly object _nativeWorkerGate = new();
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
private readonly SemaphoreSlim _nativeWorkerRunLimiter = new(NativeWorkerMaxConcurrent);
private bool _nativeWorkersDisposed;
private int _nativeWorkerCreationFailedLogged;
@ -49,6 +69,9 @@ public sealed partial class DirectExecutionBackend
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool TerminateThread(nint hThread, uint dwExitCode);
// 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.
@ -56,43 +79,154 @@ public sealed partial class DirectExecutionBackend
// Callers set the Active* thread-statics before emitting the stub and read the
// yield/forced-exit flags right after this returns, so the worker outcome is
// copied back into this thread's statics before returning.
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
private unsafe ulong RunGuestEntryStub(void* entryStub, ulong hostRspSlot, bool requireNativeWorker = false)
{
var worker = RentNativeGuestExecutor();
if (worker is null)
{
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
return CallNativeEntry(entryStub);
}
// Limit in-flight native Runs before renting so the idle pool is not
// drained by threads blocked on the concurrency gate.
_nativeWorkerRunLimiter.Wait();
NativeGuestExecutor? worker = null;
try
{
var state = _activeGuestThreadState;
var nativeReturn = worker.Run(
_activeCpuContext!,
state,
GuestThreadExecution.CurrentGuestThreadHandle,
_activeEntryReturnSentinelRip,
_activeGuestReturnSlotAddress,
(nint)hostRspSlot,
(nint)entryStub,
state?.AffinityMask ?? 0,
out var yieldRequested,
out var yieldReason,
out var forcedExit);
_activeGuestThreadYieldRequested = yieldRequested;
_activeGuestThreadYieldReason = yieldReason;
_activeForcedGuestExit = forcedExit;
return nativeReturn;
// Astro can spawn a burst of tbb_thead while workers are still in
// TerminateThread+respawn. Wait for a native worker — never fall back
// to managed inline (FailFast) and never throw (uncaught throw mid-
// storm was a silent process die).
var maxAttempts = requireNativeWorker ? 500 : 48;
for (var attempt = 0; attempt < maxAttempts; attempt++)
{
worker = RentNativeGuestExecutor();
if (worker is not null)
{
break;
}
if (!requireNativeWorker)
{
break;
}
Thread.Sleep(attempt < 32 ? 1 : 4);
}
if (worker is null)
{
if (requireNativeWorker)
{
var n = Interlocked.Increment(ref _tbbNativeWorkerRefuseCount);
if (n <= 8 || n % 32 == 0)
{
Console.Error.WriteLine(
$"[LOADER][ERROR] tbb_native_worker unavailable #{n} after {maxAttempts} attempts; " +
"skipping run (no managed inline, no throw)");
Console.Error.Flush();
}
_activeGuestThreadYieldRequested = true;
_activeGuestThreadYieldReason = "tbb_native_worker_unavailable";
_activeForcedGuestExit = true;
return 0x80020012UL;
}
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
return CallNativeEntry(entryStub);
}
try
{
var state = _activeGuestThreadState;
if (LogThreadMode && state is { Name: "tbb_thead" })
{
var n = Interlocked.Increment(ref _tbbNativeRunEnterCount);
if (n <= 12 || n % 64 == 0)
{
Console.Error.WriteLine(
$"[LOADER][INFO] tbb_run_enter #{n} native_tid_pending handle=0x{state.ThreadHandle:X16} " +
$"max_concurrent={NativeWorkerMaxConcurrent}");
Console.Error.Flush();
}
}
var nativeReturn = worker.Run(
_activeCpuContext!,
state,
GuestThreadExecution.CurrentGuestThreadHandle,
_activeEntryReturnSentinelRip,
_activeGuestReturnSlotAddress,
(nint)hostRspSlot,
(nint)entryStub,
state?.AffinityMask ?? 0,
out var yieldRequested,
out var yieldReason,
out var forcedExit);
_activeGuestThreadYieldRequested = yieldRequested;
_activeGuestThreadYieldReason = yieldReason;
_activeForcedGuestExit = forcedExit;
return nativeReturn;
}
finally
{
ReturnNativeGuestExecutor(worker);
}
}
finally
{
ReturnNativeGuestExecutor(worker);
_nativeWorkerRunLimiter.Release();
}
}
private static int _tbbNativeRunEnterCount;
private static int _tbbNativeWorkerRefuseCount;
internal static int _tbbWorkerPrologueFaultCount;
private void PrewarmNativeGuestWorkers(int count)
{
if (!OperatingSystem.IsWindows() || NativeGuestWorkersDisabled || count <= 0)
{
return;
}
var warmed = new List<NativeGuestExecutor>(count);
for (var i = 0; i < count; i++)
{
var worker = NativeGuestExecutor.TryCreate(this);
if (worker is null)
{
break;
}
warmed.Add(worker);
}
lock (_nativeWorkerGate)
{
if (_nativeWorkersDisposed)
{
foreach (var worker in warmed)
{
worker.Dispose();
}
return;
}
foreach (var worker in warmed)
{
_allNativeWorkers.Add(worker);
_idleNativeWorkers.Push(worker);
}
}
Console.Error.WriteLine(
$"[LOADER][INFO] Native guest workers prewarmed: {warmed.Count}/{count} " +
$"max_concurrent={NativeWorkerMaxConcurrent}");
Console.Error.Flush();
}
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;
}
@ -168,8 +302,8 @@ public sealed partial class DirectExecutionBackend
// loop: WaitForSingleObject(work);
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
// if (rax != 0) rax = rax(); // guest entry stub — zero managed frames below
// RunEpilogue(self, rax); // managed: captures outcome, restores ambient
// SetEvent(done); goto loop;
//
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
@ -185,8 +319,20 @@ public sealed partial class DirectExecutionBackend
private static nint _exitThreadAddress;
private readonly DirectExecutionBackend _backend;
private readonly AutoResetEvent _workAvailable = new(false);
private readonly AutoResetEvent _workCompleted = new(false);
// Windows uses AutoResetEvent (its SafeWaitHandle is a real kernel
// event the emitted loop can wait on); POSIX uses worker-event
// semaphores shared the same way via PosixHostStubs.
private readonly AutoResetEvent? _workAvailable;
private readonly AutoResetEvent? _workCompleted;
private nint _workSemaphore;
private nint _doneSemaphore;
// RunPrologue/RunEpilogue compile to the host ABI (SysV on POSIX); the
// emitted loop calls them with Win64 registers, so POSIX routes the
// calls through register-shuffling thunks (shared by all workers).
private static nint _posixPrologueThunk;
private static nint _posixEpilogueThunk;
private static readonly object PosixThunkGate = new();
private GCHandle _selfHandle;
private void* _controlBlock;
private void* _loopStub;
@ -202,7 +348,7 @@ public sealed partial class DirectExecutionBackend
private nint _runHostRspSlot;
private nint _runEntryStub;
private ulong _runAffinityMask;
private int _runNativeResult;
private ulong _runNativeResult;
private bool _runYieldRequested;
private string? _runYieldReason;
private bool _runForcedExit;
@ -225,6 +371,11 @@ public sealed partial class DirectExecutionBackend
private NativeGuestExecutor(DirectExecutionBackend backend)
{
_backend = backend;
if (OperatingSystem.IsWindows())
{
_workAvailable = new AutoResetEvent(false);
_workCompleted = new AutoResetEvent(false);
}
}
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
@ -274,10 +425,36 @@ public sealed partial class DirectExecutionBackend
}
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
var epiloguePtr = (nint)(delegate* unmanaged<nint, ulong, void>)&RunEpilogue;
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
nint workHandle;
nint doneHandle;
if (OperatingSystem.IsWindows())
{
workHandle = _workAvailable!.SafeWaitHandle.DangerousGetHandle();
doneHandle = _workCompleted!.SafeWaitHandle.DangerousGetHandle();
}
else
{
lock (PosixThunkGate)
{
if (_posixPrologueThunk == 0)
{
_posixPrologueThunk = PosixHostStubs.CreateWin64ToSysVThunk(prologuePtr);
_posixEpilogueThunk = PosixHostStubs.CreateWin64ToSysVThunk(epiloguePtr);
}
}
prologuePtr = _posixPrologueThunk;
epiloguePtr = _posixEpilogueThunk;
_workSemaphore = PosixHostStubs.CreateWorkerEvent();
_doneSemaphore = PosixHostStubs.CreateWorkerEvent();
if (_workSemaphore == 0 || _doneSemaphore == 0)
{
return false;
}
workHandle = _workSemaphore;
doneHandle = _doneSemaphore;
}
byte* code = (byte*)_loopStub;
int offset = 0;
@ -327,9 +504,9 @@ public sealed partial class DirectExecutionBackend
Emit(0x0F); Emit(0x84); // je skipEntry
int skipJump = offset;
offset += sizeof(int);
EmitCallRax(); // guest entry stub -> eax
EmitCallRax(); // guest entry stub -> rax
int skipEntryOffset = offset;
Emit(0x89); Emit(0xC2); // mov edx, eax
Emit(0x48); Emit(0x89); Emit(0xC2); // mov rdx, rax
EmitMovRcxImm64((ulong)executorHandle);
EmitMovRaxImm64((ulong)epiloguePtr);
EmitCallRax();
@ -354,6 +531,22 @@ public sealed partial class DirectExecutionBackend
return false;
}
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
return StartWorkerThread();
}
private bool RestartWorkerThread()
{
if (_loopStub == null || _controlBlock == null)
{
return false;
}
*(int*)_controlBlock = 0;
return StartWorkerThread();
}
private bool StartWorkerThread()
{
_threadHandle = CreateThread(
0,
WorkerStackReservation,
@ -372,7 +565,7 @@ public sealed partial class DirectExecutionBackend
return true;
}
public int Run(
public ulong Run(
CpuContext context,
GuestThreadState? state,
ulong guestThreadHandle,
@ -397,8 +590,51 @@ public sealed partial class DirectExecutionBackend
_runYieldRequested = false;
_runYieldReason = null;
_runForcedExit = false;
_workAvailable.Set();
_workCompleted.WaitOne();
SignalWorkAvailable();
WaitWorkCompleted();
// Normal path: RunEpilogue/ExitRun clears _entered before SetEvent(done).
// TBB abort stub SetEvent's without ExitRun — _entered stays true.
if (_entered)
{
var waitRc = WaitForSingleObject(_threadHandle, 500u);
Console.Error.WriteLine(
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} aborted during run; " +
$"wait_rc=0x{waitRc:X8} respawning");
Console.Error.Flush();
if (_runState is { } abortedState)
{
_ = GuestThreadExecution.NotifyGuestThreadAbandoned(
abortedState.ThreadHandle,
"tbb_worker_abort");
Volatile.Write(ref abortedState.HostThreadId, _prevHostThreadId);
}
_entered = false;
if (_threadHandle != 0)
{
// Abort stub parks (no ExitThread). Force-kill the parked OS
// thread so we can recreate the loop without process teardown.
if (waitRc != 0u)
{
_ = TerminateThread(_threadHandle, unchecked((uint)(-1)));
_ = WaitForSingleObject(_threadHandle, 1000u);
}
CloseHandle(_threadHandle);
_threadHandle = 0;
_nativeThreadId = 0;
}
if (!RestartWorkerThread())
{
_runPrologueFailed = true;
}
else
{
_runPrologueFailed = false;
_runForcedExit = true;
_runNativeResult = 0;
}
}
_runContext = null;
_runState = null;
yieldRequested = _runYieldRequested;
@ -406,11 +642,48 @@ public sealed partial class DirectExecutionBackend
forcedExit = _runForcedExit;
if (_runPrologueFailed)
{
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
// Never throw out of the native-worker rent path: an uncaught
// exception mid-TBB storm kills the process with no FailFast
// breadcrumb.
var n = Interlocked.Increment(ref _tbbWorkerPrologueFaultCount);
if (n <= 8 || n % 32 == 0)
{
Console.Error.WriteLine(
$"[LOADER][ERROR] tbb_worker prologue fault #{n}; soft-fail run " +
$"(tid={_nativeThreadId})");
Console.Error.Flush();
}
yieldRequested = true;
yieldReason = "tbb_worker_prologue_fault";
forcedExit = true;
return 0x80020012UL;
}
return _runNativeResult;
}
private void SignalWorkAvailable()
{
if (_workAvailable is not null)
{
_workAvailable.Set();
return;
}
_ = PosixHostStubs.SignalWorkerEvent(_workSemaphore);
}
private void WaitWorkCompleted()
{
if (_workCompleted is not null)
{
_workCompleted.WaitOne();
return;
}
_ = PosixHostStubs.WaitWorkerEvent(_doneSemaphore, -1);
}
[UnmanagedCallersOnly]
private static nint RunPrologue(nint executorHandle)
{
@ -434,7 +707,7 @@ public sealed partial class DirectExecutionBackend
}
[UnmanagedCallersOnly]
private static void RunEpilogue(nint executorHandle, int nativeResult)
private static void RunEpilogue(nint executorHandle, ulong nativeResult)
{
try
{
@ -477,7 +750,20 @@ public sealed partial class DirectExecutionBackend
_activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!);
RouteGuestAccessFaultsToSignals();
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (backend._workerDoneEventTlsIndex != uint.MaxValue)
{
nint doneHandle = OperatingSystem.IsWindows()
? _workCompleted!.SafeWaitHandle.DangerousGetHandle()
: _doneSemaphore;
TlsSetValue(backend._workerDoneEventTlsIndex, doneHandle);
}
if (backend._tbbAbortEligibleTlsIndex != uint.MaxValue)
{
nint eligible = _runState is { Name: "tbb_thead" } ? 1 : 0;
TlsSetValue(backend._tbbAbortEligibleTlsIndex, eligible);
}
if (_runState is { } state)
{
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
@ -496,7 +782,7 @@ public sealed partial class DirectExecutionBackend
return _runEntryStub;
}
private void ExitRun(int nativeResult)
private void ExitRun(ulong nativeResult)
{
_runNativeResult = nativeResult;
_runYieldRequested = _activeGuestThreadYieldRequested;
@ -512,6 +798,14 @@ public sealed partial class DirectExecutionBackend
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
}
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
if (_backend._workerDoneEventTlsIndex != uint.MaxValue)
{
TlsSetValue(_backend._workerDoneEventTlsIndex, 0);
}
if (_backend._tbbAbortEligibleTlsIndex != uint.MaxValue)
{
TlsSetValue(_backend._tbbAbortEligibleTlsIndex, 0);
}
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
_activeExecutionBackend = _prevBackend;
_activeCpuContext = _prevContext;
@ -528,7 +822,7 @@ public sealed partial class DirectExecutionBackend
if (LogThreadMode)
{
TraceThreadMode(
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X16} yield={_runYieldRequested}");
}
}
@ -540,7 +834,7 @@ public sealed partial class DirectExecutionBackend
}
try
{
_workAvailable.Set();
SignalWorkAvailable();
}
catch (ObjectDisposedException)
{
@ -575,8 +869,18 @@ public sealed partial class DirectExecutionBackend
{
_selfHandle.Free();
}
_workAvailable.Dispose();
_workCompleted.Dispose();
_workAvailable?.Dispose();
_workCompleted?.Dispose();
if (_workSemaphore != 0)
{
PosixHostStubs.DestroyWorkerEvent(_workSemaphore);
_workSemaphore = 0;
}
if (_doneSemaphore != 0)
{
PosixHostStubs.DestroyWorkerEvent(_doneSemaphore);
_doneSemaphore = 0;
}
}
}
}

View File

@ -0,0 +1,140 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Runtime.InteropServices;
using Iced.Intel;
using SharpEmu.HLE;
using static Iced.Intel.AssemblerRegisters;
namespace SharpEmu.Core.Cpu.Native;
// Guest faults are delivered on the guest stack. The runtime only knows the thread's own stack,
// so managed code that runs on the guest stack can make a garbage collection misread the frames
// of that thread. Windows swaps the stack bounds in the TEB while guest code runs; POSIX cannot,
// so the signal handler moves to the host stack below the frames that entered guest code first.
public sealed unsafe partial class DirectExecutionBackend
{
// Host stack below the saved host RSP that is still treated as the host frames in use.
private const ulong HostStackSearchBytes = 0x100_0000;
private const int SignalStackGap = 0x1000;
// [0] the host RSP TLS slot (uint.MaxValue while unknown), [8] the Win64 TlsGetValue stub.
private static nint _signalStackCells;
private static unsafe nint CreatePosixSignalTrampoline()
{
_signalStackCells = (nint)NativeMemory.AllocZeroed(16);
*(uint*)_signalStackCells = uint.MaxValue;
var a = new Assembler(64);
var direct = a.CreateLabel();
var call = a.CreateLabel();
var done = a.CreateLabel();
a.push(rbx);
a.push(r12);
a.push(r13);
a.push(r14);
a.push(r15);
a.mov(r12d, edi);
a.mov(r13, rsi);
a.mov(r14, rdx);
a.mov(rbx, rsp);
// The saved host RSP of this thread, when it entered guest code.
a.mov(rax, (ulong)_signalStackCells);
a.mov(ecx, __dword_ptr[rax]);
a.cmp(ecx, -1);
a.je(direct);
a.mov(rax, __qword_ptr[rax + 8]);
a.test(rax, rax);
a.je(direct);
a.sub(rsp, 48);
a.call(rax);
a.add(rsp, 48);
a.test(rax, rax);
a.je(direct);
a.mov(r15, __qword_ptr[rax]);
a.test(r15, r15);
a.je(direct);
// Already on the host stack below the saved RSP: an import or host code faulted.
a.cmp(rbx, r15);
a.jae(call);
a.mov(rax, r15);
a.sub(rax, rbx);
a.cmp(rax, (int)HostStackSearchBytes);
a.jb(direct);
a.Label(ref call);
a.lea(rsp, __[r15 - SignalStackGap]);
a.and(rsp, -16);
a.Label(ref direct);
a.mov(edi, r12d);
a.mov(rsi, r13);
a.mov(rdx, r14);
a.mov(rax, (ulong)(nint)(delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal);
a.call(rax);
a.Label(ref done);
a.mov(rsp, rbx);
a.pop(r15);
a.pop(r14);
a.pop(r13);
a.pop(r12);
a.pop(rbx);
a.ret();
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 the POSIX signal trampoline page");
}
var writer = new SpanCodeWriter(new Span<byte>(page, 4096));
a.Assemble(writer, (ulong)page);
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
{
throw new InvalidOperationException("Failed to protect the POSIX signal trampoline page");
}
return (nint)page;
}
private unsafe void PublishSignalStackState()
{
if (_signalStackCells == 0)
{
return;
}
*(nint*)(_signalStackCells + 8) = _tlsGetValueAddress;
*(uint*)_signalStackCells = _hostRspSlotTlsIndex;
}
private sealed unsafe class SpanCodeWriter : CodeWriter
{
private readonly byte* _start;
private readonly int _length;
private int _offset;
public SpanCodeWriter(Span<byte> destination)
{
fixed (byte* start = destination)
{
_start = start;
}
_length = destination.Length;
}
public override void WriteByte(byte value)
{
if (_offset >= _length)
{
throw new InvalidOperationException("The POSIX signal trampoline does not fit its page.");
}
_start[_offset++] = value;
}
}
}

View File

@ -0,0 +1,521 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.HLE.GpuMemory;
namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe partial class DirectExecutionBackend
{
// POSIX bridge for the Windows vectored-exception-handler logic. A
// sigaction(SIGSEGV/SIGBUS/SIGILL) handler rebuilds the EXCEPTION_POINTERS
// view the shared handlers expect (Win64 CONTEXT register offsets) from
// the signal's mcontext, runs the same recovery chain the VEH path uses
// (unresolved-import trap sentinels, demand-paging of lazily-committed
// guest pages, fault diagnostics), and writes register changes back into
// the mcontext so sigreturn resumes the repaired guest. Unrecovered
// faults are forwarded to the previously installed handler so the .NET
// 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 PosixSigFpe = 8;
private const int PosixSigSegv = 11;
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
// struct sigaction: the handler pointer leads on both platforms; Darwin
// packs { handler(8), mask(4), flags(4) }, Linux glibc/musl packs
// { handler(8), mask(128), flags(4), restorer(8) }.
private static readonly int PosixSigactionSize = OperatingSystem.IsMacOS() ? 16 : 152;
private static readonly int PosixSigactionFlagsOffset = OperatingSystem.IsMacOS() ? 12 : 136;
private static readonly int PosixSaSigInfo = OperatingSystem.IsMacOS() ? 0x0040 : 0x0004;
private static readonly int PosixSaNoDefer = OperatingSystem.IsMacOS() ? 0x0010 : 0x40000000;
// siginfo_t.si_addr: Darwin { signo, errno, code, pid, uid, status, addr },
// Linux { signo, errno, code, pad32, addr }.
private static readonly int PosixSigInfoAddressOffset = OperatingSystem.IsMacOS() ? 24 : 16;
// Darwin ucontext_t stores a pointer to __darwin_mcontext64 at +48; the
// general registers live in its __ss thread state after the 16-byte
// exception state. Linux glibc embeds mcontext_t inline at +40 with the
// registers in gregs[23]. Rosetta 2 delivers the regular x86-64 layout
// to translated processes.
private const int DarwinUcontextMcontextOffset = 48;
private const int DarwinUserContextMachineContextSizeOffset = 40;
// Darwin stores vector registers at the same offset in both signal frame formats.
// The system headers i386/_mcontext.h and mach/i386/_structs.h define this offset.
private const int DarwinMachineContextVectorRegistersOffset = 352;
private const int DarwinMcontextErrOffset = 4;
private const int DarwinMcontextFaultAddressOffset = 8;
private const int LinuxUcontextGregsOffset = 40;
private const int LinuxGregsErrOffset = 19 * 8;
// The kernel's x86-64 sigcontext places the FXSAVE-image pointer right
// after the general registers it hands to the handler: err(152)
// trapno(160) oldmask(168) cr2(176) fpstate(184), all relative to
// GetPosixRegisterBase. glibc and musl both overlay this kernel layout
// verbatim (glibc's mcontext_t.fpregs is the same slot), so the offset
// is libc-independent. Inside the FXSAVE image the XMM registers start
// at +160 (32-byte header + 8 legacy x87/MMX slots x 16 bytes) - the
// same relative position they occupy in the Win64 CONTEXT's FltSave
// area (Win64ContextXmm0Offset = 256 + 160).
private const int LinuxGregsFpstateOffset = 184;
private const int FxsaveXmmOffset = 160;
private const int XmmBlockSize = 16 * 16;
// Byte offsets of the general registers relative to GetPosixRegisterBase,
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
// against the x86-64 platform headers.
private static readonly int[] PosixRegisterOffsets = OperatingSystem.IsMacOS()
? new[] { 16, 32, 40, 24, 72, 64, 56, 48, 80, 88, 96, 104, 112, 120, 128, 136, 144 }
: new[] { 104, 112, 96, 88, 120, 80, 72, 64, 0, 8, 16, 24, 32, 40, 48, 56, 128 };
private static DirectExecutionBackend? _posixSignalBackend;
private static bool _posixSignalHandlersInstalled;
private static nint _posixSignalTrampoline;
private static bool _posixRawRecoveryEnabled;
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;
// True when the signal context contains the current vector register values.
// Copy changes back to these registers before the guest continues.
[ThreadStatic]
private static bool _posixXmmContextBridged;
private void SetupPosixExceptionHandler()
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_POSIX_SIGNALS"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine("[LOADER][WARN] POSIX signal exception bridge disabled by SHARPEMU_DISABLE_POSIX_SIGNALS=1; guest faults will not be recovered.");
return;
}
_posixSignalBackend = this;
if (_posixSignalHandlersInstalled)
{
PublishSignalStackState();
return;
}
_posixRawRecoveryEnabled = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal);
if (!_posixRawRecoveryEnabled)
{
Console.Error.WriteLine("[LOADER][INFO] Raw sentinel recovery disabled by SHARPEMU_DISABLE_RAW_HANDLER=1");
}
WarmUpPosixSignalPath();
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
_posixSignalTrampoline = CreatePosixSignalTrampoline();
PublishSignalStackState();
if (!InstallPosixSignalHandler(PosixSigSegv) ||
!InstallPosixSignalHandler(PosixSigBus) ||
!InstallPosixSignalHandler(PosixSigIll) ||
!InstallPosixSignalHandler(PosixSigTrap) ||
!InstallPosixSignalHandler(PosixSigAbort) ||
!InstallPosixSignalHandler(PosixSigFpe))
{
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
}
_posixSignalHandlersInstalled = true;
Console.Error.WriteLine("[LOADER][INFO] POSIX signal exception bridge installed (SIGSEGV/SIGBUS/SIGILL/SIGFPE)");
}
/// <summary>
/// Runs the signal-recovery path once with fabricated inputs before the
/// handlers are installed. The first entry into the handler must not
/// require JIT compilation (a fault can interrupt arbitrary runtime
/// states), and under Rosetta 2 the signal trampoline cannot enter x86
/// code that has never been executed (and therefore never translated): a
/// cold handler is silently never invoked and the faulting instruction
/// retries forever.
/// </summary>
private void WarmUpPosixSignalPath()
{
byte* testUserContext = stackalloc byte[512];
new Span<byte>(testUserContext, 512).Clear();
byte* testMachineContext = stackalloc byte[1024];
new Span<byte>(testMachineContext, 1024).Clear();
if (OperatingSystem.IsMacOS())
{
*(byte**)(testUserContext + DarwinUcontextMcontextOffset) = testMachineContext;
*(ulong*)(testUserContext + DarwinUserContextMachineContextSizeOffset) = 1024;
// Load the Mach memory functions before the first signal uses them.
byte readProbe = 0;
_ = TryReadMacOsMemory((ulong)testMachineContext, &readProbe, 1);
}
_posixSignalWarmup = true;
try
{
((delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal)(PosixSigSegv, 0, (nint)testUserContext);
// Warm the branches the fabricated fault above skips without
// spamming diagnostics: the benign-exception path through
// VectoredHandler, the lazy-commit probe (fault address 0 bails
// out immediately), and the chain helper (signal 0 has no saved
// action and sigaction(0, ...) fails with EINVAL).
EXCEPTION_RECORD record = default;
record.ExceptionCode = DBG_PRINTEXCEPTION_C;
byte* contextRecord = stackalloc byte[Win64ContextSize];
new Span<byte>(contextRecord, Win64ContextSize).Clear();
EXCEPTION_POINTERS pointers;
pointers.ExceptionRecord = &record;
pointers.ContextRecord = contextRecord;
_ = VectoredHandler(&pointers);
record.ExceptionCode = 3221225477u;
record.NumberParameters = 2;
// 0x70000 is never guest-owned, so this walks the vmem region
// scan and the PRT range check, then bails out silently.
record.ExceptionInformation[1] = 0x70000;
_ = TryHandleLazyCommittedPage(&record, 0, 0);
ChainPreviousPosixAction(0, 0, 0);
}
finally
{
_posixSignalWarmup = false;
}
}
private static bool InstallPosixSignalHandler(int signal)
{
byte* action = stackalloc byte[PosixSigactionSize];
new Span<byte>(action, PosixSigactionSize).Clear();
*(nint*)action = _posixSignalTrampoline;
// No SA_ONSTACK: the runtime's alternate stacks are far too small for
// the recovery/diagnostic path (JIT compilation of cold handler code
// can run inside the signal frame). Guest faults deliver onto the 2MB
// guest stack, host faults onto the regular thread stack — the same
// stacks Windows dispatches exceptions on.
*(int*)(action + PosixSigactionFlagsOffset) = PosixSaSigInfo | PosixSaNoDefer;
var previous = (byte*)NativeMemory.AllocZeroed((nuint)PosixSigactionSize);
if (sigaction(signal, action, previous) != 0)
{
NativeMemory.Free(previous);
Console.Error.WriteLine($"[LOADER][ERROR] sigaction({signal}) failed: errno={Marshal.GetLastPInvokeError()}");
return false;
}
_posixPreviousActions[signal] = (nint)previous;
return true;
}
[UnmanagedCallersOnly]
private static void HandlePosixSignal(int signal, nint siginfo, nint ucontext)
{
if (_posixSignalHandlerDepth > 0)
{
// A fault inside our own fault handler (diagnostics touched an
// unmapped address): restore the default action and return so the
// re-executed instruction terminates the process.
RestoreDefaultPosixAction(signal);
return;
}
_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 &&
signal != PosixSigFpe &&
siginfo != 0 &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
*(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset)))
{
return;
}
// GPU-tracked pages fault on every first CPU access after the GPU or an upload used them,
// thousands of times a second in a frame. Resolving them needs only the address and the
// access kind, so they skip the Win64 context conversion and the other handlers.
if (PosixGpuFaultFastPath && !_posixSignalWarmup &&
(signal == PosixSigSegv || signal == PosixSigBus) &&
siginfo != 0 &&
TryResolvePosixGpuFault(siginfo, ucontext))
{
return;
}
if (TryHandlePosixFault(signal, siginfo, ucontext))
{
return;
}
}
catch
{
// A managed exception must never unwind out of a signal frame.
}
finally
{
_posixSignalHandlerDepth--;
}
ChainPreviousPosixAction(signal, siginfo, ucontext);
}
private static readonly bool PosixGpuFaultFastPath =
Environment.GetEnvironmentVariable("SHARPEMU_POSIX_GPU_FAULT_FAST_PATH") != "0";
private static bool TryResolvePosixGpuFault(nint siginfo, nint ucontext)
{
byte* registers = GetPosixRegisterBase(ucontext);
if (registers == null)
{
return false;
}
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
ulong rip = *(ulong*)(registers + PosixRegisterOffsets[16]);
var kind = GetPosixAccessType(registers, faultAddress, rip) switch
{
0 => FaultKind.Read,
1 => FaultKind.Write,
_ => FaultKind.Unknown,
};
return kind != FaultKind.Unknown && GuestGpuMemoryHook.TryResolveFault(kind, faultAddress);
}
private static bool TryHandlePosixFault(int signal, nint siginfo, nint ucontext)
{
byte* registers = GetPosixRegisterBase(ucontext);
if (registers == null)
{
return false;
}
byte* contextRecord = stackalloc byte[Win64ContextSize];
new Span<byte>(contextRecord, Win64ContextSize).Clear();
int[] offsets = PosixRegisterOffsets;
for (int i = 0; i < offsets.Length; i++)
{
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
}
// Copy the vector registers that instruction recovery can change.
// Keep all other floating-point state unchanged.
byte* vectorRegisters = GetSignalVectorRegisterAddress(ucontext, registers);
if (vectorRegisters != null)
{
Buffer.MemoryCopy(
vectorRegisters,
contextRecord + Win64ContextXmm0Offset,
XmmBlockSize,
XmmBlockSize);
}
_posixXmmContextBridged = vectorRegisters != null;
EXCEPTION_RECORD record = default;
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
if (signal == PosixSigIll)
{
record.ExceptionCode = 3221225501u;
}
else if (signal == PosixSigTrap)
{
record.ExceptionCode = 2147483651u;
}
else if (signal == PosixSigAbort)
{
record.ExceptionCode = 1073741845u;
}
else if (signal == PosixSigFpe)
{
// STATUS_INTEGER_DIVIDE_BY_ZERO: #DE from div/idiv (zero divisor or quotient overflow).
record.ExceptionCode = 3221225620u;
}
else
{
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
record.ExceptionCode = 3221225477u;
record.NumberParameters = 2;
record.ExceptionInformation[0] = GetPosixAccessType(registers, faultAddress, ReadCtxU64(contextRecord, CTX_RIP));
record.ExceptionInformation[1] = faultAddress;
}
EXCEPTION_POINTERS pointers;
pointers.ExceptionRecord = &record;
pointers.ContextRecord = contextRecord;
int traceIndex = _posixSignalWarmup ? 0 : Interlocked.Increment(ref _posixSignalTraceCount);
bool traceSignal = traceIndex > 0 && (traceIndex <= 16 || traceIndex % 1024 == 0 ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_POSIX_SIGNALS"), "1", StringComparison.Ordinal));
if (traceSignal)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] posix-signal#{traceIndex}: sig={signal} rip=0x{ReadCtxU64(contextRecord, CTX_RIP):X16} " +
$"fault=0x{record.ExceptionInformation[1]:X16} access={record.ExceptionInformation[0]} rsp=0x{ReadCtxU64(contextRecord, CTX_RSP):X16}");
Console.Error.Flush();
}
// Sentinel recovery runs first: on Windows both vectored handlers see
// every fault anyway, and recovering here avoids dumping the full
// VectoredHandler diagnostics for each recoverable trap.
int disposition = 0;
if (_posixRawRecoveryEnabled)
{
disposition = TryRecoverUnresolvedSentinel(&pointers);
}
if (disposition != -1 && !_posixSignalWarmup && _posixSignalBackend is { } backend)
{
disposition = backend.VectoredHandler(&pointers);
}
if (traceSignal)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] posix-signal#{traceIndex}: recovered={disposition == -1} new_rip=0x{ReadCtxU64(contextRecord, CTX_RIP):X16}");
Console.Error.Flush();
}
if (disposition != -1 && !_posixSignalWarmup)
{
return false;
}
for (int i = 0; i < offsets.Length; i++)
{
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
}
if (vectorRegisters != null)
{
Buffer.MemoryCopy(
contextRecord + Win64ContextXmm0Offset,
vectorRegisters,
XmmBlockSize,
XmmBlockSize);
}
return true;
}
private static byte* GetSignalVectorRegisterAddress(nint userContextAddress, byte* machineContext)
{
if (OperatingSystem.IsMacOS())
{
var machineContextSize = *(ulong*)((byte*)userContextAddress + DarwinUserContextMachineContextSizeOffset);
return machineContextSize >= DarwinMachineContextVectorRegistersOffset + XmmBlockSize
? machineContext + DarwinMachineContextVectorRegistersOffset
: null;
}
if (OperatingSystem.IsLinux())
{
byte* floatingPointState = *(byte**)(machineContext + LinuxGregsFpstateOffset);
return floatingPointState != null ? floatingPointState + FxsaveXmmOffset : null;
}
return null;
}
private static byte* GetPosixRegisterBase(nint ucontext)
{
if (ucontext == 0)
{
return null;
}
if (OperatingSystem.IsMacOS())
{
return *(byte**)((byte*)ucontext + DarwinUcontextMcontextOffset);
}
return (byte*)ucontext + LinuxUcontextGregsOffset;
}
private static ulong GetPosixFaultAddress(nint siginfo, byte* registers)
{
ulong address = siginfo != 0 ? *(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset) : 0;
if (address == 0 && OperatingSystem.IsMacOS())
{
address = *(ulong*)(registers + DarwinMcontextFaultAddressOffset);
}
return address;
}
private static ulong GetPosixAccessType(byte* registers, ulong faultAddress, ulong rip)
{
// x86 page-fault error code: bit 1 = write access, bit 4 = instruction
// fetch. Fall back to comparing the fault address against RIP when
// the error code is not populated (e.g. under Rosetta 2 translation).
ulong error = OperatingSystem.IsMacOS()
? *(uint*)(registers + DarwinMcontextErrOffset)
: *(ulong*)(registers + LinuxGregsErrOffset);
if ((error & 0x10) != 0)
{
return 8;
}
if ((error & 0x2) != 0)
{
return 1;
}
return faultAddress != 0 && faultAddress == rip ? 8u : 0u;
}
private static void RestoreDefaultPosixAction(int signal)
{
byte* action = stackalloc byte[PosixSigactionSize];
new Span<byte>(action, PosixSigactionSize).Clear();
_ = sigaction(signal, action, null);
}
private static void ChainPreviousPosixAction(int signal, nint siginfo, nint ucontext)
{
byte* previous = (uint)signal < (uint)_posixPreviousActions.Length
? (byte*)_posixPreviousActions[signal]
: null;
nint handler = previous != null ? *(nint*)previous : 0;
if (handler == 0)
{
// SIG_DFL (or nothing saved): reinstate the default action and
// return, so re-executing the faulting instruction terminates the
// process with the original fault context intact.
RestoreDefaultPosixAction(signal);
return;
}
if (handler == 1)
{
// SIG_IGN
return;
}
int flags = *(int*)(previous + PosixSigactionFlagsOffset);
if ((flags & PosixSaSigInfo) != 0)
{
((delegate* unmanaged<int, nint, nint, void>)handler)(signal, siginfo, ucontext);
}
else
{
((delegate* unmanaged<int, void>)handler)(signal);
}
}
[DllImport("libc", SetLastError = true)]
private static extern int sigaction(int signum, void* act, void* oldact);
}

View File

@ -0,0 +1,107 @@
// 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;
// The PS5 CPU has the SHA extensions and titles use them without a CPUID check.
// Rosetta 2 does not translate them, so they raise #UD on Apple Silicon hosts.
public sealed partial class DirectExecutionBackend
{
private static int _shaSoftwareFallbackAnnounced;
private static long _shaInstructionsEmulated;
private unsafe bool TryRecoverShaInstruction(void* contextRecord, ulong rip)
{
if (!OperatingSystem.IsWindows() && !_posixXmmContextBridged ||
!TryReadFaultingInstruction(rip, out var instruction))
{
return false;
}
switch (instruction.Mnemonic)
{
case Mnemonic.Sha1rnds4:
case Mnemonic.Sha1nexte:
case Mnemonic.Sha1msg1:
case Mnemonic.Sha1msg2:
case Mnemonic.Sha256rnds2:
case Mnemonic.Sha256msg1:
case Mnemonic.Sha256msg2:
break;
default:
return false;
}
if (instruction.GetOpKind(0) != OpKind.Register ||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset) ||
!TryReadXmmOperand(contextRecord, in instruction, 1, out var source2))
{
return false;
}
var source1 = ReadCtxXmm(contextRecord, destOffset);
var result = instruction.Mnemonic switch
{
Mnemonic.Sha1rnds4 => ShaInstructionEmulator.Sha1Rnds4(source1, source2, instruction.Immediate8),
Mnemonic.Sha1nexte => ShaInstructionEmulator.Sha1Nexte(source1, source2),
Mnemonic.Sha1msg1 => ShaInstructionEmulator.Sha1Msg1(source1, source2),
Mnemonic.Sha1msg2 => ShaInstructionEmulator.Sha1Msg2(source1, source2),
Mnemonic.Sha256rnds2 => ShaInstructionEmulator.Sha256Rnds2(
source1, source2, ReadCtxXmm(contextRecord, Win64ContextXmm0Offset)),
Mnemonic.Sha256msg1 => ShaInstructionEmulator.Sha256Msg1(source1, source2),
_ => ShaInstructionEmulator.Sha256Msg2(source1, source2),
};
WriteCtxU64(contextRecord, destOffset, (ulong)result);
WriteCtxU64(contextRecord, destOffset + 8, (ulong)(result >> 64));
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
Interlocked.Increment(ref _shaInstructionsEmulated);
if (Interlocked.Exchange(ref _shaSoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks the SHA extensions used by the guest; " +
"emulating those instructions in software.");
}
return true;
}
private unsafe bool TryReadXmmOperand(void* contextRecord, in Instruction instruction, int operandIndex, out UInt128 value)
{
value = 0;
switch (instruction.GetOpKind(operandIndex))
{
case OpKind.Register:
if (!TryGetXmmOffset(instruction.GetOpRegister(operandIndex), out var offset))
{
return false;
}
value = ReadCtxXmm(contextRecord, offset);
return true;
case OpKind.Memory:
var buffer = new byte[16];
if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address) ||
!TryReadHostBytes(address, buffer))
{
return false;
}
value = BinaryPrimitives.ReadUInt128LittleEndian(buffer);
return true;
default:
return false;
}
}
private static unsafe UInt128 ReadCtxXmm(void* contextRecord, int offset) =>
new(ReadCtxU64(contextRecord, offset + 8), ReadCtxU64(contextRecord, offset));
}

View File

@ -0,0 +1,206 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private const int HostStackStateBytes = 3 * sizeof(ulong);
internal static unsafe bool TryGetGuestStackBounds(ulong stackPointer, out ulong bottom, out ulong top)
{
if (OperatingSystem.IsWindows() &&
QueryVirtualMemoryInformation(-1, (void*)stackPointer, 0, out var allocation,
(nuint)sizeof(StackMemoryRegionInformation), null) != 0 &&
((allocation.Flags & 1) == 0 || allocation.CommitSize == allocation.RegionSize) &&
allocation.RegionSize != 0 && allocation.RegionSize <= ulong.MaxValue - allocation.AllocationBase &&
stackPointer >= allocation.AllocationBase && stackPointer - allocation.AllocationBase < allocation.RegionSize)
{
bottom = allocation.AllocationBase;
top = bottom + allocation.RegionSize;
return true;
}
bottom = top = 0;
var queried = VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64));
if (queried == 0 ||
region.State != 0x1000 || region.RegionSize == 0 || region.RegionSize > ulong.MaxValue - region.BaseAddress)
{
return false;
}
bottom = region.BaseAddress;
top = region.BaseAddress + region.RegionSize;
return stackPointer >= bottom && stackPointer < top;
}
private static unsafe void EmitStackBound(byte* code, ref int offset, byte register, uint field, bool store)
{
EmitByte(code, ref offset, 0x65);
EmitByte(code, ref offset, (byte)(0x48 | (register >= 8 ? 4 : 0)));
EmitByte(code, ref offset, store ? (byte)0x89 : (byte)0x8B);
EmitByte(code, ref offset, (byte)(0x04 | ((register & 7) << 3)));
EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, field);
}
// Fiber switches replace the guest stack without passing through a guest-entry stub, so the
// context-transfer stub keeps the Windows TEB bounds in sync from the target state in R11
// (stack top at +160, bottom at +168). Only Windows keeps stack bounds at gs:[8]/gs:[16]: on
// macOS gs addresses the pthread TSD, where those slots hold errno's address and another key.
// Writing a fiber's bounds there nulled errno, and the next failed libc call on the thread
// crashed reading it (Demon's Souls, about 10 s into boot).
private static unsafe void EmitFiberStackBounds(byte* code, ref int offset)
{
if (!OperatingSystem.IsWindows()) return;
ReadOnlySpan<byte> loadTop = [0x4D, 0x8B, 0x93, 160, 0, 0, 0]; // mov r10, [r11+160]
foreach (var value in loadTop) EmitByte(code, ref offset, value);
EmitStackBound(code, ref offset, 10, 8, store: true);
ReadOnlySpan<byte> loadBottom = [0x4D, 0x8B, 0x93, 168, 0, 0, 0]; // mov r10, [r11+168]
foreach (var value in loadBottom) EmitByte(code, ref offset, value);
EmitStackBound(code, ref offset, 10, 16, store: true);
}
// Test hook: the bytes the fiber stack-bounds sync emits on this host.
internal static unsafe byte[] EmitFiberStackBoundsForTest()
{
var buffer = new byte[64];
var offset = 0;
fixed (byte* code = buffer)
{
EmitFiberStackBounds(code, ref offset);
}
return buffer[..offset];
}
// R10 points to the entry state. R11 is scratch; RAX remains unchanged.
private static unsafe void EmitHostStackBounds(byte* code, ref int offset, bool save)
{
if (!OperatingSystem.IsWindows()) return;
for (byte field = 8; field <= 16; field += 8)
{
if (save) EmitStackBound(code, ref offset, 11, field, store: false);
EmitByte(code, ref offset, 0x4D);
EmitByte(code, ref offset, save ? (byte)0x89 : (byte)0x8B);
EmitByte(code, ref offset, 0x5A);
EmitByte(code, ref offset, field);
if (!save) EmitStackBound(code, ref offset, 11, field, store: true);
}
}
private static unsafe void EmitGuestStackBounds(byte* code, ref int offset, ulong stackPointer)
{
if (!OperatingSystem.IsWindows()) return;
if (!TryGetGuestStackBounds(stackPointer, out var bottom, out var top))
{
throw new InvalidOperationException("Guest stack has no committed host mapping.");
}
EmitHostStackBounds(code, ref offset, save: true);
for (uint field = 8; field <= 16; field += 8)
{
EmitByte(code, ref offset, 0x49);
EmitByte(code, ref offset, 0xBB);
*(ulong*)(code + offset) = field == 8 ? top : bottom;
offset += sizeof(ulong);
EmitStackBound(code, ref offset, 11, field, store: true);
}
}
private static unsafe void EmitSavedStackBounds(byte* code, ref int offset, bool restore)
{
if (!OperatingSystem.IsWindows()) return;
EmitStackBound(code, ref offset, 14, 8, restore);
EmitStackBound(code, ref offset, 15, 16, restore);
}
private unsafe void EmitExceptionHostStackBounds(byte* code, ref int offset)
{
EmitSavedStackBounds(code, ref offset, restore: false);
ReadOnlySpan<byte> reserve = [0x48, 0x83, 0xEC, 0x28, 0xB9];
foreach (var value in reserve) EmitByte(code, ref offset, value);
EmitUInt32(code, ref offset, _hostRspSlotTlsIndex);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = _tlsGetValueAddress;
offset += sizeof(nint);
ReadOnlySpan<byte> lookup = [0xFF, 0xD0, 0x48, 0x83, 0xC4, 0x28, 0x48, 0x85, 0xC0, 0x0F, 0x84];
foreach (var value in lookup) EmitByte(code, ref offset, value);
var missingState = offset;
EmitUInt32(code, ref offset, 0);
// The state is zeroed at allocation; bounds the entry stub has not saved yet stay untouched.
ReadOnlySpan<byte> unsaved = [0x48, 0x83, 0x78, 0x08, 0x00, 0x0F, 0x84];
foreach (var value in unsaved) EmitByte(code, ref offset, value);
var missingBounds = offset;
EmitUInt32(code, ref offset, 0);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
// A host fault already has live bounds. Do not replace a grown limit with an old one.
ReadOnlySpan<byte> sameStack = [0x4C, 0x3B, 0x70, 0x08, 0x0F, 0x85];
foreach (var value in sameStack) EmitByte(code, ref offset, value);
var guestStack = offset;
EmitUInt32(code, ref offset, 0);
EmitHostStackBounds(code, ref offset, save: true);
EmitByte(code, ref offset, 0xE9);
var hostStack = offset;
EmitUInt32(code, ref offset, 0);
*(int*)(code + guestStack) = offset - guestStack - sizeof(int);
EmitHostStackBounds(code, ref offset, save: false);
*(int*)(code + hostStack) = offset - hostStack - sizeof(int);
*(int*)(code + missingState) = offset - missingState - sizeof(int);
*(int*)(code + missingBounds) = offset - missingBounds - sizeof(int);
}
[UnmanagedCallersOnly]
private static unsafe int SelectExceptionStackBounds(void* exceptionInfo, int disposition, ulong* bounds)
{
if (disposition != -1) return disposition;
var context = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
if (context == null) return 0;
var stackPointer = ReadCtxU64(context, 152);
if (stackPointer >= bounds[1] && stackPointer < bounds[0]) return disposition;
GetCurrentThreadStackLimits(out var nativeBottom, out var nativeTop);
if (stackPointer >= nativeBottom && stackPointer < nativeTop)
{
bounds[0] = nativeTop;
bounds[1] = nativeBottom;
return disposition;
}
if (!TryGetGuestStackBounds(stackPointer, out var bottom, out var top)) return 0;
bounds[0] = top;
bounds[1] = bottom;
return disposition;
}
private static unsafe void EmitExceptionStackSelection(byte* code, ref int offset)
{
// The caller reserves 72 bytes: ABI home space and two stack bounds.
ReadOnlySpan<byte> arguments = [0x4C, 0x89, 0x74, 0x24, 0x28, 0x4C, 0x89, 0x7C, 0x24, 0x30,
0x89, 0xC2, 0x4C, 0x89, 0xE9, 0x4C, 0x8D, 0x44, 0x24, 0x28, 0x48, 0xB8];
foreach (var value in arguments) EmitByte(code, ref offset, value);
*(nint*)(code + offset) = (nint)(delegate* unmanaged<void*, int, ulong*, int>)&SelectExceptionStackBounds;
offset += sizeof(nint);
ReadOnlySpan<byte> result = [0xFF, 0xD0, 0x4C, 0x8B, 0x74, 0x24, 0x28, 0x4C, 0x8B, 0x7C, 0x24, 0x30];
foreach (var value in result) EmitByte(code, ref offset, value);
}
[DllImport("kernel32.dll", ExactSpelling = true)]
private static extern void GetCurrentThreadStackLimits(out ulong lowLimit, out ulong highLimit);
[StructLayout(LayoutKind.Sequential)]
private struct StackMemoryRegionInformation
{
public ulong AllocationBase;
public uint AllocationProtect;
public uint Flags;
public ulong RegionSize;
public ulong CommitSize;
}
[DllImport("api-ms-win-core-memory-l1-1-4.dll", ExactSpelling = true)]
private static extern unsafe int QueryVirtualMemoryInformation(nint process, void* address, int informationClass,
out StackMemoryRegionInformation information, nuint informationSize, nuint* returnSize);
}

View File

@ -0,0 +1,182 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe partial class DirectExecutionBackend
{
private SafeWaitHandle? _mainExecutionThreadHandle;
// One immutable code page serves all diagnostic samples for the process lifetime.
private static readonly Lazy<ThreadContextCaptureCode> ThreadCaptureCode = new(() => new());
private sealed class ThreadContextCaptureCode : SafeHandle
{
public override bool IsInvalid => handle == 0;
public ThreadContextCaptureCode() : base(0, ownsHandle: true)
{
var kernelModule = GetModuleHandle("kernel32.dll");
var suspendAddress = GetProcAddress(kernelModule, "SuspendThread");
var contextAddress = GetProcAddress(kernelModule, "GetThreadContext");
var resumeAddress = GetProcAddress(kernelModule, "ResumeThread");
if (suspendAddress == 0 || contextAddress == 0 || resumeAddress == 0) return;
var instructions = BuildThreadContextCaptureCode(suspendAddress, contextAddress, resumeAddress);
SetHandle((nint)VirtualAlloc(null, (nuint)instructions.Length, 0x3000, 0x04));
if (IsInvalid) return;
instructions.CopyTo(new Span<byte>((void*)handle, instructions.Length));
uint previousProtection = 0;
if (!VirtualProtect((void*)handle, (nuint)instructions.Length, 0x20, &previousProtection))
{
Dispose();
return;
}
FlushInstructionCache(GetCurrentProcess(), (void*)handle, (nuint)instructions.Length);
}
protected override bool ReleaseHandle() => VirtualFree((void*)handle, 0, 0x8000);
}
private static SafeWaitHandle? OpenThreadForContext(uint threadId)
{
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64 || threadId == 0)
return null;
const uint queryLimitedInformation = 0x0800;
var threadHandle = OpenThread(ThreadGetContext | ThreadSuspendResume | queryLimitedInformation, false, threadId);
return threadHandle == 0 ? null : new SafeWaitHandle(threadHandle, ownsHandle: true);
}
private static bool TryCaptureHostThreadContext(int threadId, out HostThreadContextSnapshot snapshot)
{
using var threadHandle = OpenThreadForContext(unchecked((uint)threadId));
return TryCaptureThreadContext(threadHandle, out snapshot);
}
private static bool TryCaptureThreadContext(SafeWaitHandle? threadHandle, out HostThreadContextSnapshot snapshot)
{
snapshot = default;
if (threadHandle is null || !OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
return false;
var handleReferenced = false;
try
{
threadHandle.DangerousAddRef(ref handleReferenced);
var nativeHandle = threadHandle.DangerousGetHandle();
var threadId = GetThreadId(nativeHandle);
if (threadId == 0 || threadId == GetCurrentThreadId()) return false;
var captureCode = ThreadCaptureCode.Value;
if (captureCode.IsInvalid || captureCode.IsClosed) return false;
byte* contextStorage = stackalloc byte[Win64ContextSize + 15];
var contextRecord = (void*)(((nuint)contextStorage + 15) & ~(nuint)15);
new Span<byte>(contextRecord, Win64ContextSize).Clear();
WriteCtxU32(contextRecord, Win64ContextFlagsOffset, ContextAmd64ControlInteger);
// One native transition keeps managed work outside the suspended interval.
var captured = ((delegate* unmanaged<nint, void*, int>)captureCode.DangerousGetHandle())(nativeHandle, contextRecord);
if (captured == 0) return false;
snapshot = DecodeHostThreadContext(threadId, contextRecord);
return true;
}
catch (ObjectDisposedException)
{
return false;
}
finally
{
if (handleReferenced) threadHandle!.DangerousRelease();
}
}
private static HostThreadContextSnapshot DecodeHostThreadContext(uint threadId, void* contextRecord) => new(
threadId,
ReadCtxU64(contextRecord, 248), ReadCtxU64(contextRecord, 152), ReadCtxU64(contextRecord, 160),
ReadCtxU64(contextRecord, 120), ReadCtxU64(contextRecord, 144), ReadCtxU64(contextRecord, 128),
ReadCtxU64(contextRecord, 136), ReadCtxU64(contextRecord, 168), ReadCtxU64(contextRecord, 176),
ReadCtxU64(contextRecord, 184), ReadCtxU64(contextRecord, 192), ReadCtxU64(contextRecord, 200),
ReadCtxU64(contextRecord, 208), ReadCtxU64(contextRecord, 216), ReadCtxU64(contextRecord, 224),
ReadCtxU64(contextRecord, 232), ReadCtxU64(contextRecord, 240));
internal static byte[] BuildThreadContextCaptureCode(nint suspendAddress, nint contextAddress, nint resumeAddress)
{
var instructions = new List<byte>();
void CallAddress(nint address)
{
instructions.AddRange([0x48, 0xB8]);
instructions.AddRange(BitConverter.GetBytes((long)address));
instructions.AddRange([0xFF, 0xD0]);
}
// Preserve nonvolatile registers and reserve aligned argument space.
instructions.AddRange([0x53, 0x56, 0x57, 0x48, 0x83, 0xEC, 0x20,
0x48, 0x89, 0xCB, 0x48, 0x89, 0xD6, 0x31, 0xFF]);
CallAddress(suspendAddress);
instructions.AddRange([0x83, 0xF8, 0xFF, 0x74, 0]);
var failedSuspendJump = instructions.Count - 1;
instructions.AddRange([0x48, 0x89, 0xD9, 0x48, 0x89, 0xF2]);
CallAddress(contextAddress);
instructions.AddRange([0x89, 0xC7, 0x48, 0x89, 0xD9]);
CallAddress(resumeAddress);
instructions.AddRange([0x83, 0xF8, 0xFF, 0x75, 0x02, 0x31, 0xFF]);
var returnOffset = instructions.Count;
instructions.AddRange([0x89, 0xF8, 0x48, 0x83, 0xC4, 0x20, 0x5F, 0x5E, 0x5B, 0xC3]);
instructions[failedSuspendJump] = checked((byte)(returnOffset - failedSuspendJump - 1));
return instructions.ToArray();
}
private void LogMainThreadStallContext()
{
if (!TryCaptureThreadContext(Volatile.Read(ref _mainExecutionThreadHandle), out var context)) return;
Console.Error.WriteLine($"[LOADER][ERROR] Live hardware context (main thread {context.ThreadId}):");
Console.Error.WriteLine($"[LOADER][ERROR] RIP=0x{context.Rip:X16} RSP=0x{context.Rsp:X16} RBP=0x{context.Rbp:X16}");
Console.Error.WriteLine($"[LOADER][ERROR] RAX=0x{context.Rax:X16} RBX=0x{context.Rbx:X16} RCX=0x{context.Rcx:X16} RDX=0x{context.Rdx:X16}");
Console.Error.WriteLine($"[LOADER][ERROR] RSI=0x{context.Rsi:X16} RDI=0x{context.Rdi:X16} R8=0x{context.R8:X16} R9=0x{context.R9:X16}");
Console.Error.WriteLine($"[LOADER][ERROR] R10=0x{context.R10:X16} R11=0x{context.R11:X16} R12=0x{context.R12:X16} R13=0x{context.R13:X16} R14=0x{context.R14:X16} R15=0x{context.R15:X16}");
var modules = Array.Empty<(ulong Start, ulong Size, string Name)>();
try
{
using var process = Process.GetCurrentProcess();
modules = process.Modules.Cast<ProcessModule>().Select(module =>
(Start: unchecked((ulong)module.BaseAddress), Size: (ulong)module.ModuleMemorySize, Name: module.ModuleName)).ToArray();
}
catch (Win32Exception) { }
catch (InvalidOperationException) { }
string DescribeAddress(ulong address)
{
foreach (var module in modules)
if (address >= module.Start && address - module.Start < module.Size)
return $"{module.Name}+0x{address - module.Start:X}";
return $"0x{address:X16}";
}
Console.Error.WriteLine($"[LOADER][ERROR] RIP location: {DescribeAddress(context.Rip)}");
// The thread has resumed. These bytes are best-effort observations, not an atomic snapshot.
var instructionBytes = new byte[48];
if (context.Rip >= 32 && TryReadHostBytes(context.Rip - 32, instructionBytes))
Console.Error.WriteLine($"[LOADER][ERROR] Live bytes @[RIP-32..RIP+16]: {Convert.ToHexString(instructionBytes)}");
var currentInstruction = new byte[16];
if (TryReadHostBytes(context.Rip, currentInstruction))
Console.Error.WriteLine($"[LOADER][ERROR] Live bytes @RIP: {Convert.ToHexString(currentInstruction)}");
Console.Error.WriteLine("[LOADER][ERROR] Live stack pointer candidates:");
for (var offset = 0; offset < 256; offset += sizeof(ulong))
{
var address = context.Rsp + (ulong)offset;
if (address < context.Rsp || !TryReadDiagnosticHostQword(address, out var value)) break;
if (value != 0)
Console.Error.WriteLine($"[LOADER][ERROR] [rsp+0x{offset:X2}] = 0x{value:X16} ({DescribeAddress(value)})");
}
}
[DllImport("kernel32.dll")]
private static extern uint GetThreadId(nint threadHandle);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.Core.Cpu.Native;
internal sealed class GuestThreadFlowProfile(int capacity = 131072)
{
internal static readonly bool Enabled =
(Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE") == "1" ||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_SCHEDULER") == "1") &&
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE") == "1";
internal enum EventKind
{
Blocked, Ready, Claimed, Scheduled, RunStarted, RunStopped, CallbackResumed,
ExecutorReleased, ClaimDeferred
}
internal readonly record struct TraceEvent(long Timestamp, int HostThreadId, ulong GuestThreadHandle,
string Name, EventKind Kind, string? Reason, string? WakeKey, string? ImportNid,
ulong Argument0, ulong Argument1, long DeadlineTimestamp);
internal readonly record struct Snapshot(TraceEvent[] Events, long TotalEvents);
private readonly int _capacity = capacity > 0 ? capacity : throw new ArgumentOutOfRangeException(nameof(capacity));
private readonly object _gate = new();
private readonly object _writeGate = new();
private TraceEvent[]? _events;
private long _eventCount;
private bool _closed;
internal void StartSession()
{
lock (_writeGate)
{
lock (_gate)
{
_events = null;
_eventCount = 0;
_closed = false;
}
}
}
internal void Record(in TraceEvent traceEvent)
{
lock (_gate)
{
if (_closed) return;
_events ??= new TraceEvent[_capacity];
_events[(int)(_eventCount % _capacity)] = traceEvent;
_eventCount++;
}
}
internal Snapshot Close()
{
lock (_gate)
{
_closed = true;
var retained = (int)Math.Min(_eventCount, _capacity);
var records = new TraceEvent[retained];
var first = _eventCount - retained;
for (var index = 0; index < retained; index++)
records[index] = _events![(int)((first + index) % _capacity)];
var snapshot = new Snapshot(records, _eventCount);
_events = null;
_eventCount = 0;
return snapshot;
}
}
internal void WriteTrace(TextWriter output)
{
// A concurrent shutdown caller must wait for the first writer to finish.
lock (_writeGate)
{
WriteRetainedEvents(output);
}
}
private void WriteRetainedEvents(TextWriter output)
{
var snapshot = Close();
if (snapshot.TotalEvents == 0) return;
output.WriteLine($"[PERF][GUEST_FLOW_TRACE] frequency={Stopwatch.Frequency} retained={snapshot.Events.Length} overwritten={snapshot.TotalEvents - snapshot.Events.Length}");
for (var index = 0; index < snapshot.Events.Length; index++)
{
var item = snapshot.Events[index];
output.WriteLine($"[PERF][GUEST_FLOW] sequence={snapshot.TotalEvents - snapshot.Events.Length + index} timestamp={item.Timestamp} thread={item.HostThreadId} guest=0x{item.GuestThreadHandle:X} name='{Quote(item.Name)}' event={item.Kind} reason='{Quote(item.Reason)}' wake='{Quote(item.WakeKey)}' nid='{Quote(item.ImportNid)}' arg0=0x{item.Argument0:X} arg1=0x{item.Argument1:X} deadline={item.DeadlineTimestamp}");
}
output.Flush();
}
// Guest names must not split one trace event across multiple output lines.
private static string Quote(string? value) => (value ?? "none")
.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n");
}

View File

@ -252,7 +252,7 @@ public static unsafe class JitStubs
var pattern = TlsAccessPattern;
var end = start + length - pattern.Length;
for (var ptr = start; ptr < end; ptr++)
for (var ptr = start; ptr <= end; ptr++)
{
if (MatchesPattern(ptr, pattern))
{

View File

@ -0,0 +1,49 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native;
/// <summary>
/// Placeholder for hosts whose fault bridge is installed directly by the
/// execution backend. POSIX uses its sigaction bridge and never calls these
/// Windows-shaped registration methods.
/// </summary>
internal sealed class NullHostFaultHandling : IHostFaultHandling
{
public static NullHostFaultHandling Instance { get; } = new();
private NullHostFaultHandling()
{
}
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
{
_ = managedCallback;
_ = hostRspSwitchTlsSlot;
_ = tlsGetValueAddress;
return 0;
}
public void FreeThunk(nint thunk)
{
_ = thunk;
}
public nint AddFirstChanceHandler(nint thunk)
{
_ = thunk;
return 0;
}
public void RemoveHandler(nint handle)
{
_ = handle;
}
public void SetUnhandledFilter(nint thunk)
{
_ = thunk;
}
}

View File

@ -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 = NativeLibrary.GetExport(NativeLibrary.Load("libpthread.so.0"), "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("libpthread.so.0", 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("libpthread.so.0", 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("libpthread.so.0", 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("libpthread.so.0", 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);
}

View File

@ -194,11 +194,11 @@ public sealed unsafe class StubManager : IDisposable
_stubAddresses.Clear();
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
private static void* VirtualAlloc(void* lpAddress, nuint dwSize, AllocationType flAllocationType, MemoryProtection flProtect) =>
HostMemory.Alloc(lpAddress, dwSize, (uint)flAllocationType, (uint)flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType);
private static bool VirtualFree(void* lpAddress, nuint dwSize, FreeType dwFreeType) =>
HostMemory.Free(lpAddress, dwSize, (uint)dwFreeType);
[Flags]
private enum AllocationType : uint

View File

@ -0,0 +1,33 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Byte offsets into the Win64 CONTEXT record delivered to vectored exception
/// handlers. The handlers read/write guest registers directly at these offsets
/// (no managed CONTEXT struct exists); a future POSIX backend gets a sibling
/// class for its mcontext layout.
/// </summary>
internal static class Win64ContextOffsets
{
public const int Size = 0x4D0;
public const int Mxcsr = 52;
public const int Rax = 120;
public const int Rcx = 128;
public const int Rdx = 136;
public const int Rbx = 144;
public const int Rsp = 152;
public const int Rbp = 160;
public const int Rsi = 168;
public const int Rdi = 176;
public const int R8 = 184;
public const int R9 = 192;
public const int R10 = 200;
public const int R11 = 208;
public const int R12 = 216;
public const int R13 = 224;
public const int R14 = 232;
public const int R15 = 240;
public const int Rip = 248;
}

View File

@ -0,0 +1,26 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Windows NTSTATUS exception codes and EXCEPTION_RECORD access-type values the
/// fault handlers filter on. Values are the same numbers the handlers previously
/// compared as bare literals; only the spelling changed.
/// </summary>
internal static class WindowsFaultCodes
{
public const uint AccessViolation = 0xC0000005u; // 3221225477
public const uint Breakpoint = 0x80000003u; // 2147483651
public const uint IllegalInstruction = 0xC000001Du; // 3221225501
public const uint FastFail = 0xC0000409u; // 3221226505
public const uint StackOverflow = 0xC00000FDu;
public const uint ClrManagedException = 0xE0434352u;
public const uint DebugOutputAnsi = 0x40010006u;
public const uint DebugOutputUnicode = 0x4001000Au;
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
public const ulong AccessRead = 0;
public const ulong AccessWrite = 1;
public const ulong AccessExecute = 8;
}

View File

@ -0,0 +1,345 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.HLE.Host;
namespace SharpEmu.Core.Cpu.Native.Windows;
/// <summary>
/// Vectored-exception-handler installation and the handler pre-filter thunk.
/// The thunk is inherently Windows-shaped (TEB stack-limit reads via gs:,
/// NTSTATUS pre-filtering, Win64 calling convention) and moved here whole from
/// DirectExecutionBackend; a POSIX backend supplies a sibling built around
/// sigaction/sigaltstack instead.
/// </summary>
internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
{
private readonly IHostMemory _memory;
public WindowsFaultHandling(IHostMemory memory)
{
_memory = memory;
}
public nint CreateHandlerThunk(nint managedCallback, uint hostRspSwitchTlsSlot, nint tlsGetValueAddress)
{
const uint stubSize = 1024u;
void* ptr = (void*)_memory.Allocate(0, stubSize, HostPageProtection.ReadWriteExecute);
if (ptr == null)
{
return 0;
}
byte* code = (byte*)ptr;
int offset = 0;
// Native pre-filter: these exception codes are raised while the thread can be in
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
// code" — this is why no managed throw (even one with a catch handler) ever
// survived inside the emulator. Continue the handler search without touching
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
// returned CONTINUE_SEARCH for them.
//
// FastFail (0xC0000409) is logged from this native path only: managed VEH never
// sees it (tLT18–21 silent exits after TBB AV recovery).
ReadOnlySpan<uint> nonManagedExceptionCodes =
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow,
WindowsFaultCodes.DebugOutputAnsi, WindowsFaultCodes.DebugOutputUnicode];
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
int fastFailJumpSlot = -1;
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
EmitByte(code, ref offset, 0x74); // je pass
passJumpOffsets[i] = offset;
EmitByte(code, ref offset, 0x00);
if (nonManagedExceptionCodes[i] == WindowsFaultCodes.FastFail)
{
fastFailJumpSlot = i;
}
}
EmitByte(code, ref offset, 0xE9); // jmp mainBody rel32 (FastFail breadcrumb sits between)
var mainBodyJumpSlot = offset;
EmitUInt32(code, ref offset, 0u);
int passOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
EmitByte(code, ref offset, 0xC3); // ret
// FastFail: native stderr breadcrumb with Context.Rip (no managed entry), then CONTINUE_SEARCH.
// Keep in sync with DirectExecutionBackend.CreateExceptionHandlerTrampoline.
int fastFailPassOffset = offset;
var fastFailLogInstalled = false;
if (fastFailJumpSlot >= 0 &&
NativeLibrary.TryLoad("kernel32.dll", out var kernel32) &&
NativeLibrary.TryGetExport(kernel32, "GetStdHandle", out var getStdHandle) &&
NativeLibrary.TryGetExport(kernel32, "WriteFile", out var writeFile))
{
ReadOnlySpan<byte> msg =
"[LOADER][FATAL] VEH_PASS FastFail 0xC0000409 (native; no managed VEH) rip=0x"u8;
ReadOnlySpan<byte> hexDigits = "0123456789ABCDEF"u8;
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x41);
EmitByte(code, ref offset, 0x08); // mov rax, [rcx+8]
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x90);
EmitUInt32(code, ref offset, 0xF8u); // mov r10, [rax+0xF8]
EmitByte(code, ref offset, 0x50); // push rax
EmitByte(code, ref offset, 0x51); // push rcx
EmitByte(code, ref offset, 0x52); // push rdx
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x50); // push r8
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x51); // push r9
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x52); // push r10
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x40); // sub rsp, 0x40
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, unchecked((uint)-12));
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = getStdHandle;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89);
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x28); // mov [rsp+0x28], rax
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC1);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
var msgAbsSlot = offset;
*(nint*)(code + offset) = 0;
offset += sizeof(nint);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
EmitUInt32(code, ref offset, (uint)msg.Length);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x20);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = writeFile;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B);
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x40); // mov r10, [rsp+0x40]
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xB8);
var hexDigitsAbsSlot = offset;
*(nint*)(code + offset) = 0;
offset += sizeof(nint);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
EmitByte(code, ref offset, 0x5C); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x30); // lea r11, [rsp+0x30]
EmitByte(code, ref offset, 0xB9); EmitUInt32(code, ref offset, 16u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xD0);
int hexLoopOffset = offset;
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC1); EmitByte(code, ref offset, 0xC0);
EmitByte(code, ref offset, 0x04);
EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xE2); EmitByte(code, ref offset, 0x0F);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0xB6);
EmitByte(code, ref offset, 0x14); EmitByte(code, ref offset, 0x10);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x88); EmitByte(code, ref offset, 0x13);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC3);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xC9);
EmitByte(code, ref offset, 0x75);
EmitByte(code, ref offset, unchecked((byte)(hexLoopOffset - (offset + 1)))); // jnz rel8
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xC6); EmitByte(code, ref offset, 0x03);
EmitByte(code, ref offset, 0x0A);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8D);
EmitByte(code, ref offset, 0x54); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x30);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0xB8);
EmitUInt32(code, ref offset, 17u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8D);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x20);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x20); EmitUInt32(code, ref offset, 0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xC7);
EmitByte(code, ref offset, 0x44); EmitByte(code, ref offset, 0x24);
EmitByte(code, ref offset, 0x38); EmitUInt32(code, ref offset, 0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = writeFile;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83);
EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x40);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5A);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x59);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x58);
EmitByte(code, ref offset, 0x5A);
EmitByte(code, ref offset, 0x59);
EmitByte(code, ref offset, 0x58);
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0);
EmitByte(code, ref offset, 0xC3);
var msgOffset = offset;
for (int i = 0; i < msg.Length; i++)
{
EmitByte(code, ref offset, msg[i]);
}
var hexDigitsOffset = offset;
for (int i = 0; i < hexDigits.Length; i++)
{
EmitByte(code, ref offset, hexDigits[i]);
}
*(nint*)(code + msgAbsSlot) = (nint)ptr + msgOffset;
*(nint*)(code + hexDigitsAbsSlot) = (nint)ptr + hexDigitsOffset;
code[passJumpOffsets[fastFailJumpSlot]] =
checked((byte)(fastFailPassOffset - (passJumpOffsets[fastFailJumpSlot] + 1)));
fastFailLogInstalled = true;
}
int mainBodyOffset = offset;
*(int*)(code + mainBodyJumpSlot) = mainBodyOffset - (mainBodyJumpSlot + sizeof(int));
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
{
if (i == fastFailJumpSlot && fastFailLogInstalled)
{
continue;
}
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
}
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xCD); // mov r13, rcx
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[8]
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, 8u);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x83); // jae guestStack
int aboveStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x65); EmitByte(code, ref offset, 0x48); // mov rax, gs:[0x10]
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x04); EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, 0x10u);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x39); EmitByte(code, ref offset, 0xC4); // cmp r12, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x82); // jb guestStack
int belowStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = managedCallback;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xE9);
int hostRestoreJump = offset;
EmitUInt32(code, ref offset, 0u);
int guestStackOffset = offset;
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xB9);
EmitUInt32(code, ref offset, hostRspSwitchTlsSlot);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = tlsGetValueAddress;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xC0); // test rax, rax
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
int missingTlsJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x18); // mov r11, [rax]
EmitByte(code, ref offset, 0x4D); EmitByte(code, ref offset, 0x85); EmitByte(code, ref offset, 0xDB); // test r11, r11
EmitByte(code, ref offset, 0x0F); EmitByte(code, ref offset, 0x84);
int missingHostStackJump = offset;
EmitUInt32(code, ref offset, 0u);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xDC); // mov rsp, r11
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xEC); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE9); // mov rcx, r13
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = managedCallback;
offset += sizeof(nint);
EmitByte(code, ref offset, 0xFF); EmitByte(code, ref offset, 0xD0);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x83); EmitByte(code, ref offset, 0xC4); EmitByte(code, ref offset, 0x28);
EmitByte(code, ref offset, 0xE9);
int guestRestoreJump = offset;
EmitUInt32(code, ref offset, 0u);
int passThroughOffset = offset;
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // xor eax, eax
int restoreOffset = offset;
EmitByte(code, ref offset, 0x4C); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov rsp, r12
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5D);
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x5C);
EmitByte(code, ref offset, 0xC3);
*(int*)(code + aboveStackJump) = guestStackOffset - (aboveStackJump + sizeof(int));
*(int*)(code + belowStackJump) = guestStackOffset - (belowStackJump + sizeof(int));
*(int*)(code + hostRestoreJump) = restoreOffset - (hostRestoreJump + sizeof(int));
*(int*)(code + missingTlsJump) = passThroughOffset - (missingTlsJump + sizeof(int));
*(int*)(code + missingHostStackJump) = passThroughOffset - (missingHostStackJump + sizeof(int));
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
if (!_memory.Protect((ulong)ptr, stubSize, HostPageProtection.ReadExecute, out _))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
_ = _memory.Free((ulong)ptr);
return 0;
}
_memory.FlushInstructionCache((ulong)ptr, (ulong)offset);
return (nint)ptr;
}
public void FreeThunk(nint thunk)
{
_ = _memory.Free((ulong)thunk);
}
public nint AddFirstChanceHandler(nint thunk)
{
return (nint)AddVectoredExceptionHandler(1u, thunk);
}
public void RemoveHandler(nint handle)
{
_ = RemoveVectoredExceptionHandler((void*)handle);
}
public void SetUnhandledFilter(nint thunk)
{
_ = SetUnhandledExceptionFilter(thunk);
}
private static void EmitByte(byte* code, ref int offset, byte value)
{
code[offset++] = value;
}
private static void EmitUInt32(byte* code, ref int offset, uint value)
{
*(uint*)(code + offset) = value;
offset += sizeof(uint);
}
[LibraryImport("kernel32.dll")]
private static partial void* AddVectoredExceptionHandler(uint first, IntPtr handler);
[LibraryImport("kernel32.dll")]
private static partial uint RemoveVectoredExceptionHandler(void* handle);
[LibraryImport("kernel32.dll")]
private static partial IntPtr SetUnhandledExceptionFilter(IntPtr lpTopLevelExceptionFilter);
}

View File

@ -5,7 +5,7 @@ using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu;
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator, ICpuMemoryWrapper
{
private readonly ICpuMemory _inner;
@ -18,6 +18,8 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
public ICpuMemory Inner => _inner;
public string DescribeReadRange(ulong address, ulong size) => _inner.DescribeReadRange(address, size);
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
var result = _inner.TryRead(virtualAddress, destination);
@ -40,6 +42,20 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
return result;
}
public bool TryCompare(
ulong virtualAddress,
ReadOnlySpan<byte> expected,
out bool equal) =>
_inner.TryCompare(virtualAddress, expected, out equal);
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) =>
_inner.TryCopy(destinationAddress, sourceAddress, length);
public bool CanRead(ulong address, ulong size) => _inner.CanRead(address, size);
public bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match) =>
_inner.TryScanCString(address, needle, findLast, maxLength, out match);
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
if (_inner is IGuestMemoryAllocator allocator)
@ -50,4 +66,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
address = 0;
return false;
}
public bool TryFreeGuestMemory(ulong address)
{
return _inner is IGuestMemoryAllocator allocator && allocator.TryFreeGuestMemory(address);
}
}

View File

@ -0,0 +1,122 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection.PortableExecutable;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Core.Diagnostics;
public static partial class WindowsCrashCapture
{
internal sealed class DebuggerBreakTracker
{
private readonly Dictionary<uint, nint> _threadStarts = new();
private nint _libraryBase;
private nint _breakThreadEntry;
private nint _breakpointAddress;
internal void Observe(DebugEvent debugEvent)
{
switch (debugEvent.Kind)
{
case CreateProcessEvent:
_threadStarts[debugEvent.ThreadId] = debugEvent.ProcessThreadStartAddress;
break;
case CreateThreadEvent:
_threadStarts[debugEvent.ThreadId] = debugEvent.ThreadStartAddress;
break;
case ExitThreadEvent:
_threadStarts.Remove(debugEvent.ThreadId);
break;
case LoadLibraryEvent:
if (TryReadDebuggerExports(debugEvent.FileHandle, out var threadOffset, out var breakpointOffset))
{
_libraryBase = debugEvent.LoadedLibraryBase;
_breakThreadEntry = checked(_libraryBase + threadOffset);
_breakpointAddress = checked(_libraryBase + breakpointOffset);
}
break;
case UnloadLibraryEvent when debugEvent.UnloadedLibraryBase == _libraryBase:
_libraryBase = _breakThreadEntry = _breakpointAddress = 0;
break;
}
}
internal bool IsDebuggerBreak(DebugEvent debugEvent) =>
debugEvent.Kind == ExceptionEvent && debugEvent.FirstChance != 0 &&
debugEvent.Exception.Code == BreakpointException && _libraryBase != 0 &&
debugEvent.Exception.Address == _breakpointAddress &&
_threadStarts.TryGetValue(debugEvent.ThreadId, out var startAddress) && startAddress == _breakThreadEntry;
}
private static unsafe bool TryReadDebuggerExports(nint fileHandle, out int threadOffset, out int breakpointOffset)
{
threadOffset = breakpointOffset = 0;
if (fileHandle == 0 || fileHandle == -1)
return false;
const int pathCapacity = 512;
var pathBuffer = stackalloc char[pathCapacity];
var pathLength = GetFinalPathNameByHandle(fileHandle, pathBuffer, pathCapacity, 0);
if (pathLength == 0 || pathLength >= pathCapacity)
return false;
var path = new string(pathBuffer, 0, (int)pathLength);
var systemLibrary = @"\\?\" + Path.Combine(Environment.SystemDirectory, "ntdll.dll");
if (!string.Equals(path, systemLibrary, StringComparison.OrdinalIgnoreCase))
return false;
try
{
// Read the file supplied by the debug event, then use the target's load address.
using var handle = new SafeFileHandle(fileHandle, ownsHandle: false);
using var stream = new FileStream(handle, FileAccess.Read);
using var image = new PEReader(stream);
if (image.PEHeaders.PEHeader is not { Magic: PEMagic.PE32Plus } header)
return false;
var directory = header.ExportTableDirectory;
if (directory.Size < 40)
return false;
var exports = image.GetSectionData(directory.RelativeVirtualAddress).GetReader(0, 40);
exports.Offset = 20;
var functionCount = exports.ReadInt32();
var nameCount = exports.ReadInt32();
var functionsAddress = exports.ReadInt32();
var namesAddress = exports.ReadInt32();
var ordinalsAddress = exports.ReadInt32();
var functions = image.GetSectionData(functionsAddress).GetReader(0, checked(functionCount * 4));
var names = image.GetSectionData(namesAddress).GetReader(0, checked(nameCount * 4));
var ordinals = image.GetSectionData(ordinalsAddress).GetReader(0, checked(nameCount * 2));
for (var nameIndex = 0; nameIndex < nameCount; nameIndex++)
{
var nameBlock = image.GetSectionData(names.ReadInt32());
var nameBytes = nameBlock.GetContent(0, Math.Min(nameBlock.Length, "DbgUiRemoteBreakin".Length + 1)).AsSpan();
var terminator = nameBytes.IndexOf((byte)0);
var ordinal = ordinals.ReadUInt16();
if (ordinal >= functionCount)
return false;
if (terminator < 0)
continue;
var name = Encoding.ASCII.GetString(nameBytes[..terminator]);
if (name is not ("DbgUiRemoteBreakin" or "DbgBreakPoint"))
continue;
functions.Offset = ordinal * 4;
var functionOffset = functions.ReadInt32();
// A forwarded export is not executable code in this image.
if (functionOffset <= 0 || functionOffset >= header.SizeOfImage ||
(functionOffset >= directory.RelativeVirtualAddress &&
functionOffset - directory.RelativeVirtualAddress < directory.Size))
return false;
if (name == "DbgUiRemoteBreakin")
threadOffset = functionOffset;
else
breakpointOffset = functionOffset;
}
return threadOffset != 0 && breakpointOffset != 0;
}
catch (Exception exception) when (exception is IOException or BadImageFormatException or ArgumentException or OverflowException)
{
threadOffset = breakpointOffset = 0;
return false;
}
}
}

View File

@ -0,0 +1,161 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Core.Diagnostics;
public static partial class WindowsCrashCapture
{
private const uint ExceptionEvent = 1;
private const uint CreateThreadEvent = 2;
private const uint CreateProcessEvent = 3;
private const uint ExitThreadEvent = 4;
private const uint ExitProcessEvent = 5;
private const uint LoadLibraryEvent = 6;
private const uint UnloadLibraryEvent = 7;
private const uint BreakpointException = 0x80000003;
private const uint DebugContinue = 0x00010002;
private const uint DebugExceptionNotHandled = 0x80010001;
private const uint ProcessReadAccess = 0x0450;
private const uint ThreadContextAccess = 0x0008;
private const uint DumpFullMemory = 0x00000002;
private const uint DumpHandleData = 0x00000004;
private const uint DumpUnloadedModules = 0x00000020;
private const uint DumpMemoryInformation = 0x00000800;
private const uint DumpThreadInformation = 0x00001000;
private const uint DumpIgnoreInaccessibleMemory = 0x00020000;
private const uint FullDumpFlags = DumpFullMemory | DumpHandleData | DumpUnloadedModules |
DumpMemoryInformation | DumpThreadInformation | DumpIgnoreInaccessibleMemory;
private const uint AllX64ContextFlags = 0x0010001F;
private static unsafe void WriteDump(uint processId, DebugEvent debugEvent, string dumpPath)
{
using var process = OpenProcess(ProcessReadAccess, false, processId);
if (process.IsInvalid)
throw NativeFailure("Open the crashed process");
using var thread = OpenThread(ThreadContextAccess, false, debugEvent.ThreadId);
if (thread.IsInvalid)
throw NativeFailure("Open the faulting thread");
var context = (ThreadContext*)NativeMemory.AlignedAlloc((nuint)sizeof(ThreadContext), 16);
if (context == null)
throw new OutOfMemoryException();
try
{
NativeMemory.Clear(context, (nuint)sizeof(ThreadContext));
context->Flags = AllX64ContextFlags;
if (!GetThreadContext(thread, context))
throw NativeFailure("Read the faulting thread context");
// The exception pointers refer to this helper, not to the crashed process.
var record = debugEvent.Exception;
record.NestedRecord = 0;
var pointers = new ExceptionPointers { Record = &record, Context = context };
var exception = new DumpExceptionInformation
{
ThreadId = debugEvent.ThreadId,
Pointers = &pointers,
ClientPointers = 0,
};
using var output = new FileStream(dumpPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
if (!MiniDumpWriteDump(process, processId, output.SafeFileHandle, FullDumpFlags, &exception, 0, 0))
throw NativeFailure("Write the crash dump");
output.Flush(flushToDisk: true);
}
finally
{
NativeMemory.AlignedFree(context);
}
}
[StructLayout(LayoutKind.Explicit, Size = 176)]
internal struct DebugEvent
{
[FieldOffset(0)] public uint Kind;
[FieldOffset(4)] public uint ProcessId;
[FieldOffset(8)] public uint ThreadId;
[FieldOffset(16)] public ExceptionRecord Exception;
[FieldOffset(168)] public uint FirstChance;
[FieldOffset(16)] public nint FileHandle;
[FieldOffset(16)] public uint ExitCode;
[FieldOffset(24)] public nint LoadedLibraryBase;
[FieldOffset(16)] public nint UnloadedLibraryBase;
[FieldOffset(32)] public nint ThreadStartAddress;
[FieldOffset(64)] public nint ProcessThreadStartAddress;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct ExceptionRecord
{
public uint Code;
public uint Flags;
public nint NestedRecord;
public nint Address;
public uint ParameterCount;
public fixed ulong Parameters[15];
}
[StructLayout(LayoutKind.Explicit, Size = 1232)]
internal struct ThreadContext
{
[FieldOffset(48)] public uint Flags;
}
[StructLayout(LayoutKind.Sequential)]
private unsafe struct ExceptionPointers
{
public ExceptionRecord* Record;
public ThreadContext* Context;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal unsafe struct DumpExceptionInformation
{
public uint ThreadId;
public void* Pointers;
public int ClientPointers;
}
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DebugActiveProcess(uint processId);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DebugActiveProcessStop(uint processId);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DebugSetProcessKillOnExit([MarshalAs(UnmanagedType.Bool)] bool killOnExit);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool WaitForDebugEventEx(out DebugEvent debugEvent, uint milliseconds);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ContinueDebugEvent(uint processId, uint threadId, uint status);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial SafeProcessHandle OpenProcess(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, uint processId);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial SafeWaitHandle OpenThread(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, uint threadId);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static unsafe partial bool GetThreadContext(SafeWaitHandle thread, ThreadContext* context);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(nint handle);
[LibraryImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", SetLastError = true)]
private static unsafe partial uint GetFinalPathNameByHandle(nint file, char* path, uint length, uint flags);
[LibraryImport("dbghelp.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static unsafe partial bool MiniDumpWriteDump(SafeProcessHandle process, uint processId,
SafeFileHandle file, uint dumpType, DumpExceptionInformation* exception, nint userStream, nint callback);
}

View File

@ -0,0 +1,182 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Core.Diagnostics;
public static partial class WindowsCrashCapture
{
public const string HelperArgument = "--sharpemu-crash-helper";
private const string CaptureVariable = "SHARPEMU_CRASH_CAPTURE";
public static void StartIfEnabled(string? logFilePath)
{
if (!string.Equals(Environment.GetEnvironmentVariable(CaptureVariable), "1", StringComparison.Ordinal))
return;
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
Console.Error.WriteLine("[CRASH][WARN] Crash capture requires a Windows x64 process.");
return;
}
try
{
var logPath = string.IsNullOrWhiteSpace(logFilePath)
? Path.Combine(AppContext.BaseDirectory, "user", "logs", $"run-{DateTime.UtcNow:yyyyMMdd-HHmmss}.log")
: Path.GetFullPath(logFilePath);
var dumpPath = Path.ChangeExtension(logPath, $"crash-{Environment.ProcessId}.dmp");
Directory.CreateDirectory(Path.GetDirectoryName(dumpPath)!);
var readyEventName = $"Local\\SharpEmu-Crash-{Guid.NewGuid():N}";
using var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, readyEventName);
var executable = Environment.ProcessPath ?? throw new InvalidOperationException("The executable path is not available.");
var startInfo = new ProcessStartInfo(executable) { UseShellExecute = false, CreateNoWindow = true };
if (string.Equals(Path.GetFileNameWithoutExtension(executable), "dotnet", StringComparison.OrdinalIgnoreCase))
startInfo.ArgumentList.Add(Assembly.GetEntryAssembly()!.Location);
startInfo.ArgumentList.Add(HelperArgument);
startInfo.ArgumentList.Add(Environment.ProcessId.ToString(CultureInfo.InvariantCulture));
startInfo.ArgumentList.Add(readyEventName);
startInfo.ArgumentList.Add(dumpPath);
startInfo.Environment.Remove(CaptureVariable);
// The helper stays attached until this process exits, including runtime teardown.
using var helper = Process.Start(startInfo) ?? throw new InvalidOperationException("The crash helper did not start.");
using var exited = new ProcessExitWaitHandle(helper);
WaitForHelper(readyEvent, exited);
Console.Error.WriteLine($"[CRASH][INFO] Crash capture ready. Dump: {dumpPath}");
Console.Error.WriteLine($"[CRASH][INFO] Capture report: {dumpPath}.capture.log");
Console.Error.WriteLine($"[CRASH][INFO] Runtime: {RuntimeInformation.FrameworkDescription}. Debugging can change timing.");
}
catch (Exception exception)
{
Console.Error.WriteLine($"[CRASH][ERROR] Crash capture is not active: {exception.Message}");
}
}
internal static void WaitForHelper(WaitHandle ready, WaitHandle exited)
{
if (WaitHandle.WaitAny([ready, exited]) != 0)
throw new InvalidOperationException("The crash helper exited before capture was ready. Check the capture report.");
}
private sealed class ProcessExitWaitHandle : WaitHandle
{
public ProcessExitWaitHandle(Process process) =>
SafeWaitHandle = new SafeWaitHandle(process.Handle, ownsHandle: false);
}
public static int RunHelper(string[] arguments)
{
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64 ||
arguments.Length != 4 || arguments[0] != HelperArgument ||
!uint.TryParse(arguments[1], NumberStyles.None, CultureInfo.InvariantCulture, out var processId) || processId == 0)
return 1;
var attached = false;
StreamWriter? report = null;
try
{
report = new StreamWriter(arguments[3] + ".capture.log", append: true) { AutoFlush = true };
report.WriteLine($"Capture helper started. Process: {processId}. Dump: {arguments[3]}");
report.WriteLine($"Runtime: {RuntimeInformation.FrameworkDescription}. OS: {Environment.OSVersion}.");
using var readyEvent = EventWaitHandle.OpenExisting(arguments[2]);
if (!DebugActiveProcess(processId))
throw NativeFailure("Attach to the game process");
attached = true;
if (!DebugSetProcessKillOnExit(false))
throw NativeFailure("Keep the game alive if the helper exits");
return ProcessDebugEvents(processId, arguments[3], readyEvent, report);
}
catch (Exception exception)
{
report?.WriteLine($"Capture failed: {exception.Message}");
Console.Error.WriteLine($"[CRASH][ERROR] Capture failed: {exception.Message}");
return 1;
}
finally
{
if (attached)
_ = DebugActiveProcessStop(processId);
report?.Dispose();
}
}
private static int ProcessDebugEvents(uint processId, string dumpPath, EventWaitHandle readyEvent, TextWriter report)
{
var initialBreakpoint = true;
var captured = false;
var debuggerBreaks = new DebuggerBreakTracker();
while (true)
{
if (!WaitForDebugEventEx(out var debugEvent, uint.MaxValue))
throw NativeFailure("Wait for a debug event");
var continuation = DebugContinue;
var ready = false;
try
{
try
{
debuggerBreaks.Observe(debugEvent);
}
finally
{
if (debugEvent.Kind is CreateProcessEvent or LoadLibraryEvent && debugEvent.FileHandle != 0)
_ = CloseHandle(debugEvent.FileHandle);
}
if (debugEvent.Kind == ExceptionEvent)
{
continuation = DebugExceptionNotHandled;
if (initialBreakpoint && debugEvent.FirstChance != 0 && debugEvent.Exception.Code == BreakpointException)
{
initialBreakpoint = false;
continuation = DebugContinue;
ready = true;
}
else if (debuggerBreaks.IsDebuggerBreak(debugEvent))
{
continuation = DebugContinue;
report.WriteLine($"Debugger break continued. Thread: {debugEvent.ThreadId}.");
}
else if (debugEvent.FirstChance == 0 && !captured)
{
captured = true;
report.WriteLine($"Unhandled exception: 0x{debugEvent.Exception.Code:X8}. " +
$"Address: 0x{debugEvent.Exception.Address:X16}. Thread: {debugEvent.ThreadId}.");
WriteDump(processId, debugEvent, dumpPath);
report.WriteLine("Dump complete.");
}
}
}
finally
{
// Even a failed dump must leave the original exception unhandled.
if (!ContinueDebugEvent(debugEvent.ProcessId, debugEvent.ThreadId, continuation))
throw NativeFailure("Continue a debug event");
}
if (ready)
{
report.WriteLine("Capture ready. Application exceptions remain unhandled by the helper.");
readyEvent.Set();
}
if (debugEvent.Kind == ExitProcessEvent)
{
report.WriteLine($"Process exited: 0x{debugEvent.ExitCode:X8}. Dump captured: {captured}.");
return 0;
}
}
}
private static Win32Exception NativeFailure(string operation)
{
var error = Marshal.GetLastPInvokeError();
return new Win32Exception(error, $"{operation} failed (Windows error {error}).");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -48,6 +48,8 @@ public enum ProgramHeaderType : uint
Tls = 7,
GnuEhFrame = 0x6474E550,
SceRela = 0x60000000,
SceProcParam = 0x61000001,

View File

@ -0,0 +1,66 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using Iced.Intel;
namespace SharpEmu.Core.Loader;
// Use two 128-bit stores to let Rosetta report a memory fault.
// Keep the source register, general registers, and flags unchanged.
internal static class RosettaVectorStorePatch
{
internal static bool IsRequired { get; } = IsRosettaProcess();
private static unsafe bool IsRosettaProcess()
{
if (!OperatingSystem.IsMacOS() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
return false;
}
int translationStatus = 0;
nuint valueByteCount = sizeof(int);
return ReadSystemControlValue("sysctl.proc_translated", &translationStatus, &valueByteCount, null, 0) == 0 && translationStatus == 1;
}
[DllImport("libSystem.B.dylib", EntryPoint = "sysctlbyname")]
private static extern unsafe int ReadSystemControlValue(string controlName, void* valueBuffer, nuint* valueByteCount, void* newValueBuffer, nuint newValueByteCount);
internal static bool RequiresStoreSplit(in Instruction instruction) =>
instruction.Code is Code.VEX_Vmovups_ymmm256_ymm or Code.VEX_Vmovupd_ymmm256_ymm or Code.VEX_Vmovdqu_ymmm256_ymm &&
instruction.Op0Kind == OpKind.Memory && instruction.Op1Register is >= Register.YMM0 and <= Register.YMM15;
internal static IList<Instruction> SplitVectorStores(IList<Instruction> instructions)
{
var splitInstructions = new List<Instruction>(instructions.Count * 2);
foreach (var instruction in instructions)
{
if (!RequiresStoreSplit(instruction))
{
splitInstructions.Add(instruction);
continue;
}
var lowerStore = instruction;
lowerStore.Code = Code.VEX_Vmovups_xmmm128_xmm;
lowerStore.Op1Register = Register.XMM0 + (instruction.Op1Register - Register.YMM0);
splitInstructions.Add(lowerStore);
var upperStore = instruction;
upperStore.Code = Code.VEX_Vextractf128_xmmm128_ymm_imm8;
upperStore.Op2Kind = OpKind.Immediate8;
upperStore.Immediate8 = 1;
upperStore.MemoryDisplacement64 = unchecked(instruction.MemoryDisplacement64 + 16);
// Use the address size for the displacement size that Iced requires.
upperStore.MemoryDisplSize = instruction.MemoryBase is >= Register.EAX and <= Register.R15D ||
instruction.MemoryIndex is >= Register.EAX and <= Register.R15D ? 4 : 8;
// Give the added instruction a separate address for the block encoder.
// Keep branch targets at the first store.
upperStore.IP = instruction.IP + 1;
splitInstructions.Add(upperStore);
}
return splitInstructions;
}
}

View File

@ -24,7 +24,11 @@ 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,
IReadOnlyList<string>? importedModuleNames = null)
{
ArgumentNullException.ThrowIfNull(programHeaders);
ArgumentNullException.ThrowIfNull(mappedRegions);
@ -44,6 +48,10 @@ public sealed class SelfImage
Title = title;
TitleId = titleId;
Version = version;
TlsModuleId = tlsModuleId;
TlsMemorySize = tlsMemorySize;
TlsStaticOffset = tlsStaticOffset;
ImportedModuleNames = importedModuleNames ?? Array.Empty<string>();
}
public bool IsSelf { get; }
@ -75,4 +83,13 @@ 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; }
public IReadOnlyList<string> ImportedModuleNames { get; }
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,405 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.Intrinsics.X86;
using Iced.Intel;
using static Iced.Intel.AssemblerRegisters;
namespace SharpEmu.Core.Loader;
// Rewrite SHA extension instructions into general-register code when the host CPU lacks them.
// Rosetta 2 and some x86 hosts raise #UD for them, and a signal per instruction is far too slow
// for titles that hash whole files. The expansion keeps every register except the destination
// XMM register and keeps the flags. It runs inside the trampoline, after the red zone is skipped.
internal static class ShaInstructionRewrite
{
// pushfq plus the saved general registers.
private const int SavedBytes = 8 * 13;
// Scratch layout: source 1 (and result), source 2, XMM0 (SHA256RNDS2 round inputs).
private const int Source1 = 0;
private const int Source2 = 16;
private const int Xmm0Copy = 32;
private const int ScratchBytes = 48;
private static readonly Register[] SavedRegisters =
{
Register.RAX, Register.RCX, Register.RDX, Register.RBX, Register.RSI, Register.RDI,
Register.R8, Register.R9, Register.R10, Register.R11, Register.R12, Register.R13,
};
internal static bool IsRequired { get; } = HostLacksSha() &&
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_SHA_REWRITE"), "1", StringComparison.Ordinal);
// Bytes the expansion of one instruction needs at most in the trampoline.
internal const int MaximumExpansionBytes = 1024;
private static bool HostLacksSha()
{
if (!X86Base.IsSupported)
{
return false;
}
var (_, ebx, _, _) = X86Base.CpuId(7, 0);
return (ebx & (1 << 29)) == 0;
}
internal static bool IsShaInstruction(in Instruction instruction) => instruction.Mnemonic is
Mnemonic.Sha1rnds4 or Mnemonic.Sha1nexte or Mnemonic.Sha1msg1 or Mnemonic.Sha1msg2 or
Mnemonic.Sha256rnds2 or Mnemonic.Sha256msg1 or Mnemonic.Sha256msg2;
// A memory source with a segment prefix or an RIP-relative address cannot be rebuilt here.
internal static bool CanRewrite(in Instruction instruction)
{
if (!IsShaInstruction(instruction) || instruction.Op0Kind != OpKind.Register)
{
return false;
}
return instruction.Op1Kind == OpKind.Register ||
(instruction.Op1Kind == OpKind.Memory &&
instruction.SegmentPrefix == Register.None &&
!instruction.IsIPRelativeMemoryOperand);
}
internal static IList<Instruction> Expand(IList<Instruction> instructions)
{
var expanded = new List<Instruction>(instructions.Count);
foreach (var instruction in instructions)
{
if (!CanRewrite(instruction))
{
expanded.Add(instruction);
continue;
}
var assembler = new Assembler(64);
EmitExpansion(assembler, instruction);
var first = true;
ulong generatedIp = 0xFFFF_0000_0000_0000UL + ((instruction.IP & 0xFFFF_FFFF) << 12);
foreach (var generated in assembler.Instructions)
{
var copy = generated;
// Keep branch targets at the first generated instruction.
copy.IP = first ? instruction.IP : generatedIp++;
first = false;
expanded.Add(copy);
}
}
return expanded;
}
internal static void EmitExpansion(Assembler a, in Instruction instruction)
{
var destination = ToXmm(instruction.Op0Register);
a.pushfq();
foreach (var register in SavedRegisters)
{
a.push(ToGpr64(register));
}
a.sub(rsp, ScratchBytes);
if (instruction.Op1Kind == OpKind.Memory)
{
// LEA reads the guest registers before any of them change.
a.lea(rax, RebaseMemory(instruction));
a.mov(rcx, __qword_ptr[rax]);
a.mov(__qword_ptr[rsp + Source2], rcx);
a.mov(rcx, __qword_ptr[rax + 8]);
a.mov(__qword_ptr[rsp + Source2 + 8], rcx);
}
else
{
a.movdqu(__xmmword_ptr[rsp + Source2], ToXmm(instruction.Op1Register));
}
a.movdqu(__xmmword_ptr[rsp + Source1], destination);
if (instruction.Mnemonic == Mnemonic.Sha256rnds2)
{
a.movdqu(__xmmword_ptr[rsp + Xmm0Copy], xmm0);
}
switch (instruction.Mnemonic)
{
case Mnemonic.Sha1rnds4: EmitSha1Rnds4(a, instruction.Immediate8 & 3); break;
case Mnemonic.Sha1nexte: EmitSha1Nexte(a); break;
case Mnemonic.Sha1msg1: EmitSha1Msg1(a); break;
case Mnemonic.Sha1msg2: EmitSha1Msg2(a); break;
case Mnemonic.Sha256rnds2: EmitSha256Rnds2(a); break;
case Mnemonic.Sha256msg1: EmitSha256Msg1(a); break;
default: EmitSha256Msg2(a); break;
}
a.movdqu(destination, __xmmword_ptr[rsp + Source1]);
a.add(rsp, ScratchBytes);
for (var index = SavedRegisters.Length - 1; index >= 0; index--)
{
a.pop(ToGpr64(SavedRegisters[index]));
}
a.popfq();
}
// The guest stack pointer is above the scratch area, the saved registers and the red zone.
private static AssemblerMemoryOperand RebaseMemory(in Instruction instruction)
{
var displacement = unchecked((long)instruction.MemoryDisplacement64);
if (instruction.MemoryBase == Register.RSP)
{
displacement += ScratchBytes + SavedBytes + 128;
}
var baseRegister = instruction.MemoryBase == Register.None ? default : ToGpr64(instruction.MemoryBase);
var memory = instruction.MemoryBase == Register.None ? __[displacement] : __[baseRegister + displacement];
if (instruction.MemoryIndex != Register.None)
{
memory = instruction.MemoryBase == Register.None
? __[ToGpr64(instruction.MemoryIndex) * instruction.MemoryIndexScale + displacement]
: __[baseRegister + ToGpr64(instruction.MemoryIndex) * instruction.MemoryIndexScale + displacement];
}
return memory;
}
private static AssemblerMemoryOperand Dword(int slot, int index) => __dword_ptr[rsp + slot + 4 * index];
private static void EmitSha1Rnds4(Assembler a, int function)
{
var k = function switch
{
0 => 0x5A827999,
1 => 0x6ED9EBA1,
2 => unchecked((int)0x8F1BBCDC),
_ => unchecked((int)0xCA62C1D6),
};
a.mov(eax, Dword(Source1, 3));
a.mov(ebx, Dword(Source1, 2));
a.mov(ecx, Dword(Source1, 1));
a.mov(edx, Dword(Source1, 0));
a.xor(esi, esi);
for (var round = 0; round < 4; round++)
{
switch (function)
{
case 0:
// (b & c) ^ (~b & d) == d ^ (b & (c ^ d))
a.mov(edi, ecx);
a.xor(edi, edx);
a.and(edi, ebx);
a.xor(edi, edx);
break;
case 2:
// (b & c) | (d & (b | c)) is the majority of b, c and d.
a.mov(edi, ebx);
a.or(edi, ecx);
a.and(edi, edx);
a.mov(r8d, ebx);
a.and(r8d, ecx);
a.or(edi, r8d);
break;
default:
a.mov(edi, ebx);
a.xor(edi, ecx);
a.xor(edi, edx);
break;
}
a.add(edi, Dword(Source2, 3 - round));
a.add(edi, esi);
a.add(edi, k);
a.mov(r8d, eax);
a.rol(r8d, (byte)5);
a.add(edi, r8d);
a.mov(esi, edx);
a.mov(edx, ecx);
a.mov(ecx, ebx);
a.rol(ecx, (byte)30);
a.mov(ebx, eax);
a.mov(eax, edi);
}
a.mov(Dword(Source1, 3), eax);
a.mov(Dword(Source1, 2), ebx);
a.mov(Dword(Source1, 1), ecx);
a.mov(Dword(Source1, 0), edx);
}
private static void EmitSha1Nexte(Assembler a)
{
a.mov(eax, Dword(Source1, 3));
a.rol(eax, (byte)30);
a.add(eax, Dword(Source2, 3));
a.mov(Dword(Source1, 3), eax);
for (var index = 0; index < 3; index++)
{
a.mov(ecx, Dword(Source2, index));
a.mov(Dword(Source1, index), ecx);
}
}
private static void EmitSha1Msg1(Assembler a)
{
// w0..w3 from source 1 (w0 in dword 3), w4 and w5 from source 2.
a.mov(eax, Dword(Source1, 3));
a.mov(ebx, Dword(Source1, 2));
a.mov(ecx, Dword(Source1, 1));
a.mov(edx, Dword(Source1, 0));
a.mov(esi, Dword(Source2, 3));
a.mov(edi, Dword(Source2, 2));
a.xor(eax, ecx);
a.xor(ebx, edx);
a.xor(ecx, esi);
a.xor(edx, edi);
a.mov(Dword(Source1, 3), eax);
a.mov(Dword(Source1, 2), ebx);
a.mov(Dword(Source1, 1), ecx);
a.mov(Dword(Source1, 0), edx);
}
private static void EmitSha1Msg2(Assembler a)
{
a.mov(eax, Dword(Source1, 3));
a.xor(eax, Dword(Source2, 2));
a.rol(eax, (byte)1);
a.mov(ebx, Dword(Source1, 2));
a.xor(ebx, Dword(Source2, 1));
a.rol(ebx, (byte)1);
a.mov(ecx, Dword(Source1, 1));
a.xor(ecx, Dword(Source2, 0));
a.rol(ecx, (byte)1);
a.mov(edx, Dword(Source1, 0));
a.xor(edx, eax);
a.rol(edx, (byte)1);
a.mov(Dword(Source1, 3), eax);
a.mov(Dword(Source1, 2), ebx);
a.mov(Dword(Source1, 1), ecx);
a.mov(Dword(Source1, 0), edx);
}
private static void EmitSha256Rnds2(Assembler a)
{
// a..h in eax, ebx, ecx, edx, esi, edi, r8d, r9d.
a.mov(eax, Dword(Source2, 3));
a.mov(ebx, Dword(Source2, 2));
a.mov(ecx, Dword(Source1, 3));
a.mov(edx, Dword(Source1, 2));
a.mov(esi, Dword(Source2, 1));
a.mov(edi, Dword(Source2, 0));
a.mov(r8d, Dword(Source1, 1));
a.mov(r9d, Dword(Source1, 0));
for (var round = 0; round < 2; round++)
{
// r10d = h + Sigma1(e) + Ch(e, f, g) + wk
EmitRotateXor(a, r10d, esi, r11d, 6, 11, 25);
a.add(r10d, r9d);
a.add(r10d, Dword(Xmm0Copy, round));
a.mov(r11d, edi);
a.xor(r11d, r8d);
a.and(r11d, esi);
a.xor(r11d, r8d);
a.add(r10d, r11d);
// r11d = Sigma0(a) + Maj(a, b, c)
EmitRotateXor(a, r11d, eax, r12d, 2, 13, 22);
a.mov(r12d, eax);
a.or(r12d, ebx);
a.and(r12d, ecx);
a.mov(r13d, eax);
a.and(r13d, ebx);
a.or(r12d, r13d);
a.add(r11d, r12d);
a.mov(r9d, r8d);
a.mov(r8d, edi);
a.mov(edi, esi);
a.mov(esi, edx);
a.add(esi, r10d);
a.mov(edx, ecx);
a.mov(ecx, ebx);
a.mov(ebx, eax);
a.mov(eax, r10d);
a.add(eax, r11d);
}
a.mov(Dword(Source1, 3), eax);
a.mov(Dword(Source1, 2), ebx);
a.mov(Dword(Source1, 1), esi);
a.mov(Dword(Source1, 0), edi);
}
private static void EmitSha256Msg1(Assembler a)
{
// w0..w3 in edx, ecx, ebx, eax; w4 in esi. Results go to r9d..r12d.
a.mov(eax, Dword(Source1, 3));
a.mov(ebx, Dword(Source1, 2));
a.mov(ecx, Dword(Source1, 1));
a.mov(edx, Dword(Source1, 0));
a.mov(esi, Dword(Source2, 0));
EmitSmallSigma(a, r9d, esi, r8d, 7, 18, 3);
a.add(r9d, eax);
EmitSmallSigma(a, r10d, eax, r8d, 7, 18, 3);
a.add(r10d, ebx);
EmitSmallSigma(a, r11d, ebx, r8d, 7, 18, 3);
a.add(r11d, ecx);
EmitSmallSigma(a, r12d, ecx, r8d, 7, 18, 3);
a.add(r12d, edx);
a.mov(Dword(Source1, 3), r9d);
a.mov(Dword(Source1, 2), r10d);
a.mov(Dword(Source1, 1), r11d);
a.mov(Dword(Source1, 0), r12d);
}
private static void EmitSha256Msg2(Assembler a)
{
// w16 = s1[0] + sigma1(w14); w17 = s1[1] + sigma1(w15); w18 and w19 use w16 and w17.
a.mov(esi, Dword(Source2, 2));
EmitSmallSigma(a, eax, esi, r8d, 17, 19, 10);
a.add(eax, Dword(Source1, 0));
a.mov(esi, Dword(Source2, 3));
EmitSmallSigma(a, ebx, esi, r8d, 17, 19, 10);
a.add(ebx, Dword(Source1, 1));
EmitSmallSigma(a, ecx, eax, r8d, 17, 19, 10);
a.add(ecx, Dword(Source1, 2));
EmitSmallSigma(a, edx, ebx, r8d, 17, 19, 10);
a.add(edx, Dword(Source1, 3));
a.mov(Dword(Source1, 0), eax);
a.mov(Dword(Source1, 1), ebx);
a.mov(Dword(Source1, 2), ecx);
a.mov(Dword(Source1, 3), edx);
}
// result = ror(x, r1) ^ ror(x, r2) ^ ror(x, r3)
private static void EmitRotateXor(
Assembler a, AssemblerRegister32 result, AssemblerRegister32 x, AssemblerRegister32 temp, int r1, int r2, int r3)
{
a.mov(result, x);
a.ror(result, (byte)r1);
a.mov(temp, x);
a.ror(temp, (byte)r2);
a.xor(result, temp);
a.mov(temp, x);
a.ror(temp, (byte)r3);
a.xor(result, temp);
}
// result = ror(x, r1) ^ ror(x, r2) ^ (x >> shift)
private static void EmitSmallSigma(
Assembler a, AssemblerRegister32 result, AssemblerRegister32 x, AssemblerRegister32 temp, int r1, int r2, int shift)
{
a.mov(result, x);
a.ror(result, (byte)r1);
a.mov(temp, x);
a.ror(temp, (byte)r2);
a.xor(result, temp);
a.mov(temp, x);
a.shr(temp, (byte)shift);
a.xor(result, temp);
}
private static AssemblerRegisterXMM ToXmm(Register register) => new(register);
private static AssemblerRegister64 ToGpr64(Register register) => new(register.GetFullRegister());
}

View File

@ -10,6 +10,8 @@ public interface IVirtualMemory : ICpuMemory
{
void Clear();
bool IsBackedView(ulong address);
void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection);
IReadOnlyList<VirtualMemoryRegion> SnapshotRegions();

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