Compare commits

...

125 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
473 changed files with 46244 additions and 3473 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 485 KiB

BIN
.github/images/gow-sos.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.4</SharpEmuVersion>
<SharpEmuVersion>0.0.5-nexus</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>

View File

@ -22,6 +22,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<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" />
<!-- 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" />

View File

@ -35,6 +35,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
---
> [!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
@ -55,17 +58,17 @@ Our goal is **not** to emulate PS4 games, as there is already an excellent emula
## Games Tested
| Demons Souls Remake | Tomb Raider V Remastered |
| Demon's Souls Remake | ASTRO BOT |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![DeS screenshot](./.github/images/demons-souls.jpg) | ![Tomb Raider V](./.github/images/tomb-raider-v-remastered.jpg) |
| ![DeS screenshot](./.github/images/demons-souls.jpg) | ![Astro Bot](./.github/images/astro-bot.jpg) |
| Hades | Dead Cells |
| ASTRO's PLAYROOM | Dead Cells |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Hades](./.github/images/hades.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
| ![Astro's Playroom](./.github/images/astros-playroom.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
| PAC-MAN World Re-PAC | Cult of the Lamb |
| God of War: Sons of Sparta | Tomb Raider V Remastered |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Pac-Man](./.github/images/pac-man-world-re-pac.jpg) | ![Cult of the Lamb](./.github/images/cult-of-the-lamb.jpg) |
| ![GoW SOS](./.github/images/gow-sos.jpg) | ![Tomb Raider V](./.github/images/tomb-raider-v-remastered.jpg) |
## Status

View File

@ -9,12 +9,12 @@ Demon's Souls plays Bink 2 (.bk2) files through a Bink implementation linked
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
decoder cannot observe or replace those frames.
SharpEmu observes successful guest .bk2 opens and, when a Bink decoder is
available, presents its decoded BGRA frames at the normal guest-flip boundary.
This preserves the game's own timing and lets the host Vulkan presenter display
the movie without trying to execute the PS5-specific Bink GPU decode path.
By default, SharpEmu leaves Bink decoding to the implementation linked into
the game (`SHARPEMU_BINK_MODE=guest`).
The default path decodes by calling FFmpeg's own C API directly from managed
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
@ -24,26 +24,13 @@ 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=guest` to leave decoding to the Bink implementation
statically linked into the game instead. Set `skip` only when explicitly
testing a title whose cinematics are optional.
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=native is equivalent to the default and mainly useful for
being explicit about it.
The experimental `SHARPEMU_BINK_MODE=ffmpeg` override is unrelated to the
default path above: instead of calling into FFmpeg in-process, it spawns a
standalone `ffmpeg` executable and reads raw frames from its stdout
(`src/SharpEmu.Libs/Bink/FfmpegBinkFrameSource.cs`). SharpEmu searches
`SHARPEMU_FFMPEG_PATH`, the executable directory, its `ffmpeg` subdirectory,
and then `PATH` (plus a couple of common Homebrew paths on macOS). That
`ffmpeg` build must contain a Bink 2 decoder itself; a stock FFmpeg build that
only recognizes the Bink container is not sufficient. Most users want the
default `native` mode instead, which always has Bink 2 support since it's
built against `ffmpeg-core` specifically.
`SHARPEMU_BINK_MODE=ffmpeg` is an alias for `native`.
## Supplying the FFmpeg libraries

View File

@ -14,7 +14,7 @@ The GUI shows 14 variables on its **Options** and **Game options** pages. This l
`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 252 variables. The source was examined on 2026-09-19, branch `dev`.
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
@ -29,18 +29,18 @@ The descriptions come from the code that reads each variable. The emulator was n
| Group | Variables |
| --- | --- |
| Behavior switches | 16 |
| GPU and Vulkan | 50 |
| Behavior switches | 19 |
| GPU and Vulkan | 59 |
| Metal | 10 |
| Audio and video | 7 |
| Audio and video | 6 |
| Input | 5 |
| Memory and CPU | 17 |
| Memory and CPU | 19 |
| Loader | 11 |
| Debugger | 1 |
| Log channels | 72 |
| Traces | 22 |
| Traces | 25 |
| Data dumps | 12 |
| Performance measurement | 16 |
| Performance measurement | 17 |
| Internal | 1 |
| Tests and tools | 12 |
@ -52,13 +52,16 @@ These variables change how the emulator operates.
| --- | --- | --- | --- |
| `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 | Sets the time that an import call pattern can repeat before the import loop guard forces a guest exit. The default is 5 seconds. The value 0 turns the guard off. | `DirectExecutionBackend.Imports.cs`, `DirectExecutionBackend.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` |
@ -73,6 +76,7 @@ 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` |
@ -109,20 +113,28 @@ These variables change the GPU path or the Vulkan presenter.
| `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. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanPipelineCacheStorage.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE` | `0` | `0` stops the save and the load of the Vulkan pipeline cache file. The cache then stays in memory only. Default is a persistent cache file. | `VulkanVideoPresenter.Device.Setup.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
@ -149,7 +161,6 @@ These variables apply to audio output and video playback.
| --- | --- | --- | --- |
| `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_AUDIO_OUT2_STACK_WRITES` | `1` or list (`portstate`, `systemstate`, `speaker`) | Controls writes of AudioOut2 results to output buffers on the guest stack. The default `1` permits all writes. A list permits only the named outputs and the other calls return success without a write. | `AudioOut2Exports.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` |
@ -182,7 +193,9 @@ These variables apply to guest memory and guest code execution.
| `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_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. | `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` |
@ -310,9 +323,11 @@ Each variable records a detailed sequence of events. Traces can be large.
| `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` |
@ -324,6 +339,7 @@ Each variable records a detailed sequence of events. Traces can be large.
| `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
@ -356,6 +372,7 @@ Each variable records timings or counts.
| `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` |

View File

@ -323,6 +323,16 @@ internal static partial class Program
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();
});
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
result = runtime.Run(ebootPath);

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

@ -88,6 +88,14 @@ public sealed partial class DirectExecutionBackend
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}");
}
}
}
@ -463,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

@ -114,7 +114,7 @@ public sealed partial class DirectExecutionBackend
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
if (TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
if (MayBeGuestInt41(exceptionRecord) && TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
{
return -1;
}
@ -157,6 +157,11 @@ public sealed partial class DirectExecutionBackend
{
return -1;
}
if (exceptionCode == StatusIllegalInstruction &&
TryRecoverShaInstruction(contextRecord, rip))
{
return -1;
}
if (IsBenignHostDebugException(exceptionCode))
{
return -1;
@ -360,7 +365,13 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
byte[] code = new byte[16];
if (TryReadHostBytes(rip, code))
DumpHostRegion("RIP", rip);
// Guest code can be mapped execute-only, which makes every
// host-side read of it fail. The guest memory view still
// resolves those pages, so prefer it here: otherwise the
// whole call-site report below is silently skipped on
// exactly the faults that need it.
if (TryReadGuestOrHostBytes(rip, code))
{
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
if (code[0] == 100)
@ -378,12 +389,12 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
}
byte[] before = new byte[16];
if (rip > 16 && TryReadHostBytes(rip - 16, before))
if (rip > 16 && TryReadGuestOrHostBytes(rip - 16, before))
{
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
}
byte[] window = new byte[64];
if (rip > 32 && TryReadHostBytes(rip - 32, window))
if (rip > 32 && TryReadGuestOrHostBytes(rip - 32, window))
{
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
}
@ -400,7 +411,7 @@ public sealed partial class DirectExecutionBackend
continue;
}
byte[] callSiteWindow = new byte[48];
if (TryReadHostBytes(candidate - 24, callSiteWindow))
if (TryReadGuestOrHostBytes(candidate - 24, callSiteWindow))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
@ -412,6 +423,11 @@ public sealed partial class DirectExecutionBackend
{
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
}
// The faulting instruction is the first thing an access
// violation report has to answer, so decode it through guest
// memory as well: that path still works when the host code
// page refuses a plain read.
DumpGuestInstructionStream("fault", rip, 4);
DumpRecentImportTrace();
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
DumpGuestRegisterWindowDiagnostics(
@ -603,6 +619,16 @@ public sealed partial class DirectExecutionBackend
return (ulong)(_workerAbortStack + (nint)WorkerAbortStackSize - 0x100) & ~0xFUL;
}
// On Windows an INT n the guest may not execute raises a general-protection fault,
// reported as an access violation whose fault address is all ones; a page fault
// carries the data address instead. GPU-tracked and lazily committed pages fault
// thousands of times per frame, so skip the opcode probe (a VirtualQuery and a
// read of the faulting instruction) for them.
private static unsafe bool MayBeGuestInt41(EXCEPTION_RECORD* exceptionRecord) =>
!OperatingSystem.IsWindows() ||
exceptionRecord->NumberParameters < 2 ||
exceptionRecord->ExceptionInformation[1] == ulong.MaxValue;
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
{
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
@ -1458,6 +1484,44 @@ public sealed partial class DirectExecutionBackend
return true;
}
/// <summary>
/// Reads guest bytes for a crash report, preferring the guest memory view
/// over a raw host read. Guest code pages are commonly committed
/// PAGE_EXECUTE, which no host read can satisfy.
/// </summary>
private bool TryReadGuestOrHostBytes(ulong address, byte[] buffer)
{
if (_cpuContext is { } context)
{
try
{
if (context.Memory.TryRead(address, buffer))
{
return true;
}
}
catch
{
// Fall through to the host reader.
}
}
return TryReadHostBytes(address, buffer);
}
private unsafe static void DumpHostRegion(string name, ulong address)
{
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
Console.Error.WriteLine($"[LOADER][INFO] {name} region: <VirtualQuery failed> address=0x{address:X16}");
return;
}
Console.Error.WriteLine(
$"[LOADER][INFO] {name} region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} " +
$"state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08} type=0x{mbi.Type:X08}");
}
private unsafe static bool IsReadableHostRange(ulong address, int byteCount, bool allowExecuteOnly = false)
{
if (byteCount < 0 || !IsCanonicalUserAddress(address))

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

@ -185,6 +185,11 @@ public sealed partial class DirectExecutionBackend
return hotMemoryResult;
}
if (importStubEntry.IsTrivialLeaf && !_disableTrivialLeafDispatch)
{
return DispatchTrivialLeaf(cpuContext, in importStubEntry, argPackPtr, num);
}
if (importStubEntry.IsLeaf &&
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
{
@ -238,8 +243,8 @@ public sealed partial class DirectExecutionBackend
if (probeTarget &&
Interlocked.Increment(ref _probeImportReturnAddressCount) <= 2048)
{
var frameValue = TryReadStackU64(value4, out var savedRbp) ? savedRbp : 0;
var frameReturn = TryReadStackU64(value4 + sizeof(ulong), out var savedReturn)
var frameValue = TryReadImportStackU64(value4, out var savedRbp) ? savedRbp : 0;
var frameReturn = TryReadImportStackU64(value4 + sizeof(ulong), out var savedReturn)
? savedReturn
: 0;
Console.Error.WriteLine(
@ -340,6 +345,14 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
}
// Once the host is shutting down, a guest thread still running reaches no more HLE: the
// services behind the imports are being torn down, and a title that sees them fail (a null
// command buffer from the GPU library, say) crashes the process on its way out.
if (_forcedGuestExit && !ActiveForcedGuestExit && TryEndGuestSliceForShutdown(argPackPtr))
{
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
if (!isGuestWorker &&
!ActiveForcedGuestExit &&
ShouldForceGuestExitOnImportLoop(in importStubEntry, num7, num, value, value2))
@ -1217,10 +1230,56 @@ public sealed partial class DirectExecutionBackend
}
}
// Every import reads its stack arguments. On POSIX a region query takes the global region lock,
// so keep the last readable range per thread until any mapping changes. The signal handlers
// use TryReadStackU64 instead: they must not touch thread-static storage.
[ThreadStatic] private static ulong _importReadableStart;
[ThreadStatic] private static ulong _importReadableEnd;
[ThreadStatic] private static long _importReadableGeneration;
private unsafe static bool TryReadImportStackU64(ulong address, out ulong value)
{
if (OperatingSystem.IsWindows())
{
return TryReadHostQword(address, out value);
}
// Stack arguments sit on the calling guest thread's own stack, which the backend mapped read-write
// for the thread's lifetime and no tracker ever protects. Reading inside it needs no query: the
// cached range below is keyed by a mapping generation that every tracker protection change bumps,
// so falling through here cost a locked host region query per argument on nearly every import.
if (_activeGuestThreadState is { StackSize: > 0 } thread &&
address >= thread.StackBase && address <= thread.StackBase + thread.StackSize - sizeof(ulong))
{
value = *(ulong*)address;
return true;
}
var generation = HostMemory.MappingGeneration;
if (generation == _importReadableGeneration &&
address >= _importReadableStart && address <= _importReadableEnd - 8)
{
value = *(ulong*)address;
return true;
}
if (!TryQueryReadableRange(address, out var start, out var end))
{
value = 0;
return false;
}
_importReadableStart = start;
_importReadableEnd = end;
_importReadableGeneration = generation;
value = *(ulong*)address;
return true;
}
private static ulong ReadImportStackArgument(nint argPackPtr, int index)
{
var address = checked((ulong)argPackPtr + 104UL + (ulong)index * sizeof(ulong));
return TryReadHostQword(address, out var value) ? value : 0;
return TryReadImportStackU64(address, out var value) ? value : 0;
}
private static GuestCpuContinuation CaptureImportBoundaryContinuation(
@ -1459,6 +1518,68 @@ public sealed partial class DirectExecutionBackend
return true;
}
private static readonly bool _disableTrivialLeafDispatch = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRIVIAL_LEAF"), "1", StringComparison.Ordinal);
// Tiny exports called millions of times per second, where the ~250ns of full
// import bookkeeping costs more than the export itself. Each one reads only
// argument registers (RDI..R9) and thread-local state, returns in RAX, never
// blocks, yields, calls back into the guest or reads the guest stack.
private static bool IsTrivialLeafImport(string nid) =>
nid is
"0-KXaS70xy4" or // pthread_getspecific
"eoht7mQOCmo" or // scePthreadGetspecific
"EI-5-jlq2dE" or // scePthreadGetthreadid
"3eqs37G74-s" or // pthread_getthreadid_np
"BNowx2l588E" or // sceKernelGetProcessTimeCounterFrequency
"fgxnMeTNUtY" or // sceKernelGetProcessTimeCounter
"4J2sUJmuHZQ" or // sceKernelGetProcessTime
"-2IRUCO--PM" or // sceKernelReadTsc
"1j3S3n-tTW4" or // sceKernelGetTscFrequency
"ob5xAW4ln-0" or // strchr
"9yDWMxEFdJU" or // strrchr
"V++UgBtQhn0"; // sceAgcGetDataPacketPayloadAddress
private unsafe ulong DispatchTrivialLeaf(
CpuContext cpuContext,
in ImportStubEntry importStubEntry,
nint argPackPtr,
long dispatchIndex)
{
// Polling loops on the host main thread use the time queries as loop-guard
// boundaries; keep resetting the pattern so skipping the guard here cannot
// turn a legitimate time-polling loop into a forced exit.
if (importStubEntry.IsLoopGuardBoundary && !GuestThreadExecution.IsGuestThread)
{
ResetImportLoopPattern();
}
cpuContext.Rip = importStubEntry.Address;
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
cpuContext[CpuRegister.Rdx] = *(ulong*)(argPackPtr + 16);
cpuContext[CpuRegister.Rcx] = *(ulong*)(argPackPtr + 24);
cpuContext[CpuRegister.R8] = *(ulong*)(argPackPtr + 32);
cpuContext[CpuRegister.R9] = *(ulong*)(argPackPtr + 40);
cpuContext.ClearRaxWriteFlag();
var returnValue = importStubEntry.Export!.Function(cpuContext);
if (!cpuContext.WasRaxWritten)
{
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
}
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK &&
ShouldLogImportResult(importStubEntry.Nid, (OrbisGen2Result)returnValue))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Import#{dispatchIndex} result: {(OrbisGen2Result)returnValue} ({importStubEntry.Nid}) " +
$"rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
$"ret=0x{*(ulong*)(argPackPtr + 96):X16}");
}
return cpuContext[CpuRegister.Rax];
}
private static bool IsNoBlockLeafImport(string nid) =>
nid is
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
@ -1516,6 +1637,19 @@ public sealed partial class DirectExecutionBackend
var expectedEqueueTimeout =
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
// scePthreadCondTimedwait and sceKernelWaitEventFlag report an elapsed timeout.
var expectedWaitTimeout =
(nid is "BmMjYxmew1w" or "JTvBflhYazQ") &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
// scePthreadMutexLock on an error-checking mutex the caller already owns; titles use it
// to build their own recursive locks.
var expectedErrorCheckRelock =
string.Equals(nid, "9UK1vLZQft4", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
// scePadReadState on a handle that is not open; titles poll every pad slot.
var expectedPadNotOpen =
string.Equals(nid, "YndgXqQVV7c", StringComparison.Ordinal) &&
resultValue == unchecked((int)0x80920003);
var expectedMutexTrylockBusy =
(nid is "K-jXhbt2gn4" or "upoVrzMHFeE") &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
@ -1540,6 +1674,9 @@ public sealed partial class DirectExecutionBackend
if (!expectedFileProbeMiss &&
!expectedTimedWaitTimeout &&
!expectedEqueueTimeout &&
!expectedWaitTimeout &&
!expectedErrorCheckRelock &&
!expectedPadNotOpen &&
!expectedMutexTrylockBusy &&
!expectedSemaphoreTrywaitAgain &&
!expectedPollSemaBusy &&
@ -1732,6 +1869,20 @@ public sealed partial class DirectExecutionBackend
}
}
// Returns the import straight to the host entry stub, which ends this guest slice.
private unsafe bool TryEndGuestSliceForShutdown(nint argPackPtr)
{
ulong sentinel = ActiveEntryReturnSentinelRip;
if (sentinel < 65536 || !TryPatchActiveGuestReturnSlot(sentinel))
{
return false;
}
*(ulong*)(argPackPtr + 96) = sentinel;
ActiveForcedGuestExit = true;
return true;
}
private unsafe bool TryForceGuestExitToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid)
{
ulong num = ActiveEntryReturnSentinelRip;
@ -1867,6 +2018,7 @@ public sealed partial class DirectExecutionBackend
"BmMjYxmew1w" or // scePthreadCondTimedwait
"Op8TBGY5KHg" or // pthread_cond_wait
"27bAgiJmOh0" or // pthread_cond_timedwait
"Zxa0VhQVTsk" or // sceKernelWaitSema
"n88vx3C5nW8" or // gettimeofday
"lLMT9vJAck0" or // clock_gettime
"-2IRUCO--PM" or // sceKernelReadTsc

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

@ -134,7 +134,7 @@ public sealed partial class DirectExecutionBackend
try
{
var state = _activeGuestThreadState;
if (state is { Name: "tbb_thead" })
if (LogThreadMode && state is { Name: "tbb_thead" })
{
var n = Interlocked.Increment(ref _tbbNativeRunEnterCount);
if (n <= 12 || n % 64 == 0)
@ -750,6 +750,7 @@ public sealed partial class DirectExecutionBackend
_activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!);
RouteGuestAccessFaultsToSignals();
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (backend._workerDoneEventTlsIndex != uint.MaxValue)
{

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

@ -5,6 +5,7 @@ using System;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.HLE.GpuMemory;
namespace SharpEmu.Core.Cpu.Native;
@ -23,6 +24,7 @@ public sealed unsafe partial class DirectExecutionBackend
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;
@ -77,6 +79,7 @@ public sealed unsafe partial class DirectExecutionBackend
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];
@ -104,6 +107,7 @@ public sealed unsafe partial class DirectExecutionBackend
_posixSignalBackend = this;
if (_posixSignalHandlersInstalled)
{
PublishSignalStackState();
return;
}
@ -117,17 +121,20 @@ public sealed unsafe partial class DirectExecutionBackend
WarmUpPosixSignalPath();
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
_posixSignalTrampoline = CreatePosixSignalTrampoline();
PublishSignalStackState();
if (!InstallPosixSignalHandler(PosixSigSegv) ||
!InstallPosixSignalHandler(PosixSigBus) ||
!InstallPosixSignalHandler(PosixSigIll) ||
!InstallPosixSignalHandler(PosixSigTrap) ||
!InstallPosixSignalHandler(PosixSigAbort))
!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)");
Console.Error.WriteLine("[LOADER][INFO] POSIX signal exception bridge installed (SIGSEGV/SIGBUS/SIGILL/SIGFPE)");
}
/// <summary>
@ -191,7 +198,7 @@ public sealed unsafe partial class DirectExecutionBackend
{
byte* action = stackalloc byte[PosixSigactionSize];
new Span<byte>(action, PosixSigactionSize).Clear();
*(nint*)action = (nint)(delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal;
*(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
@ -238,6 +245,7 @@ public sealed unsafe partial class DirectExecutionBackend
// 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)))
@ -245,6 +253,17 @@ public sealed unsafe partial class DirectExecutionBackend
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;
@ -262,6 +281,28 @@ public sealed unsafe partial class DirectExecutionBackend
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);
@ -305,6 +346,11 @@ public sealed unsafe partial class DirectExecutionBackend
{
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);

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

@ -9,10 +9,23 @@ public sealed partial class DirectExecutionBackend
{
private const int HostStackStateBytes = 3 * sizeof(ulong);
private static unsafe bool TryGetGuestStackBounds(ulong stackPointer, out ulong bottom, out ulong top)
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;
if (VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 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;
@ -33,6 +46,36 @@ public sealed partial class DirectExecutionBackend
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)
{
@ -146,4 +189,18 @@ public sealed partial class DirectExecutionBackend
[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

@ -53,6 +53,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
public bool IsNoBlockLeaf { get; }
// Argument-register-only exports that never block or touch the guest
// stack; DispatchImport runs them without the full import bookkeeping.
public bool IsTrivialLeaf { get; }
public bool SuppressStrlenTrace { get; }
public bool IsLoopGuardBoundary { get; }
@ -74,6 +78,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
Export = export;
IsLeaf = isLeaf;
IsNoBlockLeaf = isNoBlockLeaf;
IsTrivialLeaf = export is not null && IsTrivialLeafImport(nid);
SuppressStrlenTrace = suppressStrlenTrace;
IsLoopGuardBoundary = isLoopGuardBoundary;
NidHash = nidHash;
@ -151,6 +156,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private static readonly ulong GuestThreadTlsBaseAddress = OperatingSystem.IsWindows() ? 0x7FFE_0000_0000UL : 0x6FFE_0000_0000UL;
private const ulong GuestThreadStackSize = 0x0020_0000UL;
private const ulong GuestThreadStackHeadroom = 0x0010_0000UL;
private const ulong GuestThreadStackSizeLimit = 0x0400_0000UL;
private const ulong GuestThreadTlsSize = 0x0001_0000UL;
@ -285,7 +292,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private readonly List<nint> _importHandlerTrampolines = new List<nint>();
private const int GuestContextTransferFrameQwords = 20;
private const int GuestContextTransferFrameQwords = 22;
private readonly object _guestContextTransferStubGate = new();
@ -606,9 +613,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private Action? _work;
private volatile bool _stopping;
// 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 rather than unwinding. Demon's Souls' render thread
// overflowed the default thread stack within a minute of reaching its main
// loop; the reservation is virtual, so a generous stack costs nothing until
// it is touched.
private const int GuestExecutionStackBytes = 64 * 1024 * 1024;
public GuestExecutionRunner(ulong guestThreadHandle, string name, ThreadPriority priority)
{
_thread = new Thread(() => ThreadMain(guestThreadHandle))
_thread = new Thread(() => ThreadMain(guestThreadHandle), GuestExecutionStackBytes)
{
IsBackground = true,
Name = $"SharpEmu-{name}",
@ -659,18 +674,30 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
finally
{
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
_workAvailable.Dispose();
}
}
public void RequestStop()
{
lock (_gate)
{
if (_stopping)
{
return;
}
_stopping = true;
_workAvailable.Set();
}
}
public void Dispose()
{
_stopping = true;
_workAvailable.Set();
RequestStop();
if (!ReferenceEquals(Thread.CurrentThread, _thread))
{
_thread.Join(500);
}
_workAvailable.Dispose();
}
}
@ -715,6 +742,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private int _readyGuestThreadCount;
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
private readonly Queue<(IVirtualMemory Memory, ulong StackBase, ulong StackSize, ulong TlsBase)> _reusableGuestThreadRegions = new();
private readonly Dictionary<ulong, ExternalGuestThreadState> _externalGuestThreads = new Dictionary<ulong, ExternalGuestThreadState>();
@ -1183,9 +1211,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
Environment.GetEnvironmentVariable("SHARPEMU_PROBE_IMPORT_RET_ADDRESS"));
_probeImportReturnAddressCount = 0;
_importFilter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FILTER");
_disableImportLoopGuard = string.Equals(
_disableImportLoopGuard = !string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"),
"1",
"0",
StringComparison.Ordinal);
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
_entryReturnSentinelRip = 0uL;
@ -1212,6 +1240,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_patchedEa020eLookupCall = false;
MarkExecutionProgress();
BindTlsBase(context);
RouteGuestAccessFaultsToSignals();
var previousGuestThreadScheduler = GuestThreadExecution.Scheduler;
GuestThreadExecution.Scheduler = this;
try
@ -1260,6 +1289,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
Console.Error.WriteLine($"[LOADER][INFO] Setting up {importStubs.Count} import stubs...");
ClearImportHandlerTrampolines();
ConfigureGuestFastPath();
_importEntries = new ImportStubEntry[importStubs.Count];
HashSet<ulong> hashSet = new HashSet<ulong>(importStubs.Keys);
int num = 0;
@ -1326,6 +1356,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
LastError = "Failed to create import trampoline for NID " + text2;
return false;
}
if (TryCreateGuestFastPathStub(text2, num5, out var fastPathStub))
{
num5 = fastPathStub;
}
if (_logAllImports)
{
Console.Error.WriteLine($"[LOADER][DEBUG] SetupImportStubs: Trampoline for {text2} -> 0x{num5:X16}");
@ -1338,7 +1372,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
num2++;
num++;
}
Console.Error.WriteLine($"[LOADER][INFO] Setup {num2}/{importStubs.Count} import stubs (direct bridge, lle_redirects={num3})");
Console.Error.WriteLine($"[LOADER][INFO] Setup {num2}/{importStubs.Count} import stubs (direct bridge, lle_redirects={num3}, fast_path_stubs={_guestFastPathStubCount})");
return num2 == importStubs.Count;
}
@ -1422,6 +1456,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
var importIndex = currentEntries.Length + i;
var trampoline = CreateImportHandlerTrampoline(importIndex);
if (trampoline != 0 &&
TryCreateGuestFastPathStub(nid, trampoline, out var runtimeFastPathStub))
{
trampoline = runtimeFastPathStub;
}
if (trampoline == 0 || !PatchImportStub((nint)(long)address, trampoline))
{
error = $"failed to install runtime import trampoline at 0x{address:X16}";
@ -1833,7 +1872,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private static bool IsHlePreferredNid(string nid)
{
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
// The shipped libc memset body contains a PS5-specific int 45h path
// which is not a Windows host instruction. Use the bounds-aware HLE
// implementation instead of redirecting this import to LLE.
return string.Equals(nid, "8zTFvBIAIN8", StringComparison.Ordinal) ||
string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) ||
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
}
@ -1882,6 +1925,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
return HasUsableLleLibcExport("tcVi5SivF7Q", "sprintf");
}
if (IsLibcFileObjectExport(exportName))
{
return HasUsableLleLibcExport(ComputePsNid(exportName), exportName);
}
if (string.Equals(value, "0", StringComparison.Ordinal))
{
return true;
@ -2140,6 +2187,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
error = $"guest context transfer slot is not writable at 0x{target.Rsp - sizeof(ulong):X16}";
return false;
}
ulong stackBottom = 0;
ulong stackTop = 0;
if (OperatingSystem.IsWindows() && !TryGetGuestStackBounds(target.Rsp, out stackBottom, out stackTop))
{
error = $"guest context transfer target rsp=0x{target.Rsp:X16} has no committed host stack mapping";
return false;
}
transferStub = GetOrCreateGuestContextTransferStub();
if (transferStub == 0)
@ -2176,6 +2230,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
frame[17] = target.Mxcsr == 0 ? 0x1F80u : target.Mxcsr;
frame[18] = target.FpuControlWord == 0 ? 0x037Fu : target.FpuControlWord;
frame[19] = target.RestoreFullFpuState ? 1u : 0u;
// The TEB stores NT_TIB.StackBase (the high address) at +0x08 and
// NT_TIB.StackLimit (the low address) at +0x10. Keep the frame in that
// same order so the transfer stub can store the slots straight through.
frame[20] = stackTop;
frame[21] = stackBottom;
return true;
}
@ -2243,6 +2302,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
Emit(0x49); Emit(0x89); Emit(0xC3); // mov r11, rax
EmitFiberStackBounds(code, ref offset);
// A new >=3.50 fiber receives the SDK-defined MXCSR verbatim. A
// resumed fiber follows _sceFiberLongJmp: preserve status bits 0-5
// while restoring the saved control bits.
@ -2305,7 +2365,31 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
}
// A trampoline only encodes its import index, so a runtime module load that sets up every
// module's stubs again reuses them: the stubs then keep their bytes and are not rewritten
// under guest threads that may be executing them.
private readonly Dictionary<int, nint> _importHandlerTrampolineByIndex = new();
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
{
lock (ImportStubPatchGate)
{
if (_importHandlerTrampolineByIndex.TryGetValue(importIndex, out var existing))
{
return existing;
}
var created = CreateImportHandlerTrampolineCore(importIndex);
if (created != 0)
{
_importHandlerTrampolineByIndex[importIndex] = created;
}
return created;
}
}
private unsafe nint CreateImportHandlerTrampolineCore(int importIndex)
{
const uint stubSize = 1024u;
void* ptr = VirtualAlloc(null, stubSize, 12288u, 64u);
@ -2481,8 +2565,35 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
}
// Patches unprotect and re-protect whole pages: two threads patching code that shares a page
// (runtime module loads, lazy TLS site patches on guest threads) would restore the other's page
// to read-execute while it is still writing, so every patch runs under one gate.
private static readonly object ImportStubPatchGate = new();
private unsafe bool PatchImportStub(nint address, nint trampoline)
{
lock (ImportStubPatchGate)
{
return PatchImportStubLocked(address, trampoline);
}
}
private unsafe bool PatchImportStubLocked(nint address, nint trampoline)
{
// Every runtime module load sets up the stubs of all loaded modules again; a stub that
// already jumps to this trampoline needs no unprotect/write/restore cycle.
Span<byte> patch = stackalloc byte[16];
patch[0] = 0x48;
patch[1] = 0xB8;
System.Buffers.Binary.BinaryPrimitives.WriteInt64LittleEndian(patch[2..], trampoline);
patch[10] = 0xFF;
patch[11] = 0xE0;
patch[12..].Fill(0x90);
if (new ReadOnlySpan<byte>((void*)address, 16).SequenceEqual(patch))
{
return true;
}
uint flNewProtect = default(uint);
if (!VirtualProtect((void*)address, 16u, 64u, &flNewProtect))
{
@ -2519,6 +2630,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
}
_importHandlerTrampolines.Clear();
_importHandlerTrampolineByIndex.Clear();
}
private unsafe void CreateTlsHandler()
@ -3671,6 +3783,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
private static unsafe bool WriteTlsInstruction(nint address, ReadOnlySpan<byte> replacement)
{
// Guest threads patch their TLS sites as they first reach them; two sites on one page would
// otherwise race the unprotect/restore pair exactly like the import stubs.
lock (ImportStubPatchGate)
{
return WriteTlsInstructionLocked(address, replacement);
}
}
private static unsafe bool WriteTlsInstructionLocked(nint address, ReadOnlySpan<byte> replacement)
{
if (replacement.Length is < 1 or > 15 ||
VirtualQuery((void*)address, out var information, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
@ -3860,6 +3982,33 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
public bool SupportsGuestContextTransfer => true;
public bool TryGetGuestThreadStackBounds(
ulong threadHandle,
out ulong stackBase,
out ulong stackSize)
{
stackBase = 0;
stackSize = 0;
if (threadHandle == 0)
{
return false;
}
lock (_guestThreadGate)
{
if (!_guestThreads.TryGetValue(threadHandle, out var thread) ||
thread.StackBase == 0 ||
thread.StackSize == 0)
{
return false;
}
stackBase = thread.StackBase;
stackSize = thread.StackSize;
return true;
}
}
public void RegisterGuestThreadContext(ulong threadHandle, CpuContext context)
{
if (threadHandle == 0)
@ -5226,7 +5375,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return true;
}
private static bool TryWriteGuestExceptionContext(
internal static bool TryWriteGuestExceptionContext(
CpuContext context,
ulong address,
GuestCpuContinuation continuation,
@ -5237,11 +5386,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
BinaryPrimitives.WriteUInt64LittleEndian(bytes.AsSpan(offset, sizeof(ulong)), value);
var hasContinuation = continuation.Rip >= 65536 && continuation.Rsp != 0;
// Orbis ucontext_t has a 0x10-byte signal mask and 0x30 bytes of
// private fields before its amd64 mcontext. These offsets match the
// platform ABI used by libScePs5Util and Unity's Boehm GC. Supplying a
// bare mcontext here makes the collector miss live register roots.
const int mcontext = 0x40;
const int mcontext = 0;
Write64(mcontext + 0x08, hasContinuation ? continuation.Rdi : context[CpuRegister.Rdi]);
Write64(mcontext + 0x10, hasContinuation ? continuation.Rsi : context[CpuRegister.Rsi]);
Write64(mcontext + 0x18, hasContinuation ? continuation.Rdx : context[CpuRegister.Rdx]);
@ -5260,9 +5405,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
var rip = hasContinuation ? continuation.Rip : context.Rip;
var rsp = hasContinuation ? continuation.Rsp : context[CpuRegister.Rsp];
Write64(mcontext + 0xA0, rip);
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : 0);
Write64(mcontext + 0xB0, hasContinuation ? continuation.Rflags : context.Rflags);
Write64(mcontext + 0xB8, rsp);
Write64(mcontext + 0xC8, 0x480); // sizeof(Orbis mcontext_t)
Write64(mcontext + 0xF8, rsp);
Write64(mcontext + 0x440, hasContinuation ? continuation.FsBase : context.FsBase);
Write64(mcontext + 0x448, hasContinuation ? continuation.GsBase : context.GsBase);
return context.Memory.TryWrite(address, bytes);
@ -5318,6 +5464,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_readyGuestThreads.Clear();
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
_guestThreads.Clear();
_reusableGuestThreadRegions.Clear();
_externalGuestThreads.Clear();
_pendingGuestExceptions.Clear();
Volatile.Write(ref _pendingGuestExceptionCount, 0);
@ -5342,13 +5489,64 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
error = "creator context memory is not backed by IVirtualMemory";
return false;
}
if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, GuestThreadStackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error))
var requestedStackSize = request.StackSize;
if (requestedStackSize > GuestThreadStackSizeLimit)
{
return false;
requestedStackSize = GuestThreadStackSizeLimit;
}
if (!TryMapGuestThreadTlsRegion(virtualMemory, out var tlsBase, out error))
var stackSize = requestedStackSize == 0
? GuestThreadStackSize
: AlignUp(requestedStackSize + GuestThreadStackHeadroom, 0x1000UL);
if (stackSize < GuestThreadStackSize)
{
return false;
stackSize = GuestThreadStackSize;
}
ulong stackBase = 0;
ulong tlsBase = 0;
lock (_guestThreadGate)
{
var count = _reusableGuestThreadRegions.Count;
for (var i = 0; i < count; i++)
{
var region = _reusableGuestThreadRegions.Dequeue();
if (stackBase == 0 && ReferenceEquals(region.Memory, virtualMemory) && region.StackSize >= stackSize)
{
stackBase = region.StackBase;
stackSize = region.StackSize;
tlsBase = region.TlsBase;
}
else
{
_reusableGuestThreadRegions.Enqueue(region);
}
}
}
if (stackBase == 0)
{
if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, stackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out stackBase, out error))
{
return false;
}
if (!TryMapGuestThreadTlsRegion(virtualMemory, out tlsBase, out error))
{
return false;
}
}
else
{
Span<byte> zero = stackalloc byte[4096];
zero.Clear();
var start = tlsBase - GuestThreadTlsPrefixSize;
for (ulong offset = 0; offset < GuestThreadTlsPrefixSize + GuestThreadTlsSize; offset += (ulong)zero.Length)
{
if (!virtualMemory.TryWrite(start + offset, zero[..(int)Math.Min((ulong)zero.Length, GuestThreadTlsPrefixSize + GuestThreadTlsSize - offset)]))
{
error = "failed to reset reused guest TLS";
return false;
}
}
}
var trackedMemory = new TrackedCpuMemory(virtualMemory);
@ -5359,7 +5557,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
FsBase = tlsBase,
GsBase = tlsBase,
};
context[CpuRegister.Rsp] = stackBase + GuestThreadStackSize - sizeof(ulong);
context[CpuRegister.Rsp] = stackBase + stackSize - sizeof(ulong);
context[CpuRegister.Rdi] = request.Argument;
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = 0;
@ -5382,7 +5580,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
AffinityMask = request.AffinityMask,
Context = context,
StackBase = stackBase,
StackSize = GuestThreadStackSize,
StackSize = stackSize,
State = GuestThreadRunState.Ready,
};
error = null;
@ -5548,6 +5746,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
var hostAffinityMask = MapGuestThreadAffinity(guestAffinityMask);
if (hostAffinityMask == 0)
{
SharpEmu.HLE.Host.HostLaneReservation.ApplyToGuestThread();
return;
}
@ -5561,7 +5760,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private static ulong MapGuestThreadAffinity(ulong guestAffinityMask)
{
if (guestAffinityMask == 0 || guestAffinityMask == ulong.MaxValue)
if (guestAffinityMask == 0 || guestAffinityMask == ulong.MaxValue || !GuestAffinityEnabled)
{
return 0;
}
@ -5621,9 +5820,20 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
/// <summary>
/// Host lanes kept away from guest threads. Measured on a 16-lane host with
/// Demon's Souls: reserving 0/4/6/8 lanes gave 6.08/6.78/7.20/5.62 fps, so
/// the useful range is a bit over a third of the machine — too few and the
/// Guest affinity is not applied to host threads unless
/// SHARPEMU_GUEST_AFFINITY=1. A console title pins one spinning worker per
/// dedicated core; on a shared host, pinning traps its renderer and any lock
/// holder on a lane next to a busy spinner of equal priority, where it waits
/// out whole scheduler quanta. Measured with Demon's Souls on a 16-lane host:
/// intro 10.6 → 44.7 fps and menus 11.6 → 47.5 fps with pinning off.
/// </summary>
private static readonly bool GuestAffinityEnabled =
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_AFFINITY") == "1";
/// <summary>
/// Host lanes kept away from pinned guest threads. Measured on a 16-lane host
/// with Demon's Souls: reserving 0/4/6/8 lanes gave 6.08/6.78/7.20/5.62 fps,
/// so the useful range is a bit over a third of the machine — too few and the
/// emulator is crowded out, too many and the guest cannot make progress.
/// </summary>
private static readonly int EmulatorReservedLanes =
@ -5813,6 +6023,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
pendingAfterExecutorRelease = pending;
}
if (thread.State == GuestThreadRunState.Exited &&
TryGetVirtualMemory(thread.Context, out var memory))
{
_reusableGuestThreadRegions.Enqueue((memory, thread.StackBase, thread.StackSize, thread.Context.FsBase));
thread.ExecutionRunner?.RequestStop();
}
}
if (pendingAfterExecutorRelease is { } pendingException &&
!TryRaiseGuestException(
@ -5971,6 +6187,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_activeGuestThreadYieldRequested = false;
_activeGuestThreadYieldReason = null;
BindTlsBase(context);
RouteGuestAccessFaultsToSignals();
byte* ptr2 = (byte*)ptr;
// Rosetta does not reliably permit a generated x86 thunk to write data
// in the same page from which it is currently executing, even when the
@ -6197,6 +6414,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_activeGuestThreadYieldRequested = false;
_activeGuestThreadYieldReason = null;
BindTlsBase(context);
RouteGuestAccessFaultsToSignals();
byte* ptr2 = (byte*)ptr;
ulong hostRspSlot = (ulong)hostRspStorage;
var emitter = new NativeCodeEmitter(ptr2);
@ -6427,6 +6645,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
Console.Error.WriteLine($"[LOADER][INFO] ExecuteEntry starting at 0x{entryPoint:X16}");
Console.Error.WriteLine($"[LOADER][INFO] RSP=0x{context[CpuRegister.Rsp]:X16}, RDI=0x{context[CpuRegister.Rdi]:X16}");
SharpEmu.HLE.Host.HostLaneReservation.ApplyToGuestThread();
ulong num = context[CpuRegister.Rsp];
if (num == 0)
{
@ -6469,6 +6688,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_activeGuestThreadYieldRequested = false;
_activeGuestThreadYieldReason = null;
BindTlsBase(context);
RouteGuestAccessFaultsToSignals();
byte* ptr2 = (byte*)ptr;
ulong num2 = (ulong)hostRspStorage;
int num3 = 0;
@ -6774,6 +6994,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
MarkExecutionProgress();
continue;
}
if (AreAllLiveGuestThreadsExpectedlyBlocked())
{
Console.Error.WriteLine(
$"[LOADER][WARN] No import progress for {stallWatchdogSeconds}s, but every live guest thread is parked in an expected blocking primitive; continuing.");
LogStallWatchdogSnapshot();
Console.Error.Flush();
MarkExecutionProgress();
continue;
}
if (Interlocked.Exchange(ref _stallWatchdogTriggered, 1) != 0)
{
continue;
@ -6841,6 +7070,41 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false;
}
private bool AreAllLiveGuestThreadsExpectedlyBlocked()
{
using (LockGate("AreAllLiveGuestThreadsExpectedlyBlocked"))
{
var sawBlockedThread = false;
foreach (var thread in _guestThreads.Values)
{
if (thread.State is GuestThreadRunState.Exited or GuestThreadRunState.Faulted)
{
continue;
}
if (thread.State != GuestThreadRunState.Blocked)
{
return false;
}
if (Volatile.Read(ref thread.LastImportNid) is not (
"Op8TBGY5KHg" or // pthread_cond_wait
"27bAgiJmOh0" or // pthread_cond_timedwait
"WKAXJ4XBPQ4" or // scePthreadCondWait
"BmMjYxmew1w" or // scePthreadCondTimedwait
"fzyMKs9kim0" or // sceKernelWaitEqueue
"Zxa0VhQVTsk")) // sceKernelWaitSema
{
return false;
}
sawBlockedThread = true;
}
return sawBlockedThread;
}
}
private void StopStallWatchdog()
{
_stallWatchdogStop = true;

View File

@ -53,6 +53,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
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)

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

@ -371,6 +371,33 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true;
}
// Aligned starts of the free host ranges in [low, highExclusive) that can hold size
// bytes, from the host's own region map. The ranges are free when listed; another
// thread may take one before the caller allocates it.
public IEnumerable<ulong> EnumerateFreeHostRanges(ulong low, ulong highExclusive, ulong size, ulong alignment)
{
var address = low;
while (address < highExclusive && _hostMemory.Query(address, out var info) && info.RegionSize != 0)
{
var regionEnd = info.BaseAddress + info.RegionSize;
if (regionEnd <= address)
{
yield break;
}
if (info.State == HostRegionState.Free)
{
var start = (address + alignment - 1) & ~(alignment - 1);
if (start >= address && start + size >= start && start + size <= Math.Min(regionEnd, highExclusive))
{
yield return start;
}
}
address = regionEnd;
}
}
public string DescribeAddressForDiagnostics(ulong address)
{
if (!_hostMemory.Query(address, out var info))
@ -1054,6 +1081,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public bool IsBackedRange(ulong address, ulong size)
{
// A backed range confirmed from the view snapshot needs no lock; a miss (partly
// unbacked, or a view being remapped) takes the locked path.
if (_backedSpace is { } backed && !_disposed && backed.IsBackedWithoutLock(address, size))
{
return true;
}
_gate.EnterReadLock();
try
{
@ -1140,6 +1174,18 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
public object? BackingAliasSnapshot => _backedSpace?.AliasSnapshot;
public bool TryEnterBackingAliasAccess() => !_disposed && _backedSpace is { } backed && backed.TryEnterAliasAccess();
public void ExitBackingAliasAccess() => _backedSpace?.ExitAliasAccess();
public bool TryResolveBackingAlias(ulong address, ulong size, out ulong alias)
{
alias = 0;
return !_disposed && _backedSpace is { } backed && backed.TryResolveAlias(address, size, out alias);
}
private bool HasBackingOwner() => !_disposed && _backedSpace != null;
// A span may cross adjacent view records; the owner validates full coverage.
@ -1276,6 +1322,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true;
}
// POSIX cannot query foreign host mappings. The free ranges were
// reserved at startup, so a failed candidate means the gap is taken.
if (!OperatingSystem.IsWindows() && reservedCandidate != 0)
{
address = reservedCandidate;
return true;
}
start = candidate + GuestMemoryLayout.GuestPage;
if (OperatingSystem.IsWindows() && _hostMemory.Query(candidate, out var info) &&
info.BaseAddress <= candidate && info.BaseAddress < limit &&
@ -1776,6 +1830,15 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
// Backed guest memory is the shared backing itself, and the backing's view table
// is readable without a lock; only other memory needs _gate to find its region.
// Descriptor evaluation alone reads guest words ~40 at a time per draw here.
if (!destination.IsEmpty && _backedSpace is { } backed && !_disposed &&
backed.TryReadSingleView(virtualAddress, destination))
{
return true;
}
var requiresExclusiveAccess = false;
_gate.EnterReadLock();
try
@ -1843,6 +1906,85 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
}
public bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match)
{
match = 0;
_gate.EnterReadLock();
try
{
// Scan page by page in place, with the same region and protection
// checks TryRead applies before its copy. Anything TryRead would
// handle specially (backed views, lazy commit, protection changes)
// is left to the caller's copying path.
ulong offset = 0;
while (offset < maxLength)
{
var current = address + offset;
if (current < address)
{
return false;
}
var length = Math.Min(PageSize - (current & (PageSize - 1)), maxLength - offset);
var region = FindRegion(current, length);
if (region is null ||
region.IsReservedOnly ||
IsBackedSpan(region, current, length) ||
!TryResolveRegionOffset(current, length, region, out var regionOffset))
{
return false;
}
var hostAddress = region.VirtualAddress + regionOffset;
if (!CanReadWithoutProtectionChange(hostAddress, length, region))
{
return false;
}
var bytes = new ReadOnlySpan<byte>((void*)hostAddress, (int)length);
if (!findLast)
{
// One pass that stops at the needle or the terminator,
// whichever comes first.
var stop = bytes.IndexOfAny(needle, (byte)0);
if (stop >= 0)
{
match = bytes[stop] == needle ? current + (ulong)stop : 0;
return true;
}
offset += length;
continue;
}
var nulIndex = bytes.IndexOf((byte)0);
var searched = nulIndex >= 0 ? bytes[..(nulIndex + 1)] : bytes;
var found = findLast ? searched.LastIndexOf(needle) : searched.IndexOf(needle);
if (found >= 0)
{
match = current + (ulong)found;
if (!findLast)
{
return true;
}
}
if (nulIndex >= 0)
{
return true;
}
offset += length;
}
return true;
}
finally
{
_gate.ExitReadLock();
}
}
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) =>
TryCompare(virtualAddress, expected, out var equal) && equal;

View File

@ -159,6 +159,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
FiberExports.ResetRuntimeState();
Http2Exports.ResetRuntimeState();
NpAuthExports.ResetRuntimeState();
NpUniversalDataSystemExports.ResetRuntimeState();
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
@ -667,14 +668,15 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
var moduleDirectories = new[]
{
// A linked PRX can be beside eboot.bin. Load only the app-root modules
// named by the main image at boot. Map the other app-root modules so
// sceKernelLoadStartModule can start them later.
(Path: ebootDirectory, StartAtBoot: true, LinkedOnly: true, SearchOption: SearchOption.TopDirectoryOnly),
(Path: ebootDirectory, StartAtBoot: false, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
// A linked PRX can be beside eboot.bin. Load only the app-root modules
// named by the main image at boot, after the normal module directories
// they may depend on. Map the other app-root modules so
// sceKernelLoadStartModule can start them later.
(Path: ebootDirectory, StartAtBoot: true, LinkedOnly: true, SearchOption: SearchOption.TopDirectoryOnly),
(Path: ebootDirectory, StartAtBoot: false, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
// Unity native plugins are loaded later through sceKernelLoadStartModule. Map
// them up front so the HLE loader can return a real module handle and dlsym
// can resolve their exports, but defer DT_INIT until the guest requests them.

View File

@ -82,12 +82,12 @@ public sealed class GuiSettings
public bool OverlayEnabled { get; set; } = true;
public string OverlayCorner { get; set; } = "TopRight";
public string OverlayMode { get; set; } = "Full";
public string OverlayMode { get; set; } = "TitleBar";
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
public List<string> EnvironmentToggles { get; set; } = ["SHARPEMU_WRITABLE_APP0"];
public List<string> EnvironmentToggles { get; set; } = ["SHARPEMU_WRITABLE_APP0", "SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"];
public string BinkPlaybackMode { get; set; } = "Host";
public string BinkPlaybackMode { get; set; } = "Guest";
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
public double RenderResolutionScale { get; set; } = 1.0;
@ -131,7 +131,7 @@ public sealed class GuiSettings
settings.GameFolders = FilterNullOrEmpty(settings.GameFolders);
settings.ExcludedGames = FilterNullOrEmpty(settings.ExcludedGames);
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles ?? ["SHARPEMU_WRITABLE_APP0"]);
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles ?? ["SHARPEMU_WRITABLE_APP0", "SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"]);
settings.LogLevel ??= "Info";
settings.Language ??= "en";
var legacyProfile = settings.EnvironmentToggles
@ -186,9 +186,9 @@ public sealed class GuiSettings
settings.Resolution = NormalizeResolution(settings.Resolution);
settings.ScalingMode = NormalizeChoice(settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
settings.HdrMode = NormalizeChoice(settings.HdrMode, "Auto", "On", "Off");
settings.BinkPlaybackMode = NormalizeChoice(settings.BinkPlaybackMode, "Host", "Guest", "Skip");
settings.BinkPlaybackMode = NormalizeChoice(settings.BinkPlaybackMode, "Guest", "Host", "Skip");
settings.OverlayCorner = NormalizeChoice(settings.OverlayCorner, "TopRight", "TopLeft", "BottomRight", "BottomLeft");
settings.OverlayMode = NormalizeChoice(settings.OverlayMode, "Full", "Minimal", "TitleBar");
settings.OverlayMode = NormalizeChoice(settings.OverlayMode, "TitleBar", "Full", "Minimal");
settings.DisplayIndex = Math.Max(0, settings.DisplayIndex);
settings.RefreshRate = Math.Clamp(settings.RefreshRate, 0, 1000);
settings.EmbeddedConsoleHeight = NormalizeConsoleSize(settings.EmbeddedConsoleHeight, 240, 120);

View File

@ -236,7 +236,7 @@
"Options.Hdr.Desc": "استخدام HDR عندما تدعمه الشاشة المحددة وواجهة الرسومات. يعود الوضع التلقائي إلى SDR.",
"Options.Hdr.Auto": "تلقائي",
"Options.Env.BinkPlayback.Label": "تشغيل فيديو Bink",
"Options.Env.BinkPlayback.Desc": "يقوم Host بتشغيل فيديوهات Bink عبر جسر FFmpeg، لكن قد تظهر مشكلات في بعض الألعاب.\nيستخدم Guest وحدة فك الترميز الخاصة بمحرك اللعبة، والتي يدعمها SharpEmu حاليًا بشكل جزئي فقط.\nيتخطى Skip فيديوهات Bink، مما قد يجعل التطوير أسهل.",
"Options.Env.BinkPlayback.Desc": "يقوم Host بتشغيل فيديوهات Bink عبر جسر FFmpeg، لكن قد تظهر مشكلات في بعض الألعاب.\nيستخدم Guest وحدة فك الترميز الخاصة بمحرك اللعبة.\nيتخطى Skip فيديوهات Bink، مما قد يجعل التطوير أسهل.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "تخطي"

View File

@ -91,7 +91,7 @@
"Options.Hdr.Desc": "Seçilmiş ekran və qrafik alt sistem dəstəklədikdə HDR istifadə edin. Avtomatik rejimdə dəstək yoxdursa, SDR istifadə olunur.",
"Options.Hdr.Auto": "Avtomatik",
"Options.Env.BinkPlayback.Label": "Bink video oynadılması",
"Options.Env.BinkPlayback.Desc": "Host Bink videolarını FFmpeg körpüsü vasitəsilə oynadır, lakin bəzi oyunlarda problemlər yarana bilər.\nGuest oyunun öz dekoderindən istifadə edir; SharpEmu hazırda bunu yalnız qismən dəstəkləyir.\nSkip Bink videolarını ötürür və bu, inkişaf prosesini asanlaşdıra bilər.",
"Options.Env.BinkPlayback.Desc": "Host Bink videolarını FFmpeg körpüsü vasitəsilə oynadır, lakin bəzi oyunlarda problemlər yarana bilər.\nGuest oyunun öz dekoderindən istifadə edir.\nSkip Bink videolarını ötürür və bu, inkişaf prosesini asanlaşdıra bilər.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Skip",

View File

@ -237,7 +237,7 @@
"Options.Hdr.Desc": "Usa HDR quando a tela selecionada e o backend gráfico oferecem suporte. O modo automático retorna ao SDR.",
"Options.Hdr.Auto": "Automático",
"Options.Env.BinkPlayback.Label": "Reprodução de vídeo Bink",
"Options.Env.BinkPlayback.Desc": "Host reproduz vídeos Bink pela ponte FFmpeg, mas alguns jogos podem apresentar problemas.\nGuest usa o decodificador do próprio motor do jogo, que o SharpEmu atualmente suporta apenas parcialmente.\nSkip ignora os vídeos Bink, o que pode facilitar o desenvolvimento.",
"Options.Env.BinkPlayback.Desc": "Host reproduz vídeos Bink pela ponte FFmpeg, mas alguns jogos podem apresentar problemas.\nGuest usa o decodificador do próprio motor do jogo.\nSkip ignora os vídeos Bink, o que pode facilitar o desenvolvimento.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Ignorar"

View File

@ -236,7 +236,7 @@
"Options.Hdr.Desc": "HDR verwenden, wenn die ausgewählte Anzeige und das Grafik-Backend es unterstützen. Automatisch fällt auf SDR zurück.",
"Options.Hdr.Auto": "Automatisch",
"Options.Env.BinkPlayback.Label": "Bink-Videowiedergabe",
"Options.Env.BinkPlayback.Desc": "Host spielt Bink-Videos über die FFmpeg-Brücke ab, bei einigen Spielen können jedoch Probleme auftreten.\nGuest verwendet den spieleigenen Decoder, den SharpEmu derzeit nur teilweise unterstützt.\nSkip überspringt Bink-Videos, was die Entwicklung erleichtern kann.",
"Options.Env.BinkPlayback.Desc": "Host spielt Bink-Videos über die FFmpeg-Brücke ab, bei einigen Spielen können jedoch Probleme auftreten.\nGuest verwendet den spieleigenen Decoder.\nSkip überspringt Bink-Videos, was die Entwicklung erleichtern kann.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Überspringen"

View File

@ -236,7 +236,7 @@
"Options.Hdr.Desc": "Brug HDR, når den valgte skærm og grafik-backend understøtter det. Automatisk falder tilbage til SDR.",
"Options.Hdr.Auto": "Automatisk",
"Options.Env.BinkPlayback.Label": "Bink-videoafspilning",
"Options.Env.BinkPlayback.Desc": "Host afspiller Bink-videoer via FFmpeg-broen, men nogle spil kan opleve problemer.\nGuest bruger spilmotorens egen dekoder, som SharpEmu i øjeblikket kun understøtter delvist.\nSkip springer Bink-videoer over, hvilket kan gøre udviklingen nemmere.",
"Options.Env.BinkPlayback.Desc": "Host afspiller Bink-videoer via FFmpeg-broen, men nogle spil kan opleve problemer.\nGuest bruger spilmotorens egen dekoder.\nSkip springer Bink-videoer over, hvilket kan gøre udviklingen nemmere.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Spring over"

View File

@ -102,7 +102,7 @@
"Options.Hdr.Desc": "Kasuta HDR-i, kui valitud kuvar ja graafikaliides seda toetavad. Automaatrežiim lülitub tagasi SDR-ile.",
"Options.Hdr.Auto": "Automaatne",
"Options.Env.BinkPlayback.Label": "Bink-video taasesitus",
"Options.Env.BinkPlayback.Desc": "Host esitab Bink-videoid FFmpegi silla kaudu, kuid mõnes mängus võib esineda probleeme.\nGuest kasutab mängumootori enda dekoodrit, mida SharpEmu praegu ainult osaliselt toetab.\nSkip jätab Bink-videod vahele, mis võib arendust lihtsustada.",
"Options.Env.BinkPlayback.Desc": "Host esitab Bink-videoid FFmpegi silla kaudu, kuid mõnes mängus võib esineda probleeme.\nGuest kasutab mängumootori enda dekoodrit.\nSkip jätab Bink-videod vahele, mis võib arendust lihtsustada.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Jäta vahele",

View File

@ -102,7 +102,7 @@
"Options.Hdr.Desc": "Use HDR when the selected display and graphics backend support it. Auto falls back to SDR.",
"Options.Hdr.Auto": "Auto",
"Options.Env.BinkPlayback.Label": "Bink video playback",
"Options.Env.BinkPlayback.Desc": "Host plays Bink videos through the FFmpeg bridge, but some games may have issues.\nGuest uses the game engine's own decoder, which SharpEmu currently supports only partially.\nSkip skips Bink videos, which can make development easier.",
"Options.Env.BinkPlayback.Desc": "Host plays Bink videos through the FFmpeg bridge, but some games may have issues.\nGuest uses the game engine's own decoder.\nSkip skips Bink videos, which can make development easier.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Skip",

View File

@ -237,7 +237,7 @@
"Options.Hdr.Desc": "Usa HDR cuando la pantalla seleccionada y el backend gráfico sean compatibles. El modo automático vuelve a SDR.",
"Options.Hdr.Auto": "Automático",
"Options.Env.BinkPlayback.Label": "Reproducción de vídeo Bink",
"Options.Env.BinkPlayback.Desc": "Host reproduce los vídeos Bink mediante el puente FFmpeg, aunque algunos juegos pueden presentar problemas.\nGuest usa el decodificador propio del motor del juego, que SharpEmu actualmente solo admite de forma parcial.\nSkip omite los vídeos Bink, lo que puede facilitar el desarrollo.",
"Options.Env.BinkPlayback.Desc": "Host reproduce los vídeos Bink mediante el puente FFmpeg, aunque algunos juegos pueden presentar problemas.\nGuest usa el decodificador propio del motor del juego.\nSkip omite los vídeos Bink, lo que puede facilitar el desarrollo.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Omitir"

View File

@ -237,7 +237,7 @@
"Options.Hdr.Desc": "Utiliser le HDR lorsque l’écran sélectionné et le backend graphique le prennent en charge. Le mode automatique revient au SDR.",
"Options.Hdr.Auto": "Automatique",
"Options.Env.BinkPlayback.Label": "Lecture vidéo Bink",
"Options.Env.BinkPlayback.Desc": "Host lit les vidéos Bink via le pont FFmpeg, mais certains jeux peuvent rencontrer des problèmes.\nGuest utilise le décodeur propre au moteur du jeu, que SharpEmu ne prend actuellement en charge que partiellement.\nSkip ignore les vidéos Bink, ce qui peut faciliter le développement.",
"Options.Env.BinkPlayback.Desc": "Host lit les vidéos Bink via le pont FFmpeg, mais certains jeux peuvent rencontrer des problèmes.\nGuest utilise le décodeur propre au moteur du jeu.\nSkip ignore les vidéos Bink, ce qui peut faciliter le développement.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Ignorer"

View File

@ -237,7 +237,7 @@
"Options.Hdr.Desc": "HDR használata, ha a kiválasztott kijelző és grafikus backend támogatja. Az automatikus mód SDR-re vált vissza.",
"Options.Hdr.Auto": "Automatikus",
"Options.Env.BinkPlayback.Label": "Bink videólejátszás",
"Options.Env.BinkPlayback.Desc": "A Host az FFmpeg hídon keresztül játssza le a Bink videókat, de egyes játékoknál problémák jelentkezhetnek.\nA Guest a játékmotor saját dekóderét használja, amelyet a SharpEmu jelenleg csak részben támogat.\nA Skip kihagyja a Bink videókat, ami megkönnyítheti a fejlesztést.",
"Options.Env.BinkPlayback.Desc": "A Host az FFmpeg hídon keresztül játssza le a Bink videókat, de egyes játékoknál problémák jelentkezhetnek.\nA Guest a játékmotor saját dekóderét használja.\nA Skip kihagyja a Bink videókat, ami megkönnyítheti a fejlesztést.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Kihagyás"

View File

@ -241,7 +241,7 @@
"Options.Hdr.Desc": "Usa HDR quando lo schermo selezionato e il backend grafico lo supportano. La modalità automatica torna a SDR.",
"Options.Hdr.Auto": "Automatico",
"Options.Env.BinkPlayback.Label": "Riproduzione video Bink",
"Options.Env.BinkPlayback.Desc": "Host riproduce i video Bink tramite il bridge FFmpeg, ma alcuni giochi potrebbero avere problemi.\nGuest usa il decoder del motore di gioco, che SharpEmu attualmente supporta solo in parte.\nSkip salta i video Bink, facilitando lo sviluppo.",
"Options.Env.BinkPlayback.Desc": "Host riproduce i video Bink tramite il bridge FFmpeg, ma alcuni giochi potrebbero avere problemi.\nGuest usa il decoder del motore di gioco.\nSkip salta i video Bink, facilitando lo sviluppo.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Salta"

View File

@ -236,7 +236,7 @@
"Options.Hdr.Desc": "選択したディスプレイとグラフィックスバックエンドが対応している場合にHDRを使用します。自動ではSDRにフォールバックします。",
"Options.Hdr.Auto": "自動",
"Options.Env.BinkPlayback.Label": "Bink 動画再生",
"Options.Env.BinkPlayback.Desc": "Host は FFmpeg ブリッジ経由で Bink 動画を再生しますが、一部のゲームで問題が発生する場合があります。\nGuest はゲームエンジン独自のデコーダーを使用しますが、SharpEmu は現在これを部分的にしかサポートしていません。\nSkip は Bink 動画をスキップし、開発を容易にする場合があります。",
"Options.Env.BinkPlayback.Desc": "Host は FFmpeg ブリッジ経由で Bink 動画を再生しますが、一部のゲームで問題が発生する場合があります。\nGuest はゲームエンジン独自のデコーダーを使用します。\nSkip は Bink 動画をスキップし、開発を容易にする場合があります。",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "スキップ"

View File

@ -236,7 +236,7 @@
"Options.Hdr.Desc": "선택한 디스플레이와 그래픽 백엔드가 지원하는 경우 HDR을 사용합니다. 자동 모드는 SDR로 대체됩니다.",
"Options.Hdr.Auto": "자동",
"Options.Env.BinkPlayback.Label": "Bink 비디오 재생",
"Options.Env.BinkPlayback.Desc": "Host는 FFmpeg 브리지를 통해 Bink 비디오를 재생하지만 일부 게임에서 문제가 발생할 수 있습니다.\nGuest는 게임 엔진 자체 디코더를 사용하며, SharpEmu는 현재 이를 부분적으로만 지원합니다.\nSkip은 Bink 비디오를 건너뛰어 개발을 더 쉽게 할 수 있습니다.",
"Options.Env.BinkPlayback.Desc": "Host는 FFmpeg 브리지를 통해 Bink 비디오를 재생하지만 일부 게임에서 문제가 발생할 수 있습니다.\nGuest는 게임 엔진 자체 디코더를 사용합니다.\nSkip은 Bink 비디오를 건너뛰어 개발을 더 쉽게 할 수 있습니다.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "건너뛰기"

View File

@ -236,7 +236,7 @@
"Options.Hdr.Desc": "Gebruik HDR wanneer het geselecteerde beeldscherm en de grafische backend dit ondersteunen. Automatisch valt terug op SDR.",
"Options.Hdr.Auto": "Automatisch",
"Options.Env.BinkPlayback.Label": "Bink-video afspelen",
"Options.Env.BinkPlayback.Desc": "Host speelt Bink-video's af via de FFmpeg-brug, maar sommige games kunnen problemen hebben.\nGuest gebruikt de eigen decoder van de game-engine, die SharpEmu momenteel slechts gedeeltelijk ondersteunt.\nSkip slaat Bink-video's over, wat de ontwikkeling kan vergemakkelijken.",
"Options.Env.BinkPlayback.Desc": "Host speelt Bink-video's af via de FFmpeg-brug, maar sommige games kunnen problemen hebben.\nGuest gebruikt de eigen decoder van de game-engine.\nSkip slaat Bink-video's over, wat de ontwikkeling kan vergemakkelijken.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Overslaan"

View File

@ -102,7 +102,7 @@
"Options.Hdr.Desc": "Używaj HDR, gdy wybrany wyświetlacz i backend graficzny to obsługują. Automatyczny przełącza na SDR.",
"Options.Hdr.Auto": "Automatyczny",
"Options.Env.BinkPlayback.Label": "Odtwarzanie wideo Bink",
"Options.Env.BinkPlayback.Desc": "Host odtwarza filmy Bink przez most FFmpeg, ale w niektórych grach mogą wystąpić problemy.\nGuest używa dekodera silnika gry, który SharpEmu obecnie obsługuje tylko częściowo.\nSkip pomija filmy Bink, co może ułatwić rozwój.",
"Options.Env.BinkPlayback.Desc": "Host odtwarza filmy Bink przez most FFmpeg, ale w niektórych grach mogą wystąpić problemy.\nGuest używa dekodera silnika gry.\nSkip pomija filmy Bink, co może ułatwić rozwój.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Pomiń",

View File

@ -237,7 +237,7 @@
"Options.Hdr.Desc": "Utiliza HDR quando o ecrã selecionado e o backend gráfico o suportam. O modo automático regressa a SDR.",
"Options.Hdr.Auto": "Automático",
"Options.Env.BinkPlayback.Label": "Reprodução de vídeo Bink",
"Options.Env.BinkPlayback.Desc": "Host reproduz vídeos Bink pela ponte FFmpeg, mas alguns jogos podem apresentar problemas.\nGuest usa o descodificador do próprio motor do jogo, que o SharpEmu atualmente suporta apenas parcialmente.\nSkip ignora os vídeos Bink, o que pode facilitar o desenvolvimento.",
"Options.Env.BinkPlayback.Desc": "Host reproduz vídeos Bink pela ponte FFmpeg, mas alguns jogos podem apresentar problemas.\nGuest usa o descodificador do próprio motor do jogo.\nSkip ignora os vídeos Bink, o que pode facilitar o desenvolvimento.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Ignorar"

View File

@ -98,7 +98,7 @@
"Options.Hdr.Desc": "Использовать HDR, если выбранный монитор и графический бэкенд его поддерживают. Автоматический режим при необходимости переключается на SDR.",
"Options.Hdr.Auto": "Авто",
"Options.Env.BinkPlayback.Label": "Воспроизведение видео Bink",
"Options.Env.BinkPlayback.Desc": "Host воспроизводит видео Bink через мост FFmpeg, однако в некоторых играх могут возникать проблемы.\nGuest использует собственный декодер игрового движка, который SharpEmu сейчас поддерживает лишь частично.\nSkip пропускает видео Bink, что может упростить разработку.",
"Options.Env.BinkPlayback.Desc": "Host воспроизводит видео Bink через мост FFmpeg, однако в некоторых играх могут возникать проблемы.\nGuest использует собственный декодер игрового движка.\nSkip пропускает видео Bink, что может упростить разработку.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Пропустить",

View File

@ -240,7 +240,7 @@
"Options.Scaling.Integer": "Tam sayı",
"Options.Hdr.Auto": "Otomatik",
"Options.Env.BinkPlayback.Label": "Bink video oynatma",
"Options.Env.BinkPlayback.Desc": "Host, Bink videolarını FFmpeg köprüsüyle oynatır ancak bazı oyunlarda sorunlarla karşılaşabilir.\nGuest, oyun motorunun kendi kod çözücüsünü kullanır ancak SharpEmu bunu şu anda yalnızca kısmen desteklemektedir.\nSkip, Bink videolarını atlar; bu, geliştirmeyi kolaylaştırabilir.",
"Options.Env.BinkPlayback.Desc": "Host, Bink videolarını FFmpeg köprüsüyle oynatır ancak bazı oyunlarda sorunlarla karşılaşabilir.\nGuest, oyun motorunun kendi kod çözücüsünü kullanır.\nSkip, Bink videolarını atlar; bu, geliştirmeyi kolaylaştırabilir.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Atla"

View File

@ -218,7 +218,7 @@ public partial class MainWindow
"Fit");
GameVSyncToggle.IsChecked = effective.VSync;
GameOverlayEnabledToggle.IsChecked = effective.OverlayEnabled;
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "Full");
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "TitleBar");
GameOverlayCornerBox.SelectedItem = FindChoice(_overlayCornerChoices, effective.OverlayCorner, "TopRight");
GameHdrModeBox.SelectedItem = FindChoice(
_hdrModeChoices,
@ -285,7 +285,7 @@ public partial class MainWindow
VSync = GameVSyncToggle.IsChecked == true,
HdrMode = SelectedComboText(GameHdrModeBox, "Auto"),
OverlayEnabled = GameOverlayEnabledToggle.IsChecked == true,
OverlayMode = SelectedComboText(GameOverlayModeBox, "Full"),
OverlayMode = SelectedComboText(GameOverlayModeBox, "TitleBar"),
OverlayCorner = SelectedComboText(GameOverlayCornerBox, "TopRight"),
EnvironmentToggles = BuildGameEnvironmentEntries(),
CustomEnvironmentVariables = PerGameSettings.Load(_gameSettingsTitleId)?.CustomEnvironmentVariables,

View File

@ -327,7 +327,7 @@ public partial class MainWindow : Window
VSyncToggle.IsCheckedChanged += (_, _) => _settings.VSync = VSyncToggle.IsChecked == true;
HdrModeBox.SelectionChanged += (_, _) => _settings.HdrMode = SelectedComboText(HdrModeBox, "Auto");
OverlayEnabledToggle.IsCheckedChanged += (_, _) => _settings.OverlayEnabled = OverlayEnabledToggle.IsChecked == true;
OverlayModeBox.SelectionChanged += (_, _) => _settings.OverlayMode = SelectedComboText(OverlayModeBox, "Full");
OverlayModeBox.SelectionChanged += (_, _) => _settings.OverlayMode = SelectedComboText(OverlayModeBox, "TitleBar");
OverlayCornerBox.SelectionChanged += (_, _) => _settings.OverlayCorner = SelectedComboText(OverlayCornerBox, "TopRight");
UpdateButton.Click += async (_, _) => await OnUpdateButtonAsync();
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
@ -344,7 +344,7 @@ public partial class MainWindow : Window
EnvWritableApp0Toggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_WRITABLE_APP0", EnvWritableApp0Toggle.IsChecked == true);
BinkPlaybackBox.SelectionChanged += (_, _) =>
_settings.BinkPlaybackMode = SelectedComboText(BinkPlaybackBox, "Host");
_settings.BinkPlaybackMode = SelectedComboText(BinkPlaybackBox, "Guest");
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
@ -2545,6 +2545,11 @@ public partial class MainWindow : Window
_appliedEnvironmentVariables.Add(StrictComputeSettings.VariableName);
_appliedEnvironmentVariables.Add(DefaultProfileEnvironmentName);
// An absent switch now enables the guard opt-out by default; send 0 for a disabled GUI toggle.
Environment.SetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD",
effective.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD") ? "1" : "0");
_appliedEnvironmentVariables.Add("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
Environment.SetEnvironmentVariable(
BinkModeEnvironmentName,
_settings.BinkPlaybackMode switch

View File

@ -0,0 +1,38 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.GpuMemory;
// One bit per tracked block: set while the block has at least one CPU-dirty page.
// Regions update their bit under their own lock whenever their CPU-dirty mask
// changes, so a clear bit lets a reader skip the block without taking any lock.
// A reader that must observe a CPU write is ordered after it by the guest's own
// submission, which happens after the write fault published the bit.
public sealed class CpuDirtySummary
{
private const int WordBits = 64;
private readonly long[] _words = new long[(TrackerLayout.BlockCount + WordBits - 1) / WordBits];
private long _epoch;
public long Epoch => Volatile.Read(ref _epoch);
public void NoteDirtied() => Interlocked.Increment(ref _epoch);
public void Set(ulong block, bool dirty)
{
var bit = 1L << (int)(block % WordBits);
ref var word = ref _words[block / WordBits];
if (dirty)
{
Interlocked.Or(ref word, bit);
}
else
{
Interlocked.And(ref word, ~bit);
}
}
public bool IsDirty(ulong block) => (Volatile.Read(ref _words[block / WordBits]) & (1L << (int)(block % WordBits))) != 0;
public ulong Word(int index) => (ulong)Volatile.Read(ref _words[index]);
}

View File

@ -30,6 +30,9 @@ public sealed class GuestGpuMemory : IDisposable
private readonly PageGuard _pages;
private readonly ReaderWriterLockSlim _spansLock = new();
private readonly SpanSet _spans = new();
private long _spanVersion;
public long SpanVersion => Interlocked.Read(ref _spanVersion);
private sealed record GpuAttachment(IGpuQueueRelay? Gpu, IGpuTickScheduler? Scheduler);
private readonly object _attachGate = new();
@ -174,6 +177,26 @@ public sealed class GuestGpuMemory : IDisposable
}
}
// Whether any GPU-registered span intersects the range. Memory outside every span holds no GPU
// cache entries and no tracked pages, so mapping changes there need nothing from the GPU worker.
public bool OverlapsRegistered(ulong address, ulong size)
{
if (!new GuestSpan(address, size).IsValid)
{
return false;
}
_spansLock.EnterReadLock();
try
{
return _spans.Overlaps(address, size);
}
finally
{
_spansLock.ExitReadLock();
}
}
// The host mapping takes the guest protection here; the views themselves are mapped read-write.
public void Register(ulong address, ulong size, GuestPageProtection protection)
{
@ -184,6 +207,7 @@ public sealed class GuestGpuMemory : IDisposable
try
{
_spans.Add(address, size);
Interlocked.Increment(ref _spanVersion);
}
finally
{
@ -256,6 +280,7 @@ public sealed class GuestGpuMemory : IDisposable
try
{
_spans.Remove(address, size);
Interlocked.Increment(ref _spanVersion);
}
finally
{

View File

@ -102,6 +102,9 @@ public static class GuestGpuMemoryHook
public static bool NoteProtected(ulong address, ulong size, GuestPageProtection protection) =>
_current != null && IsWithinGpuAddressSpace(address, size) && _current.NoteProtected(address, size, protection);
public static bool OverlapsRegistered(ulong address, ulong size) =>
_current != null && IsWithinGpuAddressSpace(address, size) && _current.OverlapsRegistered(address, size);
public static void NoteUnmapped(ulong address, ulong size)
{
if (_current == null || !IsWithinGpuAddressSpace(address, size))

View File

@ -15,13 +15,20 @@ public sealed class GuestPageTracker
private readonly PageGuard _pages;
private readonly TrackedRegion?[] _regions = new TrackedRegion?[TrackerLayout.BlockCount];
private readonly long[] _presentRegions = new long[(TrackerLayout.BlockCount + 63) / 64];
private readonly object _regionGate = new();
private readonly CpuDirtySummary _cpuDirtySummary = new();
public GuestPageTracker(PageGuard pages) => _pages = pages;
public bool HasCpuDirtyPages(ulong vaddr, ulong size)
{
RejectUploadCallbackReentry();
if (IsKnownCpuClean(vaddr, size))
{
return false;
}
return VisitRegions(vaddr, size, create: true, (region, offset, bytes) =>
{
using var _ = region.Lock.Hold();
@ -29,6 +36,34 @@ public sealed class GuestPageTracker
});
}
public bool MayHaveCpuDirtyPages(ulong vaddr, ulong size)
{
if (IsKnownCpuClean(vaddr, size))
{
return false;
}
var remaining = size;
var index = vaddr / BlockBytes;
var offset = vaddr % BlockBytes;
while (remaining != 0)
{
var bytes = Math.Min(BlockBytes - offset, remaining);
if (Volatile.Read(ref _regions[index]) is not { } region || region.IsModified(WriteOrigin.Cpu, offset, bytes))
{
return true;
}
remaining -= bytes;
offset = 0;
index++;
}
return false;
}
public long CpuDirtyEpoch => _cpuDirtySummary.Epoch;
public bool HasGpuDirtyPages(ulong vaddr, ulong size)
{
RejectUploadCallbackReentry();
@ -39,6 +74,31 @@ public sealed class GuestPageTracker
});
}
// Lock-free pre-check for hot readers: only the GPU queue thread sets GPU-dirty bits,
// so on that thread a clear answer is exact; a stale dirty answer merely sends the
// caller to HasGpuDirtyPages. Allocation- and lock-free.
public bool MayHaveGpuDirtyPages(ulong vaddr, ulong size)
{
ValidateRange(vaddr, size);
var remaining = size;
var index = vaddr / BlockBytes;
var offset = vaddr % BlockBytes;
while (remaining != 0)
{
var bytes = Math.Min(BlockBytes - offset, remaining);
if (Volatile.Read(ref _regions[index]) is { } region && region.IsModified(WriteOrigin.Gpu, offset, bytes))
{
return true;
}
remaining -= bytes;
offset = 0;
index++;
}
return false;
}
public bool IsCpuWriteHotRange(ulong vaddr, ulong size)
{
RejectUploadCallbackReentry();
@ -110,28 +170,52 @@ public sealed class GuestPageTracker
// Removes protection from a range; a GPU-dirty region flushes through onFlush without the lock.
public bool InvalidateRegion(ulong vaddr, ulong size, Action onFlush)
{
RejectUploadCallbackReentry();
var tracked = false;
VisitRegions(vaddr, size, create: false, (region, offset, bytes) =>
var tracked = InvalidateRegion(vaddr, size, out var needsGpuFlush);
if (needsGpuFlush)
{
tracked = true;
bool shouldFlush;
using (region.Lock.Hold())
onFlush();
}
return tracked;
}
// Marks the CPU write in every region whose bytes the GPU has not modified and
// reports whether any region still holds GPU data the caller must download first
// (one download of the whole range covers them all). Allocation-free: guest
// command writes land here once per dword.
public bool InvalidateRegion(ulong vaddr, ulong size, out bool needsGpuFlush)
{
RejectUploadCallbackReentry();
ValidateRange(vaddr, size);
var tracked = false;
needsGpuFlush = false;
var remaining = size;
var index = vaddr / BlockBytes;
var offset = vaddr % BlockBytes;
while (remaining != 0)
{
var bytes = Math.Min(BlockBytes - offset, remaining);
if (Volatile.Read(ref _regions[index]) is { } region)
{
shouldFlush = region.IsModified(WriteOrigin.Gpu, offset, bytes);
if (!shouldFlush)
tracked = true;
using (region.Lock.Hold())
{
region.MarkCpuWrite(region.BaseAddress + offset, bytes);
if (region.IsModified(WriteOrigin.Gpu, offset, bytes))
{
needsGpuFlush = true;
}
else
{
region.MarkCpuWrite(region.BaseAddress + offset, bytes);
}
}
}
if (shouldFlush)
{
onFlush();
}
remaining -= bytes;
offset = 0;
index++;
}
return false;
});
return tracked;
}
@ -341,6 +425,89 @@ public sealed class GuestPageTracker
}
}
// True when every block of the range is tracked and has no CPU-dirty page. A missing
// region is not known clean: the precise path creates it, fully dirty.
// Visits the maximal runs of the range whose 4 MiB blocks may hold CPU-dirty pages: a block
// with no region yet starts all dirty, and a region's summary bit is set while any of its
// pages is dirty. Known-clean blocks are skipped without a lock, the same test
// HasCpuDirtyPages starts with, so a caller that only uploads dirty pages loses nothing.
public void ForEachPossiblyCpuDirtyRange(ulong vaddr, ulong size, Action<ulong, ulong> visit)
{
if (size == 0)
{
return;
}
if (!new GuestSpan(vaddr, size).IsValid)
{
// Outside the tracked space nothing is known clean; the caller decides as before.
visit(vaddr, size);
return;
}
var end = vaddr + size;
var last = (end - 1) / BlockBytes;
var runStart = 0UL;
var inRun = false;
var index = vaddr / BlockBytes;
var loadedWord = -1;
var possiblyDirty = 0UL;
while (index <= last)
{
var word = (int)(index / 64);
if (word != loadedWord)
{
loadedWord = word;
possiblyDirty = ~(ulong)Volatile.Read(ref _presentRegions[word]) | _cpuDirtySummary.Word(word);
}
var shift = (int)(index % 64);
var pending = (inRun ? ~possiblyDirty : possiblyDirty) >> shift;
if (pending == 0)
{
index = ((ulong)word + 1) * 64;
continue;
}
index += (ulong)System.Numerics.BitOperations.TrailingZeroCount(pending);
if (index > last)
{
break;
}
if (inRun)
{
visit(runStart, index * BlockBytes - runStart);
inRun = false;
}
else
{
runStart = Math.Max(vaddr, index * BlockBytes);
inRun = true;
}
}
if (inRun)
{
visit(runStart, end - runStart);
}
}
private bool IsKnownCpuClean(ulong vaddr, ulong size)
{
ValidateRange(vaddr, size);
var last = (vaddr + size - 1) / BlockBytes;
for (var index = vaddr / BlockBytes; index <= last; index++)
{
if (Volatile.Read(ref _regions[index]) == null || _cpuDirtySummary.IsDirty(index))
{
return false;
}
}
return true;
}
// Visits (region, offset, bytes) per 4 MiB chunk; a true result stops the walk early.
private bool VisitRegions(ulong vaddr, ulong size, bool create, Func<TrackedRegion, ulong, ulong, bool> visit)
{
@ -379,8 +546,9 @@ public sealed class GuestPageTracker
return existing;
}
var created = new TrackedRegion(_pages, index * BlockBytes);
var created = new TrackedRegion(_pages, index * BlockBytes, _cpuDirtySummary);
Volatile.Write(ref _regions[index], created);
Interlocked.Or(ref _presentRegions[index / 64], 1L << (int)(index % 64));
return created;
}
}

View File

@ -17,8 +17,9 @@ public sealed class TrackedRegion
private PageMask _recentCpuUploads;
private PageMask _repeatedCpuWrites;
private PageMask _hotCpuWrites;
private readonly CpuDirtySummary? _summary;
public TrackedRegion(PageGuard pages, ulong baseAddress)
public TrackedRegion(PageGuard pages, ulong baseAddress, CpuDirtySummary? summary = null)
{
if (baseAddress % BlockBytes != 0)
{
@ -26,10 +27,13 @@ public sealed class TrackedRegion
}
_pages = pages;
_summary = summary;
BaseAddress = baseAddress;
_cpuDirty.Fill();
_writable.Fill();
_readable.Fill();
_summary?.Set(BaseAddress / BlockBytes, dirty: true);
_summary?.NoteDirtied();
}
public RegionLock Lock { get; } = new();
@ -205,8 +209,15 @@ public sealed class TrackedRegion
private ref PageMask GetDirtyMask(WriteOrigin side) => ref side == WriteOrigin.Cpu ? ref _cpuDirty : ref _gpuDirty;
// Every change to the CPU-dirty mask ends here, so it also publishes the block summary.
private void UpdateCpuProtection(bool track)
{
_summary?.Set(BaseAddress / BlockBytes, _cpuDirty.Any);
if (_summary is not null && (_cpuDirty & ~_writable).Any)
{
_summary.NoteDirtied();
}
var mask = _cpuDirty ^ _writable;
_writable = _cpuDirty;
if (mask.None)

View File

@ -0,0 +1,414 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
namespace SharpEmu.HLE;
/// <summary>
/// Per-host-thread state that emitted native x86-64 import stubs can read
/// without entering managed code.
///
/// The hottest guest imports are trivial queries about the calling thread
/// (pthread_self, pthread_getspecific). Routing them through the managed
/// import gateway costs a guest-to-host stack switch, a full guest register
/// pack, a GCHandle dereference and the whole dispatch bookkeeping - roughly
/// three orders of magnitude more than the answer itself. This class publishes
/// the few values those imports need into a flat native block whose address
/// lives in a native TLS slot, so a stub can answer them in a handful of
/// instructions and fall back to the managed gateway whenever the fast answer
/// is not available.
///
/// The block is owned by the host thread; the values in it describe the guest
/// thread currently bound to that host thread, and are republished by
/// <see cref="GuestThreadExecution.EnterGuestThread"/> /
/// <see cref="GuestThreadExecution.RestoreGuestThread"/>. A block whose
/// ThreadHandle is zero means "no guest thread bound here": every stub then
/// takes the managed path, which is also what happens before the fast path is
/// enabled at all.
/// </summary>
public static unsafe class GuestFastPath
{
/// <summary>
/// Byte offset of the guest thread handle pthread_self must answer with.
/// Only ever a real guest thread handle: for a host thread with no guest
/// thread bound, pthread_self has to reach the managed export (it registers
/// the thread's CPU context with the scheduler), so the slot stays zero.
/// </summary>
public const int BlockSelfHandleOffset = 0;
/// <summary>
/// Byte offset of the pointer to the pthread-specific value table of the
/// thread handle KernelPthreadState would report - the bound guest thread,
/// or the host thread's own synthetic handle when none is bound.
/// </summary>
public const int BlockTlsValuesOffset = 8;
/// <summary>Byte offset of the pthread_self fast-path hit counter.</summary>
public const int BlockSelfHitsOffset = 16;
/// <summary>Byte offset of the pthread_getspecific fast-path hit counter.</summary>
public const int BlockGetspecificHitsOffset = 32;
/// <summary>Size of the per-host-thread block. One cache line, room for future fast-path state.</summary>
public const int BlockSize = 64;
/// <summary>
/// pthread TLS keys the native table can answer. Keys are handed out
/// densely from 1 upwards by pthread_key_create, so a small dense table
/// covers every key a real title creates; anything at or above this limit
/// is answered by the managed dictionary through the slow path.
/// </summary>
public const int TlsSlotCount = 1024;
private static readonly ConcurrentDictionary<ulong, nint> TlsTables = new();
/// <summary>Values for keys at or above <see cref="TlsSlotCount"/>; effectively never used.</summary>
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> TlsOverflow = new();
private static readonly List<nint> Blocks = new();
private static readonly object BlocksGate = new();
private static Action<nint>? _publishBlock;
private static volatile bool _enabled;
[ThreadStatic]
private static nint _block;
[ThreadStatic]
private static ulong _hostThreadHandle;
[ThreadStatic]
private static ulong _cachedTableHandle;
[ThreadStatic]
private static nint _cachedTable;
/// <summary>True once a backend has installed native stubs that read the block.</summary>
public static bool Enabled => _enabled;
/// <summary>
/// Arms the fast path. <paramref name="publishBlock"/> stores a block
/// pointer in the native TLS slot the emitted stubs read, for the host
/// thread that calls it.
/// </summary>
public static void Enable(Action<nint> publishBlock)
{
_publishBlock = publishBlock ?? throw new ArgumentNullException(nameof(publishBlock));
_enabled = true;
// A block is published once per host thread, so a second backend (and
// therefore a second TLS slot) must be handed the block this thread
// already owns or its stubs would never see one.
if (_block != 0)
{
_publishBlock(_block);
}
}
/// <summary>Disarms every stub by clearing the calling thread's block; used by tests.</summary>
public static void Disable()
{
_enabled = false;
var block = _block;
if (block != 0)
{
*(ulong*)(block + BlockSelfHandleOffset) = 0;
*(nint*)(block + BlockTlsValuesOffset) = 0;
}
}
/// <summary>
/// Publishes <paramref name="guestThreadHandle"/> (and its pthread TLS
/// table) as the guest thread bound to the calling host thread. Pass 0 to
/// unbind, which leaves pthread_self on the managed gateway and falls the
/// value table back to the host thread's own pthread storage.
/// </summary>
public static void BindGuestThread(ulong guestThreadHandle)
{
if (!_enabled)
{
return;
}
var block = EnsureBlock();
if (block == 0)
{
return;
}
// Clear the self handle first so a stub can never answer with a handle
// that no longer owns this host thread.
*(ulong*)(block + BlockSelfHandleOffset) = 0;
var tlsHandle = guestThreadHandle != 0 ? guestThreadHandle : _hostThreadHandle;
*(nint*)(block + BlockTlsValuesOffset) = tlsHandle == 0 ? 0 : GetOrCreateTable(tlsHandle);
*(ulong*)(block + BlockSelfHandleOffset) = guestThreadHandle;
}
/// <summary>
/// Records the calling host thread's own pthread handle - the one
/// KernelPthreadState reports when no guest thread is bound - so
/// pthread_getspecific can be answered on threads the guest scheduler does
/// not own (the primary execution thread among them).
/// </summary>
public static void BindHostThread(ulong hostThreadHandle)
{
if (!_enabled || hostThreadHandle == 0)
{
return;
}
_hostThreadHandle = hostThreadHandle;
var block = EnsureBlock();
if (block == 0 || *(ulong*)(block + BlockSelfHandleOffset) != 0)
{
// A bound guest thread owns the value table while it runs.
return;
}
*(nint*)(block + BlockTlsValuesOffset) = GetOrCreateTable(hostThreadHandle);
}
/// <summary>
/// Drops every fast-path binding for the calling host thread, so each stub
/// falls back to the managed gateway until the thread is bound again.
/// </summary>
public static void UnbindCurrentThread()
{
_hostThreadHandle = 0;
var block = _block;
if (block != 0)
{
*(ulong*)(block + BlockSelfHandleOffset) = 0;
*(nint*)(block + BlockTlsValuesOffset) = 0;
}
}
/// <summary>Fast-path hit counts summed over every host thread, for diagnostics.</summary>
public static (long SelfHits, long GetspecificHits, int Blocks) SnapshotCounters()
{
long selfHits = 0;
long getspecificHits = 0;
int blocks;
lock (BlocksGate)
{
blocks = Blocks.Count;
foreach (var block in Blocks)
{
selfHits += *(long*)(block + BlockSelfHitsOffset);
getspecificHits += *(long*)(block + BlockGetspecificHitsOffset);
}
}
return (selfHits, getspecificHits, blocks);
}
/// <summary>Address of the calling host thread's block; allocates it on first use.</summary>
public static nint EnsureBlock()
{
var block = _block;
if (block != 0)
{
return block;
}
block = (nint)NativeMemory.AllocZeroed(BlockSize);
_block = block;
lock (BlocksGate)
{
Blocks.Add(block);
}
_publishBlock?.Invoke(block);
return block;
}
/// <summary>The pthread-specific value table for a guest (or host) thread handle.</summary>
public static nint GetOrCreateTable(ulong threadHandle)
{
if (threadHandle == 0)
{
return 0;
}
if (_cachedTableHandle == threadHandle && _cachedTable != 0)
{
return _cachedTable;
}
var table = TlsTables.GetOrAdd(
threadHandle,
static _ => (nint)NativeMemory.AllocZeroed((nuint)TlsSlotCount, sizeof(ulong)));
_cachedTableHandle = threadHandle;
_cachedTable = table;
return table;
}
/// <summary>pthread_getspecific, for the managed (slow) path and for keys the table cannot hold.</summary>
public static ulong GetSpecific(ulong threadHandle, int key)
{
if (threadHandle == 0 || key < 0)
{
return 0;
}
if ((uint)key < TlsSlotCount)
{
return TlsTables.TryGetValue(threadHandle, out var table) && table != 0
? ((ulong*)table)[key]
: 0;
}
return TlsOverflow.TryGetValue(threadHandle, out var overflow) && overflow.TryGetValue(key, out var value)
? value
: 0;
}
/// <summary>pthread_setspecific. Writes the same storage the native stubs read.</summary>
public static void SetSpecific(ulong threadHandle, int key, ulong value)
{
if (threadHandle == 0 || key < 0)
{
return;
}
if ((uint)key < TlsSlotCount)
{
((ulong*)GetOrCreateTable(threadHandle))[key] = value;
return;
}
TlsOverflow
.GetOrAdd(threadHandle, static _ => new ConcurrentDictionary<int, ulong>())[key] = value;
}
/// <summary>
/// Compare-and-set used by the destructor loop: clears <paramref name="key"/>
/// only when it still holds <paramref name="expected"/>, so a destructor that
/// re-sets the key is handled on the next iteration instead of being lost.
/// </summary>
public static bool TryClearSpecific(ulong threadHandle, int key, ulong expected)
{
if (threadHandle == 0 || key < 0)
{
return false;
}
if ((uint)key < TlsSlotCount)
{
if (!TlsTables.TryGetValue(threadHandle, out var table) || table == 0)
{
return false;
}
return Interlocked.CompareExchange(ref ((ulong*)table)[key], 0, expected) == expected;
}
return TlsOverflow.TryGetValue(threadHandle, out var overflow) &&
overflow.TryUpdate(key, 0, expected);
}
/// <summary>pthread_key_delete: the key reads back as unset on every thread.</summary>
public static void ClearKeyEverywhere(int key)
{
if (key < 0)
{
return;
}
if ((uint)key < TlsSlotCount)
{
foreach (var table in TlsTables.Values)
{
if (table != 0)
{
((ulong*)table)[key] = 0;
}
}
return;
}
foreach (var overflow in TlsOverflow.Values)
{
overflow.TryRemove(key, out _);
}
}
/// <summary>Every non-zero (key, value) pair a thread currently holds, for the destructor loop.</summary>
public static List<KeyValuePair<int, ulong>> SnapshotThreadValues(ulong threadHandle)
{
var result = new List<KeyValuePair<int, ulong>>();
if (threadHandle == 0)
{
return result;
}
if (TlsTables.TryGetValue(threadHandle, out var table) && table != 0)
{
var values = (ulong*)table;
for (var key = 0; key < TlsSlotCount; key++)
{
if (values[key] != 0)
{
result.Add(new KeyValuePair<int, ulong>(key, values[key]));
}
}
}
if (TlsOverflow.TryGetValue(threadHandle, out var overflow))
{
foreach (var entry in overflow)
{
if (entry.Value != 0)
{
result.Add(entry);
}
}
}
return result;
}
/// <summary>True when the thread has ever stored a pthread-specific value.</summary>
public static bool HasThreadValues(ulong threadHandle) =>
threadHandle != 0 &&
(TlsTables.ContainsKey(threadHandle) || TlsOverflow.ContainsKey(threadHandle));
/// <summary>
/// Drops a thread's pthread-specific storage on thread exit. Called from
/// the exiting thread after its destructors have run, so no stub can still
/// be reading the table; the calling thread's block is disarmed first.
/// </summary>
public static void ReleaseThread(ulong threadHandle)
{
if (threadHandle == 0)
{
return;
}
var block = _block;
if (block != 0 && *(ulong*)(block + BlockSelfHandleOffset) == threadHandle)
{
*(ulong*)(block + BlockSelfHandleOffset) = 0;
*(nint*)(block + BlockTlsValuesOffset) = 0;
}
else if (block != 0 && _hostThreadHandle == threadHandle)
{
*(nint*)(block + BlockTlsValuesOffset) = 0;
}
if (_cachedTableHandle == threadHandle)
{
_cachedTableHandle = 0;
_cachedTable = 0;
}
TlsOverflow.TryRemove(threadHandle, out _);
if (TlsTables.TryRemove(threadHandle, out var table) && table != 0)
{
NativeMemory.Free((void*)table);
}
}
}

View File

@ -30,6 +30,7 @@ public sealed class GuestSpaceOwner : IDisposable
private readonly SortedList<ulong, ulong> _free = new();
private readonly SortedList<ulong, OwnedRange> _mapped = new();
private readonly List<(ulong Address, ulong Size)> _owned = new();
private readonly bool _preReserveGuestAddressSpace;
private bool _disposed;
public GuestSpaceOwner(IHostViewMemory host, ulong backingSize, bool preReserveGuestAddressSpace = false)
@ -39,14 +40,8 @@ public sealed class GuestSpaceOwner : IDisposable
// Create lookup views before concurrent fault handlers can read the range table.
_ = _mapped.Keys;
_ = _mapped.Values;
if (preReserveGuestAddressSpace)
{
foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange))
{
_owned.Add((range.Address, range.Size));
AddFreeRange(range.Address, range.Size);
}
}
_preReserveGuestAddressSpace = preReserveGuestAddressSpace;
PreReserveGuestAddressSpace();
_views = new SharedBackingViews(host, backingSize);
if (!_views.IsAvailable)
{
@ -69,10 +64,23 @@ public sealed class GuestSpaceOwner : IDisposable
public bool TryReadBacking(ulong address, Span<byte> data) => _views.TryReadBacking(address, data);
// Reads through a view that covers the whole range without any lock; false otherwise.
public bool TryReadSingleView(ulong address, Span<byte> data) => _views.TryReadSingleView(address, data);
public object AliasSnapshot => _views.AliasSnapshot;
public bool TryEnterAliasAccess() => _views.TryEnterAliasAccess();
public void ExitAliasAccess() => _views.ExitAliasAccess();
public bool TryResolveAlias(ulong address, ulong size, out ulong alias) => _views.TryResolveAlias(address, size, out alias);
public bool TryCopyBacking(ulong destination, ulong source, ulong size) => _views.TryCopyBacking(destination, source, size);
public bool IsBacked(ulong address, ulong size) => _views.Contains(address, size);
public bool IsBackedWithoutLock(ulong address, ulong size) => _views.ContainsWithoutLock(address, size);
public bool IsRestoredView(ulong address) => _views.IsRestoredView(address);
public bool TryReserveAddressRange(ulong address, ulong size)
@ -495,6 +503,22 @@ public sealed class GuestSpaceOwner : IDisposable
_owned.Clear();
_free.Clear();
_mapped.Clear();
// A new image load needs the guest address space reserved again.
PreReserveGuestAddressSpace();
}
}
private void PreReserveGuestAddressSpace()
{
if (!_preReserveGuestAddressSpace)
{
return;
}
foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange))
{
_owned.Add((range.Address, range.Size));
AddFreeRange(range.Address, range.Size);
}
}

View File

@ -21,6 +21,15 @@ public sealed unsafe class SharedBackingViews : IDisposable
private readonly SortedList<ulong, ViewRecord> _views = new();
private bool _disposed;
// Guest command writes and their reads by the render thread reach TryWriteBacking and
// TryReadBacking millions of times per second, a few bytes at a time, while views
// change rarely. Single-view accesses search this immutable copy of _views without
// the lock; every change to _views republishes it under the lock.
private ViewRecord[] _snapshot = [];
// Single-view accesses in flight; Dispose waits for them before releasing the alias.
private int _activeAccesses;
public SharedBackingViews(IHostViewMemory host, ulong size)
{
_host = host;
@ -49,6 +58,19 @@ public sealed unsafe class SharedBackingViews : IDisposable
public bool TryWriteBacking(ulong address, ReadOnlySpan<byte> data)
{
if (TryAccessSingleView(address, (ulong)data.Length, out var target))
{
try
{
data.CopyTo(new Span<byte>((void*)target, data.Length));
return true;
}
finally
{
Interlocked.Decrement(ref _activeAccesses);
}
}
lock (_lock)
{
if (IsAvailable && TryFindRecord(address, (ulong)data.Length, out var record))
@ -77,8 +99,40 @@ public sealed unsafe class SharedBackingViews : IDisposable
}
}
// The lock-free single-view read only; false (with nothing read) for anything else.
public bool TryReadSingleView(ulong address, Span<byte> data)
{
if (!TryAccessSingleView(address, (ulong)data.Length, out var source))
{
return false;
}
try
{
new ReadOnlySpan<byte>((void*)source, data.Length).CopyTo(data);
return true;
}
finally
{
Interlocked.Decrement(ref _activeAccesses);
}
}
public bool TryReadBacking(ulong address, Span<byte> data)
{
if (TryAccessSingleView(address, (ulong)data.Length, out var source))
{
try
{
new ReadOnlySpan<byte>((void*)source, data.Length).CopyTo(data);
return true;
}
finally
{
Interlocked.Decrement(ref _activeAccesses);
}
}
lock (_lock)
{
// A read inside one mapping needs no temporary segment list.
@ -194,6 +248,7 @@ public sealed unsafe class SharedBackingViews : IDisposable
}
_views[address] = new ViewRecord(address, size, offset, protection) { WasRestored = wasRestored };
PublishSnapshot();
}
failure = HostViewFailure.None;
@ -268,6 +323,7 @@ public sealed unsafe class SharedBackingViews : IDisposable
{
old = record;
_views.RemoveAt(index);
PublishSnapshot();
}
}
}
@ -346,6 +402,49 @@ public sealed unsafe class SharedBackingViews : IDisposable
}
}
// Contains over the published snapshot, without the lock: descriptor validation asks
// this for every bound buffer, and the lock is contended by guest threads. A true answer
// is as current as a locked one would be once the lock were released.
public bool ContainsWithoutLock(ulong address, ulong size)
{
if (size == 0 || ulong.MaxValue - address < size || Volatile.Read(ref _disposed))
{
return false;
}
var snapshot = Volatile.Read(ref _snapshot);
var end = address + size;
var current = address;
while (current < end)
{
var low = 0;
var high = snapshot.Length - 1;
var found = -1;
while (low <= high)
{
var middle = low + ((high - low) >> 1);
if (snapshot[middle].Address <= current)
{
found = middle;
low = middle + 1;
}
else
{
high = middle - 1;
}
}
if (found < 0 || current >= snapshot[found].Address + snapshot[found].Size)
{
return false;
}
current = Math.Min(end, snapshot[found].Address + snapshot[found].Size);
}
return true;
}
public bool Contains(ulong address, ulong size)
{
if (size == 0 || ulong.MaxValue - address < size)
@ -385,6 +484,15 @@ public sealed unsafe class SharedBackingViews : IDisposable
_disposed = true;
views = new List<ViewRecord>(_views.Values);
_views.Clear();
PublishSnapshot();
}
// A lock-free access that found a view before the snapshot emptied still copies
// through the alias; let it finish before the alias is released.
var spin = new SpinWait();
while (Volatile.Read(ref _activeAccesses) != 0)
{
spin.SpinOnce();
}
foreach (var view in views)
@ -495,6 +603,94 @@ public sealed unsafe class SharedBackingViews : IDisposable
return true;
}
// Must be called under _lock after every change to _views.
private void PublishSnapshot()
{
var snapshot = new ViewRecord[_views.Count];
_views.Values.CopyTo(snapshot, 0);
Volatile.Write(ref _snapshot, snapshot);
}
// Resolves an access that lies inside one view to its alias address and registers
// it as in flight; the caller must decrement _activeAccesses when done. Anything
// else (spanning views, unmapped, disposed) is left to the locked path.
private bool TryAccessSingleView(ulong address, ulong size, out ulong target)
{
target = 0;
if (size == 0 || ulong.MaxValue - address < size)
{
return false;
}
Interlocked.Increment(ref _activeAccesses);
if (TryFindAlias(Volatile.Read(ref _snapshot), address, size, out target))
{
return true;
}
Interlocked.Decrement(ref _activeAccesses);
return false;
}
public object AliasSnapshot => Volatile.Read(ref _snapshot);
public bool TryEnterAliasAccess()
{
Interlocked.Increment(ref _activeAccesses);
if (!Volatile.Read(ref _disposed) && _backing != null)
{
return true;
}
Interlocked.Decrement(ref _activeAccesses);
return false;
}
public void ExitAliasAccess() => Interlocked.Decrement(ref _activeAccesses);
public bool TryResolveAlias(ulong address, ulong size, out ulong target)
{
target = 0;
return size != 0 && ulong.MaxValue - address >= size && TryFindAlias(Volatile.Read(ref _snapshot), address, size, out target);
}
private bool TryFindAlias(ViewRecord[] snapshot, ulong address, ulong size, out ulong target)
{
target = 0;
var low = 0;
var high = snapshot.Length - 1;
var found = -1;
while (low <= high)
{
var middle = low + ((high - low) >> 1);
if (snapshot[middle].Address <= address)
{
found = middle;
low = middle + 1;
}
else
{
high = middle - 1;
}
}
if (found >= 0 && !Volatile.Read(ref _disposed) && _backing != null)
{
var record = snapshot[found];
if (address + size <= record.Address + record.Size)
{
var offset = record.Offset + address - record.Address;
if (IsWithinBacking(offset, size))
{
target = AliasBase + offset;
return true;
}
}
}
return false;
}
private bool IsWithinBacking(ulong offset, ulong size) =>
size != 0 && offset < Size && size <= Size - offset;
@ -503,6 +699,7 @@ public sealed unsafe class SharedBackingViews : IDisposable
lock (_lock)
{
_views[record.Address] = record;
PublishSnapshot();
}
}

View File

@ -12,7 +12,8 @@ public readonly record struct GuestThreadStartRequest(
ulong AttributeAddress,
string Name,
int Priority,
ulong AffinityMask);
ulong AffinityMask,
ulong StackSize = 0);
public readonly record struct GuestThreadSnapshot(
ulong ThreadHandle,
@ -49,6 +50,21 @@ public interface IGuestThreadScheduler
/// </summary>
void RegisterGuestThreadContext(ulong threadHandle, CpuContext context);
/// <summary>
/// Returns the exact mapped guest stack owned by a scheduler thread. The
/// default keeps alternate/test schedulers source-compatible when they do
/// not own guest stack mappings.
/// </summary>
bool TryGetGuestThreadStackBounds(
ulong threadHandle,
out ulong stackBase,
out ulong stackSize)
{
stackBase = 0;
stackSize = 0;
return false;
}
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
bool TryJoinThread(
@ -278,6 +294,7 @@ public static class GuestThreadExecution
GpuMemory.GpuMemoryAccessProfile.InitializeCurrentThread();
var previous = _currentGuestThreadHandle;
_currentGuestThreadHandle = threadHandle;
GuestFastPath.BindGuestThread(threadHandle);
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
@ -299,6 +316,7 @@ public static class GuestThreadExecution
public static void RestoreGuestThread(ulong previousThreadHandle)
{
_currentGuestThreadHandle = previousThreadHandle;
GuestFastPath.BindGuestThread(previousThreadHandle);
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;

View File

@ -21,6 +21,7 @@ public static class HleDataSymbols
private static readonly nint _stackChkGuardAddress = Allocate(sizeof(ulong) * 2);
private static readonly nint _progNameBufferAddress = Allocate(ProgNameMaxBytes + 1);
private static readonly nint _progNamePointerAddress = Allocate(nint.Size);
private static readonly nint _argumentVectorAddress = Allocate(nint.Size * 2);
private static readonly nint _libcNeedFlagAddress = Allocate(sizeof(uint));
private static readonly nint _libcInternalNeedFlagAddress = Allocate(sizeof(uint));
@ -55,6 +56,12 @@ public static class HleDataSymbols
yield return LibcInternalNeedFlagNid;
}
public static int ProcessArgumentCount =>
_progNameBufferAddress != 0 && _argumentVectorAddress != 0 ? 1 : 0;
public static ulong ProcessArgumentVectorAddress =>
unchecked((ulong)_argumentVectorAddress);
public static void ConfigureProcessImageName(string? processImageName)
{
var effectiveName = string.IsNullOrWhiteSpace(processImageName)
@ -65,7 +72,9 @@ public static class HleDataSymbols
lock (_gate)
{
if (_progNameBufferAddress == 0 || _progNamePointerAddress == 0)
if (_progNameBufferAddress == 0 ||
_progNamePointerAddress == 0 ||
_argumentVectorAddress == 0)
{
return;
}
@ -77,6 +86,8 @@ public static class HleDataSymbols
Marshal.Copy(encodedName, 0, _progNameBufferAddress, byteCount);
WritePointer(_progNamePointerAddress, _progNameBufferAddress);
WritePointer(_argumentVectorAddress, _progNameBufferAddress);
WritePointer(IntPtr.Add(_argumentVectorAddress, nint.Size), 0);
}
}

View File

@ -0,0 +1,121 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.HLE.Host;
public static unsafe class HostLaneReservation
{
private const int RelationProcessorCore = 0;
private const int ThreadPriorityHighest = 2;
public static readonly ulong ReservedMask;
public static readonly ulong GuestMask;
static HostLaneReservation()
{
if (!OperatingSystem.IsWindows() || Environment.GetEnvironmentVariable("SHARPEMU_RENDER_CORE") == "0")
{
return;
}
var lanes = Environment.ProcessorCount;
if (lanes < 8 || lanes > 64)
{
return;
}
var core = SelectCore();
var all = lanes == 64 ? ulong.MaxValue : (1UL << lanes) - 1;
if (core == 0 || (core & ~all) != 0 || (all & ~core) == 0)
{
return;
}
ReservedMask = core;
GuestMask = all & ~core;
}
public static bool Active => ReservedMask != 0;
public static void ApplyToRenderThread()
{
if (!Active)
{
return;
}
var thread = GetCurrentThread();
_ = SetThreadAffinityMask(thread, (nuint)ReservedMask);
_ = SetThreadPriority(thread, ThreadPriorityHighest);
Console.Error.WriteLine($"[LOADER][INFO] Render thread reserved host lanes 0x{ReservedMask:X}; guest threads use 0x{GuestMask:X}.");
}
public static void ApplyToGuestThread()
{
if (Active)
{
_ = SetThreadAffinityMask(GetCurrentThread(), (nuint)GuestMask);
}
}
private static ulong SelectCore()
{
uint length = 0;
_ = GetLogicalProcessorInformationEx(RelationProcessorCore, null, &length);
if (length == 0)
{
return 0;
}
var buffer = new byte[length];
fixed (byte* start = buffer)
{
if (!GetLogicalProcessorInformationEx(RelationProcessorCore, start, &length))
{
return 0;
}
ulong selected = 0;
var selectedClass = -1;
for (uint offset = 0; offset + 48 <= length;)
{
var entry = start + offset;
var size = *(uint*)(entry + 4);
if (size == 0)
{
break;
}
var efficiencyClass = entry[9];
var groupCount = *(ushort*)(entry + 30);
var mask = *(ulong*)(entry + 32);
var group = *(ushort*)(entry + 40);
if (*(int*)entry == RelationProcessorCore && groupCount == 1 && group == 0 && mask != 0 && efficiencyClass >= selectedClass)
{
selected = mask;
selectedClass = efficiencyClass;
}
offset += size;
}
return selected;
}
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GetLogicalProcessorInformationEx(int relationshipType, byte* buffer, uint* returnedLength);
[DllImport("kernel32.dll")]
private static extern nint GetCurrentThread();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nuint SetThreadAffinityMask(nint thread, nuint mask);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetThreadPriority(nint thread, int priority);
}

View File

@ -328,6 +328,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
}
}
HostMemory.OnMappingChanged();
Regions[(ulong)result] = new Region
{
Base = (ulong)result,
@ -357,7 +358,8 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
: munmap((nint)address, (nuint)region.Size) == 0;
if (released)
{
Regions.Remove((ulong)address);
HostMemory.OnMappingChanged();
Regions.Remove((ulong)address);
}
return released;
@ -492,6 +494,7 @@ internal sealed unsafe class PosixHostMemory : IHostMemory
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
{
HostMemory.OnMappingChanged();
if (start == region.Base && size >= region.Size)
{
region.DefaultProtect = protect;

View File

@ -96,6 +96,60 @@ internal sealed unsafe partial class PosixHostViews : IHostViewMemory
return address;
}
// POSIX has no VirtualQuery for foreign host mappings, so find the free
// ranges by reserving halves until each attempt fits or falls below the
// minimum. Occupied host ranges are few, so this needs few attempts.
public IReadOnlyList<HostAddressRange> ReserveFreeAddressRanges(ulong start, ulong end, ulong minimumSize)
{
var reserved = new List<HostAddressRange>();
start = AlignUp(start, Granularity);
end -= end % Granularity;
if (start >= end || minimumSize == 0)
{
return reserved;
}
lock (PosixViewRegions.Gate)
{
ReserveFreeAddressRangesLocked(start, end - start, Math.Max(minimumSize, Granularity), reserved);
}
return reserved;
}
private void ReserveFreeAddressRangesLocked(ulong address, ulong size, ulong minimumSize, List<HostAddressRange> reserved)
{
if (ReserveHoleCore(address, size) == address)
{
PosixViewRegions.Replace(address, size, HostMemory.MEM_RESERVE, HostMemory.PAGE_NOACCESS);
if (reserved.Count != 0 && reserved[^1].Address + reserved[^1].Size == address)
{
reserved[^1] = reserved[^1] with { Size = reserved[^1].Size + size };
}
else
{
reserved.Add(new HostAddressRange(address, size));
}
return;
}
var half = size / 2;
half -= half % Granularity;
if (half < minimumSize)
{
return;
}
ReserveFreeAddressRangesLocked(address, half, minimumSize, reserved);
ReserveFreeAddressRangesLocked(address + half, size - half, minimumSize, reserved);
}
private static ulong AlignUp(ulong value, ulong alignment)
{
var remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
public bool SplitHole(ulong address, ulong size) => size != 0;
public bool JoinHoles(ulong address, ulong size) => size != 0;

View File

@ -13,6 +13,7 @@ internal static class PosixViewRegions
internal static void Replace(ulong address, ulong size, uint state, uint protection)
{
HostMemory.OnMappingChanged();
var end = RoundPageEnd(address, size);
var replacement = new List<Region>(Regions.Count + 2);
foreach (var region in Regions)
@ -50,6 +51,7 @@ internal static class PosixViewRegions
// Mapping creates the storage. Fault-time protection updates must not allocate.
internal static void ChangeProtection(ulong address, ulong size, uint protection)
{
HostMemory.OnMappingChanged();
var end = RoundPageEnd(address, size);
var pageSize = (ulong)Environment.SystemPageSize;
foreach (var region in Regions)

View File

@ -212,7 +212,15 @@ internal sealed unsafe class SdlHostAudio : IHostPcmAudioOutput
break;
}
Thread.Sleep(1);
// Sleep until the device should have drained to the cap rather than polling every
// millisecond: this wait paces every audio grain, and 1 ms polls cost the audio
// thread several wakeups per grain.
var bytesPerMillisecond = _bytesPerFrame * (double)_sampleRate / 1_000;
var drainMilliseconds = bytesPerMillisecond > 0
? (queued - _maximumQueuedBytes) / bytesPerMillisecond
: 1;
var remainingMilliseconds = (deadline - Stopwatch.GetTimestamp()) * 1_000.0 / Stopwatch.Frequency;
Thread.Sleep((int)Math.Clamp(Math.Ceiling(Math.Min(drainMilliseconds, remainingMilliseconds)), 1, MaximumWaitMilliseconds));
}
RecordSubmission(queued, blockStart, dropped: overrun, bytes: pcm.Length);

View File

@ -16,6 +16,14 @@ namespace SharpEmu.HLE;
/// </summary>
public static unsafe class HostMemory
{
private static long _mappingGeneration;
// Changes whenever a POSIX mapping or its protection changes, so callers can cache a
// query result until the address space changes.
public static long MappingGeneration => Volatile.Read(ref _mappingGeneration);
internal static void OnMappingChanged() => Interlocked.Increment(ref _mappingGeneration);
public const uint MEM_COMMIT = 0x1000;
public const uint MEM_RESERVE = 0x2000;
public const uint MEM_RELEASE = 0x8000;
@ -254,6 +262,7 @@ public static unsafe class HostMemory
}
}
HostMemory.OnMappingChanged();
Regions[(ulong)result] = new Region
{
Base = (ulong)result,
@ -277,6 +286,7 @@ public static unsafe class HostMemory
return false;
}
HostMemory.OnMappingChanged();
Regions.Remove((ulong)address);
return munmap((nint)address, (nuint)region.Size) == 0;
}
@ -439,6 +449,7 @@ public static unsafe class HostMemory
private static void SetProtectRangeLocked(Region region, ulong start, ulong size, uint protect)
{
HostMemory.OnMappingChanged();
if (start == region.Base && size >= region.Size)
{
region.DefaultProtect = protect;

View File

@ -23,6 +23,16 @@ public interface ICpuMemory
bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) => false;
// Finds the first (or last) byte equal to needle in the NUL-terminated string
// at address, the terminator included, scanning mapped memory in place.
// match is 0 when absent. False means this memory cannot scan directly and
// the caller should read the string through TryRead instead.
bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match)
{
match = 0;
return false;
}
// True when the whole range is mapped guest memory; no bytes are copied.
bool CanRead(ulong address, ulong size) => false;

View File

@ -24,4 +24,18 @@ public interface IGuestBackedSpace
// Copies through the backing alias only: no protection change, no store notification.
bool TryWriteBacking(ulong address, ReadOnlySpan<byte> data);
bool TryReadBacking(ulong address, Span<byte> data);
object? BackingAliasSnapshot => null;
bool TryEnterBackingAliasAccess() => false;
void ExitBackingAliasAccess()
{
}
bool TryResolveBackingAlias(ulong address, ulong size, out ulong alias)
{
alias = 0;
return false;
}
}

View File

@ -21,6 +21,7 @@ namespace LibAtrac9
public int SampleRateIndex { get; }
/// <summary>
/// A 3-bit value specifying one of 6 substream channel mappings.
/// Always 0 for streams with independent mono channels.
/// </summary>
public int ChannelConfigIndex { get; }
/// <summary>
@ -70,6 +71,12 @@ namespace LibAtrac9
/// The number of samples in one superframe.
/// </summary>
public int SuperframeSamples { get; }
/// <summary>
/// Non-zero for streams whose config header is 0x30: every channel is
/// an independent mono stream with this many bytes per superframe,
/// and the channels are interleaved frame by frame.
/// </summary>
public int IndependentBlockSuperframeBytes { get; }
/// <summary>
/// Reads ATRAC9 configuration data and calculates the stream parameters from it.
@ -82,16 +89,33 @@ namespace LibAtrac9
throw new InvalidDataException("Config data must be 4 bytes long");
}
ReadConfigData(configData, out int a, out int b, out int c, out int d);
ReadConfigData(configData, out int a, out int b, out int c, out int d, out int independentChannels);
SampleRateIndex = a;
ChannelConfigIndex = b;
FrameBytes = c;
SuperframeIndex = d;
ConfigData = configData;
if (independentChannels > 0)
{
// The frame size field holds one channel's share; the frame as
// a whole carries one mono block per channel.
ChannelConfig = ChannelConfig.CreateIndependentMono(independentChannels);
IndependentBlockSuperframeBytes = c << SuperframeIndex;
FrameBytes = c * independentChannels;
}
else
{
if (ChannelConfigIndex >= Tables.ChannelConfig.Length)
{
throw new InvalidDataException("ATRAC9 Config Data is invalid");
}
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
FrameBytes = c;
}
FramesPerSuperframe = 1 << SuperframeIndex;
SuperframeBytes = FrameBytes << SuperframeIndex;
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
ChannelCount = ChannelConfig.ChannelCount;
SampleRate = Tables.SampleRates[SampleRateIndex];
@ -101,21 +125,36 @@ namespace LibAtrac9
SuperframeSamples = FrameSamples * FramesPerSuperframe;
}
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex)
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex, out int independentChannels)
{
var reader = new BitReader(configData);
int header = reader.ReadInt(8);
sampleRateIndex = reader.ReadInt(4);
channelConfigIndex = reader.ReadInt(3);
channelConfigIndex = 0;
independentChannels = 0;
if (header == IndependentMonoHeader)
{
// PS5 multichannel streams (ambisonics: 4, 9 or 16 channels)
// replace the 3-bit channel mapping with a 6-bit channel count.
independentChannels = reader.ReadInt(6) + 1;
}
else
{
channelConfigIndex = reader.ReadInt(3);
}
int validationBit = reader.ReadInt(1);
frameBytes = reader.ReadInt(11) + 1;
superframeIndex = reader.ReadInt(2);
if (header != 0xFE || validationBit != 0)
if ((header != StandardHeader && header != IndependentMonoHeader) || validationBit != 0)
{
throw new InvalidDataException("ATRAC9 Config Data is invalid");
}
}
private const int StandardHeader = 0xFE;
private const int IndependentMonoHeader = 0x30;
}
}

View File

@ -14,6 +14,7 @@ namespace LibAtrac9
public bool FirstInSuperframe { get; set; }
public bool ReuseBandParams { get; set; }
public int SuperframeBytesUsed { get; set; }
public int BandCount { get; set; }
public int StereoBand { get; set; }

View File

@ -1,5 +1,7 @@
// SPDX-License-Identifier: MIT
#nullable disable
using System;
namespace LibAtrac9
{
/// <summary>
@ -17,6 +19,13 @@ namespace LibAtrac9
}
}
internal static ChannelConfig CreateIndependentMono(int channelCount)
{
var blockTypes = new BlockType[channelCount];
Array.Fill(blockTypes, BlockType.Mono);
return new ChannelConfig(blockTypes);
}
/// <summary>
/// The number of blocks or substreams in the ATRAC9 stream
/// </summary>

View File

@ -10,12 +10,57 @@ namespace LibAtrac9
{
public static void UnpackFrame(BitReader reader, Frame frame)
{
if (frame.Config.IndependentBlockSuperframeBytes != 0)
{
UnpackIndependentMonoFrame(reader, frame);
return;
}
foreach (Block block in frame.Blocks)
{
UnpackBlock(reader, block);
}
}
// Each channel is a standalone mono superframe with a fixed byte budget,
// interleaved frame by frame. Blocks of the earlier frames are packed
// back to back; in the last frame every block gets whatever is left of
// its channel's budget, so those slots end in padding.
private static void UnpackIndependentMonoFrame(BitReader reader, Frame frame)
{
int budget = frame.Config.IndependentBlockSuperframeBytes;
bool lastFrame = frame.FrameIndex == frame.Config.FramesPerSuperframe - 1;
int slotStart = reader.Position / 8;
foreach (Block block in frame.Blocks)
{
if (frame.FrameIndex == 0)
{
block.SuperframeBytesUsed = 0;
}
int usedBefore = block.SuperframeBytesUsed;
if (lastFrame)
{
reader.Position = slotStart * 8;
}
int start = reader.Position;
UnpackBlock(reader, block);
block.SuperframeBytesUsed += (reader.Position - start) / 8;
if (block.SuperframeBytesUsed > budget)
{
throw new InvalidDataException();
}
if (lastFrame)
{
slotStart += budget - usedBefore;
reader.Position = slotStart * 8;
}
}
}
private static void UnpackBlock(BitReader reader, Block block)
{
ReadBlockHeader(reader, block);

View File

@ -546,8 +546,11 @@ public static partial class AgcExports
var increment = (uint)(incrementRaw & 0xFF);
var writeConfirm = (uint)(writeConfirmRaw & 0xFF);
// A zero destination is legal: titles emit the packet into a template
// and set the address later with sceAgcWriteDataPatchSetAddressOrOffset.
// Rejecting it returned null, and patching that relocated null pointer
// wrote garbage into the label the packet was meant to clear.
if (commandBufferAddress == 0 ||
destinationAddress == 0 ||
dataAddress == 0 ||
dwordCount > 0x3FFD)
{

View File

@ -184,24 +184,6 @@ public static partial class AgcExports
var commandBufferAddress = ctx[CpuRegister.Rdi];
var dataOffset = (uint)ctx[CpuRegister.Rsi];
var drawModifier = ctx[CpuRegister.Rdx];
var emit = Interlocked.Increment(ref _indirectDrawEmitCount);
if (emit <= 12 || emit % 250 == 0)
{
var rcx = ctx[CpuRegister.Rcx];
var dump = string.Empty;
for (var word = 0; word < 8; word++)
{
dump += TryReadUInt32(ctx, rcx + dataOffset + ((ulong)word * 4), out var raw)
? $" {raw}"
: " ?";
}
Console.Error.WriteLine(
$"[LOADER][WARN] agc.emit_indirect#{emit} buf=0x{commandBufferAddress:X16} " +
$"off=0x{dataOffset:X} rdx=0x{ctx[CpuRegister.Rdx]:X} rcx=0x{rcx:X} " +
$"r8=0x{ctx[CpuRegister.R8]:X} rcx_words:{dump}");
}
if (commandBufferAddress == 0)
{
@ -242,7 +224,7 @@ public static partial class AgcExports
TraceAgc(
$"agc.dcb_draw_indirect buf=0x{commandBufferAddress:X16} " +
$"draw=0x{drawCommand:X16} offset=0x{dataOffset:X}");
$"draw=0x{drawCommand:X16} offset=0x{dataOffset:X} modifier=0x{drawModifier:X}");
return ReturnPointer(ctx, drawCommand);
}
@ -474,6 +456,5 @@ public static partial class AgcExports
}
#pragma warning restore SHEM006
private static long _indirectDrawEmitCount;
private static long _indirectDrawEmitRejectCount;
}

View File

@ -10,6 +10,36 @@ public static partial class AgcExports
{
// This partial patches previously constructed AGC command packets.
// Uncatalogued name; the NID and import library identify this AGC export.
// Observed ABI: packet, cache policy, next-buffer address, length in dwords.
// The caller patches the previous DcbJump after allocating its continuation.
#pragma warning disable SHEM006
[SysAbiExport(Nid = "Ikfdt-rIqCE", ExportName = "sceAgcUnknownIkfdt",
Target = Generation.Gen5, LibraryName = "libSceAgc")]
public static int UnknownIkfdt(CpuContext ctx)
{
var packet = ctx[CpuRegister.Rdi];
var cachePolicy = (uint)ctx[CpuRegister.Rsi];
var target = ctx[CpuRegister.Rdx];
var dwords = (uint)ctx[CpuRegister.Rcx];
if (cachePolicy > 3 || dwords > 0xFFFFFu ||
!TryGetPacketIdentity(ctx, packet, out var opcode, out _) ||
opcode != ItIndirectBuffer ||
!TryReadUInt32(ctx, packet, out var header) ||
((header >> 16) & 0x3FFFu) != 2 ||
!TryReadUInt32(ctx, packet + 12, out var control))
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
// Preserve call/chain mode and every other control bit.
control = (control & ~0x300F_FFFFu) | (cachePolicy << 28) | dwords;
return TryWriteUInt32(ctx, packet + 4, (uint)target) &&
TryWriteUInt32(ctx, packet + 8, (uint)(target >> 32) & 0xFFFFu) &&
TryWriteUInt32(ctx, packet + 12, control)
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
#pragma warning restore SHEM006
[SysAbiExport(
Nid = "vcmNN+AAXnY",
ExportName = "sceAgcSetCxRegIndirectPatchSetAddress",

View File

@ -10,13 +10,8 @@ public static partial class AgcExports
{
// This partial exposes the shader headers the create export registered.
internal static ulong GetShaderHeaderAddress(ulong codeAddress)
{
lock (_submitTraceGate)
{
return _shaderHeadersByCode.TryGetValue(codeAddress, out var header) ? header : 0;
}
}
internal static ulong GetShaderHeaderAddress(ulong codeAddress) =>
_shaderHeadersByCode.TryGetValue(codeAddress, out var header) ? header : 0;
internal static ShaderHeaderRegistry CreateShaderHeaderRegistry(CpuContext context) =>
new(

View File

@ -1,10 +1,12 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using SharpEmu.HLE;
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
using SharpEmu.Libs.Kernel;
using SharpEmu.ShaderCompiler;
@ -31,7 +33,10 @@ public static partial class AgcExports
private const ulong ShaderNumShRegistersOffset = 0x5C;
private const int ShaderStructBytes = 0x60;
private const uint MaximumDeclaredShaderSizeBytes = 1024 * 1024;
private const int MaximumEmbeddedFusedScanBytes = 64 * 1024;
// Embedded GS/HS front/back descriptors can sit beyond the first 64 KiB of
// the upload. Scan the full shader object size accepted by AGC so the
// continuation is registered before resource planning sees the front half.
private const int MaximumEmbeddedFusedScanBytes = 1024 * 1024;
private const ulong FusedShaderImageAlignment = 4;
private const byte ComputeShaderType = 0;
private const byte PsShaderType = 1;
@ -125,10 +130,7 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
lock (_submitTraceGate)
{
_shaderHeadersByCode[codeAddress] = headerAddress;
}
_shaderHeadersByCode[codeAddress] = headerAddress;
TryRegisterEmbeddedFusedProgram(ctx, codeAddress, headerAddress);
@ -166,27 +168,6 @@ public static partial class AgcExports
return false;
}
var upload = new byte[MaximumEmbeddedFusedScanBytes];
var bytesRead = 0;
const int readChunkBytes = 4 * 1024;
while (bytesRead < upload.Length)
{
var chunkLength = Math.Min(readChunkBytes, upload.Length - bytesRead);
if (!ctx.Memory.TryRead(
entryCodeAddress + (ulong)bytesRead,
upload.AsSpan(bytesRead, chunkLength)))
{
break;
}
bytesRead += chunkLength;
}
if (bytesRead < ShaderStructBytes)
{
return false;
}
var requiredContinuationType = entryType == GsFrontShaderType
? GsBackShaderType
: HsBackShaderType;
@ -201,57 +182,95 @@ public static partial class AgcExports
ulong bestCodeAddress = 0;
ulong bestHeaderAddress = 0;
var bestDistance = ulong.MaxValue;
for (var offset = 0;
offset <= bytesRead - ShaderStructBytes;
offset += sizeof(uint))
bool ScanForContinuation(ulong scanAddress)
{
var descriptor = upload.AsSpan(offset, ShaderStructBytes);
if (BinaryPrimitives.ReadUInt32LittleEndian(descriptor) != ShaderFileHeader ||
BinaryPrimitives.ReadUInt32LittleEndian(descriptor[sizeof(uint)..]) != ShaderVersion ||
descriptor[(int)ShaderTypeOffset] != requiredContinuationType)
var upload = new byte[MaximumEmbeddedFusedScanBytes];
var bytesRead = 0;
const int readChunkBytes = 4 * 1024;
while (bytesRead < upload.Length)
{
continue;
var chunkLength = Math.Min(readChunkBytes, upload.Length - bytesRead);
if (!ctx.Memory.TryRead(
scanAddress + (ulong)bytesRead,
upload.AsSpan(bytesRead, chunkLength)))
{
break;
}
bytesRead += chunkLength;
}
var continuationCodeAddress = BinaryPrimitives.ReadUInt64LittleEndian(
descriptor[(int)ShaderCodeOffset..]);
var continuationSize = BinaryPrimitives.ReadUInt32LittleEndian(
descriptor[(int)ShaderSizeOffset..]);
if (continuationCodeAddress <= entryCodeAddress ||
continuationCodeAddress - entryCodeAddress > uint.MaxValue ||
!IsValidDeclaredShaderSize(continuationSize) ||
!CanReadShaderRange(ctx, continuationCodeAddress, continuationSize))
if (bytesRead < ShaderStructBytes)
{
continue;
return false;
}
var continuationSpecialsAddress = BinaryPrimitives.ReadUInt64LittleEndian(
descriptor[(int)ShaderSpecialsOffset..]);
if (entrySpecialsAddress != 0 && continuationSpecialsAddress != 0)
var found = false;
for (var offset = 0;
offset <= bytesRead - ShaderStructBytes;
offset += sizeof(uint))
{
if (!TryReadUInt32(
ctx,
entrySpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
out var entryStages) ||
!TryReadUInt32(
ctx,
continuationSpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
out var continuationStages) ||
((entryStages ^ continuationStages) & waveSizeBit) != 0)
var descriptor = upload.AsSpan(offset, ShaderStructBytes);
if (BinaryPrimitives.ReadUInt32LittleEndian(descriptor) != ShaderFileHeader ||
BinaryPrimitives.ReadUInt32LittleEndian(descriptor[sizeof(uint)..]) != ShaderVersion ||
descriptor[(int)ShaderTypeOffset] != requiredContinuationType)
{
continue;
}
var continuationCodeAddress = BinaryPrimitives.ReadUInt64LittleEndian(
descriptor[(int)ShaderCodeOffset..]);
var continuationSize = BinaryPrimitives.ReadUInt32LittleEndian(
descriptor[(int)ShaderSizeOffset..]);
if (continuationCodeAddress <= entryCodeAddress ||
continuationCodeAddress - entryCodeAddress > uint.MaxValue ||
!IsValidDeclaredShaderSize(continuationSize) ||
!CanReadShaderRange(ctx, continuationCodeAddress, continuationSize))
{
continue;
}
var continuationSpecialsAddress = BinaryPrimitives.ReadUInt64LittleEndian(
descriptor[(int)ShaderSpecialsOffset..]);
if (entrySpecialsAddress != 0 && continuationSpecialsAddress != 0)
{
if (!TryReadUInt32(
ctx,
entrySpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
out var entryStages) ||
!TryReadUInt32(
ctx,
continuationSpecialsAddress + ShaderSpecialVgtShaderStagesEnOffset + sizeof(uint),
out var continuationStages) ||
((entryStages ^ continuationStages) & waveSizeBit) != 0)
{
continue;
}
}
var distance = continuationCodeAddress - entryCodeAddress;
if (distance >= bestDistance)
{
continue;
}
bestDistance = distance;
bestCodeAddress = continuationCodeAddress;
bestHeaderAddress = scanAddress + (ulong)offset;
found = true;
}
var distance = continuationCodeAddress - entryCodeAddress;
if (distance >= bestDistance)
{
continue;
}
return found;
}
bestDistance = distance;
bestCodeAddress = continuationCodeAddress;
bestHeaderAddress = entryCodeAddress + (ulong)offset;
// Most uploads place the second descriptor beside the code. Some
// titles keep both descriptors in the header allocation instead, so
// inspect that region as a fallback.
ScanForContinuation(entryCodeAddress);
if (bestHeaderAddress == 0)
{
ScanForContinuation(entryHeaderAddress);
}
if (bestHeaderAddress == 0)
@ -335,7 +354,17 @@ public static partial class AgcExports
ExportName = "sceAgcFuseShaderHalves",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int FuseShaderHalves(CpuContext ctx)
public static int FuseShaderHalves(CpuContext ctx) => FuseShaderHalvesCore(ctx, legacy: false);
#pragma warning restore SHEM004
[SysAbiExport(
Nid = "nApJjpKNBl4",
ExportName = "sceAgcFuseShaderHalves",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int FuseShaderHalvesLegacy(CpuContext ctx) => FuseShaderHalvesCore(ctx, legacy: true);
private static int FuseShaderHalvesCore(CpuContext ctx, bool legacy)
{
var fusedAddress = ctx[CpuRegister.Rdi];
var frontAddress = ctx[CpuRegister.Rsi];
@ -387,6 +416,12 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
ulong userDataAddress = 0;
if (legacy && !TryReadUInt64(ctx, frontAddress + ShaderUserDataOffset, out userDataAddress))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Span<byte> header = stackalloc byte[ShaderStructBytes];
if (!ctx.Memory.TryRead(backAddress, header) ||
!ctx.Memory.TryWrite(fusedAddress, header))
@ -408,13 +443,13 @@ public static partial class AgcExports
}
if (!TryWriteByte(ctx, fusedAddress + ShaderTypeOffset, isGeometryPair ? GsShaderType : HsShaderType) ||
!ctx.TryWriteUInt64(fusedAddress + ShaderUserDataOffset, 0) ||
!ctx.TryWriteUInt64(fusedAddress + ShaderUserDataOffset, userDataAddress) ||
!ctx.TryWriteUInt64(fusedAddress + ShaderShRegistersOffset, fusedRegistersAddress))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (isGeometryPair)
if (isGeometryPair || legacy)
{
if (!TryReadUInt64(ctx, frontAddress + ShaderShRegistersOffset, out var frontRegistersAddress) ||
!TryReadByte(ctx, frontAddress + ShaderNumShRegistersOffset, out var frontRegisterCount))
@ -422,10 +457,19 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var merged = legacy
? MergeLegacyFusedShaderRegisters(ctx, frontRegistersAddress, frontRegisterCount, fusedRegistersAddress, registerCount, isGeometryPair)
: MergeFusedUserScalarCount(ctx, frontRegistersAddress, frontRegisterCount, fusedRegistersAddress, registerCount);
if (!merged)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var checksumOffset = isGeometryPair ? SpiShaderPgmChksumGs : ShaderRegisterOffset.SpiShaderPgmChksumHs;
for (var occurrence = 0; occurrence < 2; occurrence++)
{
if (!TryFindShaderRegister(ctx, fusedRegistersAddress, registerCount, SpiShaderPgmChksumGs, occurrence, out var fusedEntry) ||
!TryFindShaderRegister(ctx, frontRegistersAddress, frontRegisterCount, SpiShaderPgmChksumGs, occurrence, out var frontEntry))
if (!TryFindShaderRegister(ctx, fusedRegistersAddress, registerCount, checksumOffset, occurrence, out var fusedEntry) ||
!TryFindShaderRegister(ctx, frontRegistersAddress, frontRegisterCount, checksumOffset, occurrence, out var frontEntry))
{
continue;
}
@ -463,7 +507,6 @@ public static partial class AgcExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
#pragma warning restore SHEM004
[SysAbiExport(
Nid = "D9sr1xGUriE",
@ -563,9 +606,15 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Span<byte> registers = stackalloc byte[InterpolantRegisterCount * InterpolantRegisterPairSize];
for (var i = 0; i < InterpolantRegisterCount; i++)
{
SetInterpolantRegister(registers, i, (uint)i);
}
if (inputSemanticsCount == 0)
{
if (!TryWriteIdentityInterpolantRegisters(ctx, registersAddress, 0))
if (!ctx.Memory.TryWrite(registersAddress, registers))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@ -593,82 +642,74 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
for (uint inputIndex = 0; inputIndex < inputSemanticsCount; inputIndex++)
Span<byte> inputWords = stackalloc byte[(int)inputSemanticsCount * sizeof(uint)];
var outputBytes = outputSemanticsCount * sizeof(uint);
var rentedOutputWords = outputBytes > 1024 ? ArrayPool<byte>.Shared.Rent(outputBytes) : null;
try
{
if (!TryReadUInt32(
ctx,
inputSemanticsAddress + (inputIndex * sizeof(uint)),
out var source))
var outputWords = rentedOutputWords is null ? stackalloc byte[outputBytes] : rentedOutputWords.AsSpan(0, outputBytes);
if (!ctx.Memory.TryRead(inputSemanticsAddress, inputWords) ||
(outputBytes != 0 && !ctx.Memory.TryRead(outputSemanticsAddress, outputWords)))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var hasMask = false;
var mask = 0u;
for (uint outputIndex = 0; outputIndex < outputSemanticsCount; outputIndex++)
for (var inputIndex = 0; inputIndex < (int)inputSemanticsCount; inputIndex++)
{
if (!TryReadUInt32(
ctx,
outputSemanticsAddress + (outputIndex * sizeof(uint)),
out var candidate))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var source = BinaryPrimitives.ReadUInt32LittleEndian(inputWords[(inputIndex * sizeof(uint))..]);
var hasMask = TryFindOutputSemantic(outputWords, source, out var mask);
if ((byte)candidate == (byte)source)
var mode = (source >> 20) & 0x3u;
uint flags;
if (mode == 0)
{
hasMask = true;
mask = candidate;
break;
}
}
var mode = (source >> 20) & 0x3u;
uint flags;
if (mode == 0)
{
flags = (((source >> 24) & 0x1u) | (hasMask ? 0u : 1u)) << 5;
flags = ApplyInterpolantTwoBitField(flags, source >> 28, 8);
}
else
{
flags = ((source << 4) & 0x0300_0000u) + 0x0008_0000u;
if (mode == 2)
{
flags &= 0xFFEF_FFDFu;
flags |= hasMask ? ((~(mask & source) >> 16) & 0x20u) : 0x20u;
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 8);
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 21);
flags = (((source >> 24) & 0x1u) | (hasMask ? 0u : 1u)) << 5;
flags = ApplyInterpolantTwoBitField(flags, source >> 28, 8);
}
else
{
if (hasMask)
flags = ((source << 4) & 0x0300_0000u) + 0x0008_0000u;
if (mode == 2)
{
var masked = mask & source;
flags = (flags & 0xFFFF_FFDFu) | ((masked >> 15) & 0x20u);
flags ^= 0x20u;
flags = (flags & 0xFFEF_FFFFu) | ((~masked >> 1) & 0x0010_0000u);
flags &= 0xFFEF_FFDFu;
flags |= hasMask ? ((~(mask & source) >> 16) & 0x20u) : 0x20u;
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 8);
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 21);
}
else
{
flags |= 0x0010_0020u;
if (hasMask)
{
var masked = mask & source;
flags = (flags & 0xFFFF_FFDFu) | ((masked >> 15) & 0x20u);
flags ^= 0x20u;
flags = (flags & 0xFFEF_FFFFu) | ((~masked >> 1) & 0x0010_0000u);
}
else
{
flags |= 0x0010_0020u;
}
flags = ApplyInterpolantTwoBitField(flags, source >> 28, 8);
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 21);
}
flags = ApplyInterpolantTwoBitField(flags, source >> 28, 8);
flags = ApplyInterpolantTwoBitField(flags, source >> 30, 21);
}
}
flags = hasMask
? ApplyInterpolantFinalMask(flags, source, mask)
: flags & 0xFFFF_FBE0u;
if (!TryWriteInterpolantRegister(ctx, registersAddress, inputIndex, flags))
flags = hasMask
? ApplyInterpolantFinalMask(flags, source, mask)
: flags & 0xFFFF_FBE0u;
SetInterpolantRegister(registers, inputIndex, flags);
}
}
finally
{
if (rentedOutputWords is not null)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
ArrayPool<byte>.Shared.Return(rentedOutputWords);
}
}
if (!TryWriteIdentityInterpolantRegisters(ctx, registersAddress, inputSemanticsCount))
if (!ctx.Memory.TryWrite(registersAddress, registers))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@ -784,9 +825,17 @@ public static partial class AgcExports
}
}
// The 32 (register, value) pairs are built here and written with one guest write; titles
// call this per draw, and 64 separate 4-byte writes each paid the guest write path.
Span<byte> registers = stackalloc byte[InterpolantRegisterCount * InterpolantRegisterPairSize];
for (var i = 0; i < InterpolantRegisterCount; i++)
{
SetInterpolantRegister(registers, i, (uint)i);
}
if (inputSemanticsCount == 0 || inputSemanticsAddress == 0)
{
if (!TryWriteIdentityInterpolantRegisters(ctx, registersAddress, 0))
if (!ctx.Memory.TryWrite(registersAddress, registers))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@ -810,53 +859,48 @@ public static partial class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
inputSemanticsCount = Math.Min(inputSemanticsCount, 32u);
for (uint psIndex = 0; psIndex < inputSemanticsCount; psIndex++)
inputSemanticsCount = Math.Min(inputSemanticsCount, (uint)InterpolantRegisterCount);
Span<byte> psWords = stackalloc byte[(int)inputSemanticsCount * sizeof(uint)];
if (!ctx.Memory.TryRead(inputSemanticsAddress, psWords))
{
if (!TryReadUInt32(
ctx,
inputSemanticsAddress + (psIndex * sizeof(uint)),
out var psWord))
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var gsBytes = (outputSemanticsAddress != 0 ? outputSemanticsCount : 0) * sizeof(uint);
var rentedGsWords = gsBytes > 1024 ? ArrayPool<byte>.Shared.Rent(gsBytes) : null;
try
{
var gsWords = rentedGsWords is null ? stackalloc byte[gsBytes] : rentedGsWords.AsSpan(0, gsBytes);
if (gsBytes != 0 && !ctx.Memory.TryRead(outputSemanticsAddress, gsWords))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var psSemantic = psWord & 0xFFu;
uint? gsWord = null;
if (outputSemanticsAddress != 0)
for (var psIndex = 0; psIndex < (int)inputSemanticsCount; psIndex++)
{
for (uint gsIndex = 0; gsIndex < outputSemanticsCount; gsIndex++)
{
if (!TryReadUInt32(
ctx,
outputSemanticsAddress + (gsIndex * sizeof(uint)),
out var candidate))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var psWord = BinaryPrimitives.ReadUInt32LittleEndian(psWords[(psIndex * sizeof(uint))..]);
uint? gsWord = TryFindOutputSemantic(gsWords, psWord, out var candidate) ? candidate : null;
if ((candidate & 0xFFu) == psSemantic)
{
gsWord = candidate;
break;
}
}
var value = (psWord & 0x0030_0000u) != 0
? CreateInterpolantF16Value(psWord, gsWord)
: CreateInterpolantNonF16Value(psWord, gsWord.HasValue);
SetInterpolantRegister(
registers,
psIndex,
gsWord is { } matched
? CreateInterpolantMappingValue(value, psWord, matched)
: CreateInterpolantDefaultParamValue(value, psWord));
}
var value = (psWord & 0x0030_0000u) != 0
? CreateInterpolantF16Value(psWord, gsWord)
: CreateInterpolantNonF16Value(psWord, gsWord.HasValue);
value = gsWord is { } matched
? CreateInterpolantMappingValue(value, psWord, matched)
: CreateInterpolantDefaultParamValue(value, psWord);
if (!TryWriteInterpolantRegister(ctx, registersAddress, psIndex, value))
}
finally
{
if (rentedGsWords is not null)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
ArrayPool<byte>.Shared.Return(rentedGsWords);
}
}
if (!TryWriteIdentityInterpolantRegisters(ctx, registersAddress, inputSemanticsCount))
if (!ctx.Memory.TryWrite(registersAddress, registers))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
@ -935,31 +979,31 @@ public static partial class AgcExports
return value;
}
private static bool TryWriteInterpolantRegister(
CpuContext ctx,
ulong registersAddress,
uint index,
uint value)
{
var destination = registersAddress + (index * 8);
return TryWriteUInt32(ctx, destination, SpiPsInputCntl0 + index) &&
TryWriteUInt32(ctx, destination + sizeof(uint), value);
}
private const int InterpolantRegisterCount = 32;
private const int InterpolantRegisterPairSize = 2 * sizeof(uint);
private static bool TryWriteIdentityInterpolantRegisters(
CpuContext ctx,
ulong registersAddress,
uint firstIndex)
// The output (GS/VS export) semantic word whose semantic id (low byte) matches an input's.
private static bool TryFindOutputSemantic(ReadOnlySpan<byte> outputWords, uint inputWord, out uint outputWord)
{
for (uint i = firstIndex; i < 32u; i++)
for (var offset = 0; offset + sizeof(uint) <= outputWords.Length; offset += sizeof(uint))
{
if (!TryWriteInterpolantRegister(ctx, registersAddress, i, i))
outputWord = BinaryPrimitives.ReadUInt32LittleEndian(outputWords[offset..]);
if ((byte)outputWord == (byte)inputWord)
{
return false;
return true;
}
}
return true;
outputWord = 0;
return false;
}
// Slot i is the pair (SPI_PS_INPUT_CNTL_0 + i, value).
private static void SetInterpolantRegister(Span<byte> registers, int index, uint value)
{
var pair = registers[(index * InterpolantRegisterPairSize)..];
BinaryPrimitives.WriteUInt32LittleEndian(pair, SpiPsInputCntl0 + (uint)index);
BinaryPrimitives.WriteUInt32LittleEndian(pair[sizeof(uint)..], value);
}
private static OrbisGen2Result PatchShaderProgramRegisters(CpuContext context, ulong headerAddress, ulong codeAddress)
@ -1216,6 +1260,67 @@ public static partial class AgcExports
(frontType == GsFrontShaderType && backType == GsBackShaderType) ||
(frontType == HsFrontShaderType && backType == HsBackShaderType);
private static uint UserScalarCount(uint rsrc2) => ((rsrc2 >> 1) & 0x1Fu) | (((rsrc2 >> 27) & 1u) << 5);
private static bool MergeFusedUserScalarCount(CpuContext ctx, ulong frontRegisters, int frontCount, ulong fusedRegisters, int fusedCount)
{
if (!TryFindShaderRegister(ctx, frontRegisters, frontCount, SpiShaderPgmRsrc2Gs, 0, out var frontEntry) ||
!TryFindShaderRegister(ctx, fusedRegisters, fusedCount, SpiShaderPgmRsrc2Gs, 0, out var fusedEntry))
{
return true;
}
if (!TryReadUInt32(ctx, frontEntry + sizeof(uint), out var front) ||
!TryReadUInt32(ctx, fusedEntry + sizeof(uint), out var fused))
{
return false;
}
var count = Math.Max(UserScalarCount(front), UserScalarCount(fused));
var merged = (fused & ~((0x1Fu << 1) | (1u << 27))) | ((count & 0x1Fu) << 1) | ((count >> 5) << 27);
return merged == fused || TryWriteUInt32(ctx, fusedEntry + sizeof(uint), merged);
}
private static bool MergeLegacyFusedShaderRegisters(
CpuContext ctx, ulong frontRegisters, int frontCount, ulong fusedRegisters, int fusedCount, bool geometry)
{
var rsrc1Offset = geometry ? SpiShaderPgmRsrc1Gs : SpiShaderPgmRsrc1Hs;
var rsrc2Offset = geometry ? SpiShaderPgmRsrc2Gs : ShaderRegisterOffset.SpiShaderPgmRsrc2Hs;
if (!TryFindShaderRegister(ctx, frontRegisters, frontCount, rsrc1Offset, 0, out var frontEntry1) ||
!TryFindShaderRegister(ctx, frontRegisters, frontCount, rsrc2Offset, 0, out var frontEntry2) ||
!TryFindShaderRegister(ctx, fusedRegisters, fusedCount, rsrc1Offset, 0, out var fusedEntry1) ||
!TryFindShaderRegister(ctx, fusedRegisters, fusedCount, rsrc2Offset, 0, out var fusedEntry2))
{
return true;
}
if (!TryReadUInt32(ctx, frontEntry1 + sizeof(uint), out var front1) ||
!TryReadUInt32(ctx, frontEntry2 + sizeof(uint), out var front2) ||
!TryReadUInt32(ctx, fusedEntry1 + sizeof(uint), out var fused1) ||
!TryReadUInt32(ctx, fusedEntry2 + sizeof(uint), out var fused2))
{
return false;
}
// The older export takes maximum allocations, but user SGPRs and the
// ES component flag belong to the front half, not the back half.
fused1 = MergeMaxField(fused1, front1, 0, 0x3Fu);
fused1 = MergeMaxField(fused1, front1, geometry ? 29 : 28, 3u);
fused2 = MergeMaxField(fused2, front2, 28, 0xFu);
if (geometry)
{
fused2 = MergeMaxField(fused2, front2, 16, 3u);
fused2 = (fused2 & ~0x0004_0000u) | (front2 & 0x0004_0000u);
}
fused2 = (fused2 & ~0x0800_003Eu) | (front2 & 0x0800_003Eu);
return TryWriteUInt32(ctx, fusedEntry1 + sizeof(uint), fused1) &&
TryWriteUInt32(ctx, fusedEntry2 + sizeof(uint), fused2);
static uint MergeMaxField(uint destination, uint source, int shift, uint mask) =>
(destination & ~(mask << shift)) | (Math.Max((destination >> shift) & mask, (source >> shift) & mask) << shift);
}
private static bool TryFindShaderRegister(
CpuContext ctx,
ulong registersAddress,

View File

@ -99,6 +99,7 @@ public static partial class AgcExports
private const uint SpiShaderPgmLoGs = 0x88;
private const uint SpiShaderPgmHiGs = 0x89;
private const uint SpiShaderPgmRsrc1Gs = 0x8A;
private const uint SpiShaderPgmRsrc2Gs = 0x8B;
private const uint SpiShaderPgmChksumGs = 0x80;
private const uint SpiShaderPgmChksumPs = 0x06;
private const uint SpiPsInputEna = 0x1B3;
@ -200,10 +201,10 @@ public static partial class AgcExports
private const ulong CommandBufferCallbackOffset = 0x20;
private const ulong CommandBufferUserDataOffset = 0x28;
private const ulong CommandBufferReservedDwOffset = 0x30;
private static readonly object _submitTraceGate = new();
// Every PM4 opcode with no handler, logged once with its first two
// payload dwords as a possible target address.
private static readonly Dictionary<ulong, ulong> _shaderHeadersByCode = new();
// Read by the render thread for every draw; written by guest threads creating shaders.
private static readonly System.Collections.Concurrent.ConcurrentDictionary<ulong, ulong> _shaderHeadersByCode = new();
private static readonly bool _traceAgc = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"),
"1",

View File

@ -965,6 +965,8 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int _unknownReadFileIdWarnings;
private static int CompleteReadFileRecord(CpuContext ctx, ulong commandBuffer, ReadFileCommand command)
{
var fileId = command.FileId;
@ -982,6 +984,13 @@ public static class AmprExports
if (!AmprFileRegistry.TryGetHostPath(fileId, out hostPath))
{
// An unknown id stalls the guest's stream silently, so warn even without tracing.
if (Interlocked.Increment(ref _unknownReadFileIdWarnings) <= 16)
{
Console.Error.WriteLine(
$"[LOADER][WARN] ampr.read_unknown_file_id id=0x{fileId:X8} size=0x{size:X} offset=0x{fileOffset:X}");
}
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}

View File

@ -38,6 +38,9 @@ internal static class AmprFileRegistry
private static readonly object _resolvedFileGate = new();
private static readonly Dictionary<string, uint> _resolvedIdsByPath = new(HostFsPath.Comparer);
private static readonly ConcurrentDictionary<uint, string> _resolvedPathsById = new();
// Ids already handed to the guest; alias publishes must not re-poison them.
private static readonly ConcurrentDictionary<uint, string> _aprResolvedPathsById = new();
private static readonly ConcurrentDictionary<uint, byte> _loggedAprCollisionIds = new();
// Resolved handles use a separate range from the 31-bit compatibility hashes.
// Never reuse a handle while queued reads can still refer to it.
private static uint _nextResolvedId = 0x80000000;
@ -67,24 +70,52 @@ internal static class AmprFileRegistry
public static uint RegisterAprResolvedPath(string guestPath, string hostPath)
{
if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
{
RegisterApp0Relative(relative, hostPath);
}
// APR file ids are part of the guest ABI: ResolveFilepathsToIds returns
// the 31-bit FNV-1a hash of the guest path. Keep the collision-safe
// process-local handles used by Register() separate from this path so a
// title can compare resolved ids with ids baked into its asset tables.
// A hash shared by two files (13 /app0/ pairs in Demon's Souls) falls back to a handle.
var fileId = ComputeFileId(guestPath);
PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
return fileId;
lock (_resolvedFileGate)
{
if (_aprResolvedPathsById.TryGetValue(fileId, out var claimedPath))
{
if (IsSameHostFile(claimedPath, hostPath))
return fileId;
}
else if (!_hostPathsById.TryGetValue(fileId, out var indexed) ||
(!indexed.Ambiguous && IsSameHostFile(indexed.HostPath, hostPath)))
{
if (TryGetApp0Relative(guestPath, out var relative) && relative.Length != 0)
{
RegisterApp0Relative(relative, hostPath);
}
PublishCompatibilityPath(fileId, hostPath, AliasRank.Resolved);
_aprResolvedPathsById[fileId] = hostPath;
return fileId;
}
}
var handle = Register(guestPath, hostPath);
if (_loggedAprCollisionIds.TryAdd(fileId, 0))
{
Console.Error.WriteLine(
$"[LOADER][WARN] ampr.apr_id_collision id=0x{fileId:X8} path='{guestPath}' handle=0x{handle:X8}");
}
return handle;
}
private static bool IsSameHostFile(string left, string right) =>
HostFsPath.Comparer.Equals(Path.GetFullPath(left), Path.GetFullPath(right));
public static bool TryGetHostPath(uint id, out string hostPath)
{
if ((id & 0x80000000) != 0)
return _resolvedPathsById.TryGetValue(id, out hostPath!);
if (_aprResolvedPathsById.TryGetValue(id, out hostPath!))
return true;
if (_hostPathsById.TryGetValue(id, out var entry) && !entry.Ambiguous)
{
hostPath = entry.HostPath;
@ -101,8 +132,10 @@ internal static class AmprFileRegistry
lock (_indexGate)
{
_hostPathsById.Clear();
_loggedAprCollisionIds.Clear();
lock (_resolvedFileGate)
{
_aprResolvedPathsById.Clear();
_resolvedIdsByPath.Clear();
_resolvedPathsById.Clear();
_nextResolvedId = 0x80000000;
@ -417,7 +450,23 @@ internal static class AmprFileRegistry
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SharpEmu",
"ampr-index");
Directory.CreateDirectory(cacheDir);
try
{
Directory.CreateDirectory(cacheDir);
var probePath = Path.Combine(cacheDir, $".write-test-{Environment.ProcessId}");
using (File.Create(probePath)) { }
File.Delete(probePath);
}
catch (Exception exception) when (exception is UnauthorizedAccessException or IOException)
{
// Sandboxed launches and mitigated child processes can lose access
// to the profile AppData directory. Keep the index beside the
// executable so the next launch can still reuse it.
cacheDir = Path.Combine(AppContext.BaseDirectory, "user", "ampr-index");
Directory.CreateDirectory(cacheDir);
Console.Error.WriteLine(
$"[LOADER][WARN] ampr.app0_index_cache_fallback path={cacheDir}: {exception.Message}");
}
// Distinct roots must not share a cache file. Folding case is only
// correct where the host filesystem folds it too.

View File

@ -60,10 +60,19 @@ internal static class PakDirectoryTracker
// related assets, and the guest streams them with locality, so disambiguate collisions
// by choosing the unconsumed match nearest the running read cursor.
DirectoryEntry? best = null;
DirectoryEntry? uniqueMatch = null;
var matchCount = 0;
var bestDistance = ulong.MaxValue;
foreach (var entry in entries)
{
if (entry.Consumed || entry.FileLen != requestedSize)
if (entry.FileLen != requestedSize)
{
continue;
}
uniqueMatch = entry;
matchCount++;
if (entry.Consumed)
{
continue;
}
@ -90,6 +99,12 @@ internal static class PakDirectoryTracker
return best.FilePos;
}
// A unique archive member may be opened again after its first read.
if (matchCount == 1)
{
return uniqueMatch!.FilePos;
}
}
return state.NextOffset;

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers.Binary;
using System.Text;
using System.Text.Json;
@ -11,6 +12,9 @@ namespace SharpEmu.Libs.AppContent;
public static class AppContentExports
{
private const ulong FallbackAvailableSpaceKb = 1024UL * 1024UL; // 1 GiB
private const int AppContentErrorParameter = unchecked((int)0x80D90002);
private const int AppContentErrorDrmNoEntitlement = unchecked((int)0x80D90007);
private const int MountPointSize = 16;
private const ulong BootParamAttrOffset = 4;
private const string Temp0MountPoint = "/temp0";
private const uint AppParamSkuFlag = 0;
@ -98,6 +102,29 @@ public static class AppContentExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "VANhIWcqYak",
ExportName = "sceAppContentAddcontMount",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentAddcontMount(CpuContext ctx)
{
var entitlementLabelAddress = ctx[CpuRegister.Rsi];
var mountPointAddress = ctx[CpuRegister.Rdx];
if (entitlementLabelAddress == 0 || mountPointAddress == 0)
{
return ctx.SetReturn(AppContentErrorParameter);
}
Span<byte> emptyMountPoint = stackalloc byte[MountPointSize];
if (!ctx.Memory.TryWrite(mountPointAddress, emptyMountPoint))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return ctx.SetReturn(AppContentErrorDrmNoEntitlement);
}
[SysAbiExport(
Nid = "buYbeLOGWmA",
ExportName = "sceAppContentTemporaryDataMount2",
@ -111,7 +138,7 @@ public static class AppContentExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Directory.CreateDirectory(ResolveTemp0Root());
Directory.CreateDirectory(KernelMemoryCompatExports.ResolveTemp0Root());
var mountPointBytes = Encoding.ASCII.GetBytes($"{Temp0MountPoint}\0");
if (!ctx.Memory.TryWrite(mountPointAddress, mountPointBytes))
{
@ -163,7 +190,7 @@ public static class AppContentExports
{
try
{
var storageRoot = ResolveTemp0Root();
var storageRoot = KernelMemoryCompatExports.ResolveTemp0Root();
Directory.CreateDirectory(storageRoot);
var volumeRoot = Path.GetPathRoot(Path.GetFullPath(storageRoot));
if (!string.IsNullOrWhiteSpace(volumeRoot))
@ -240,28 +267,4 @@ public static class AppContentExports
Console.Error.WriteLine($"[LOADER][TRACE] app_content.{message}");
}
private static string ResolveTemp0Root()
{
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName);
if (!string.IsNullOrWhiteSpace(configuredRoot))
{
return Path.GetFullPath(configuredRoot);
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
var appName = string.IsNullOrWhiteSpace(app0Root)
? "default"
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
if (string.IsNullOrWhiteSpace(appName))
{
appName = "default";
}
var invalidChars = Path.GetInvalidFileNameChars();
appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
var root = Path.Combine(AppContext.BaseDirectory, "user", "temp", appName, "temp0");
Environment.SetEnvironmentVariable(temp0VariableName, root);
return root;
}
}

View File

@ -590,7 +590,16 @@ public static class AjmExports
return ctx.SetReturn(OrbisAjmErrorJobCreation);
}
var status = TryGetInstance(instanceId, out _) ? 0 : Atrac9DecodeState.ResultInvalidParameter;
var status = TryGetInstance(instanceId, out var instance) ? 0 : Atrac9DecodeState.ResultInvalidParameter;
Span<byte> gapless = stackalloc byte[AjmSidebandGaplessDecodeBytes];
if (status == 0 && gaplessAddress != 0 && ctx.Memory.TryRead(gaplessAddress, gapless))
{
instance!.Atrac9?.SetGapless(
BinaryPrimitives.ReadUInt32LittleEndian(gapless),
BinaryPrimitives.ReadUInt16LittleEndian(gapless[4..]),
reset != 0);
}
WriteBasicResult(ctx, resultAddress, status);
Trace(
$"batch_job_set_gapless_decode instance=0x{instanceId:X8} " +
@ -1163,6 +1172,10 @@ public static class AjmExports
if ((flags & AjmJobSidebandFlagGaplessDecode) != 0 && (ulong)(offset + AjmSidebandGaplessDecodeBytes) <= size)
{
var gapless = instance?.Atrac9?.Gapless ?? default;
BinaryPrimitives.WriteUInt32LittleEndian(sideband[offset..], gapless.TotalSamples);
BinaryPrimitives.WriteUInt16LittleEndian(sideband[(offset + 4)..], gapless.SkipSamples);
BinaryPrimitives.WriteUInt16LittleEndian(sideband[(offset + 6)..], gapless.SkippedSamples);
offset += AjmSidebandGaplessDecodeBytes;
}
@ -1318,8 +1331,12 @@ public static class AjmExports
TryWriteUInt64(ctx, infoAddress + AjmBatchInfoOffsetField, offset + jobSize);
}
// AjmBatchError: int error_code; const void* job_addr; uint32_t cmd_offset; const void* job_ra;
private const int AjmBatchErrorBytes = 24;
// SceAjmBatchError { int iErrorCode; const void *pJobAddress;
// unsigned int uiCommandOffset; const void *pJobOriginRa; }. The two pointers
// force 8-byte alignment, so the struct is 0x20 bytes (4+4 pad, 8, 4+4 pad, 8)
// rather than the 24-byte naive field sum — pJobOriginRa lives at +0x18 and was
// left holding stale guest bytes.
private const int AjmBatchErrorBytes = 0x20;
private static void ClearAjmBatchError(CpuContext ctx, ulong errorAddress)
{

View File

@ -20,6 +20,8 @@ internal readonly record struct Atrac9DecodeResult(
ulong TotalDecodedSamples,
uint Frames);
internal readonly record struct Atrac9Gapless(uint TotalSamples, ushort SkipSamples, ushort SkippedSamples);
internal sealed class Atrac9DecodeState
{
internal const int ResultNotInitialized = 0x00000001;
@ -47,6 +49,33 @@ internal sealed class Atrac9DecodeState
private int _containerHeaderLength;
private int _compressedLength;
private ulong _totalDecodedSamples;
private Atrac9Gapless _gaplessInit;
private Atrac9Gapless _gapless;
public Atrac9Gapless Gapless
{
get
{
lock (_gate)
{
return _gapless;
}
}
}
public void SetGapless(uint totalSamples, ushort skipSamples, bool reset)
{
lock (_gate)
{
_gaplessInit = new Atrac9Gapless(totalSamples, skipSamples, 0);
if (reset || _gapless == default)
{
_gapless = _gaplessInit;
}
}
}
private bool HasSampleLimit => _gaplessInit.TotalSamples is not 0 and not uint.MaxValue;
public Atrac9Config? Config
{
@ -82,6 +111,7 @@ internal sealed class Atrac9DecodeState
_compressedLength = 0;
_totalDecodedSamples = 0;
_containerHeaderLength = 0;
_gapless = _gaplessInit;
Trace(
$"initialized config={Convert.ToHexString(normalizedConfig)} channels={config.ChannelCount} " +
$"rate={config.SampleRate} frame_samples={config.FrameSamples} " +
@ -114,6 +144,7 @@ internal sealed class Atrac9DecodeState
_compressedLength = 0;
_totalDecodedSamples = 0;
_containerHeaderLength = 0;
_gapless = _gaplessInit;
if (_compressed is not null)
{
Array.Clear(_compressed);
@ -153,7 +184,6 @@ internal sealed class Atrac9DecodeState
}
var bytesPerSample = GetBytesPerSample(encoding);
var outputBytesPerSuperframe = checked(config.SuperframeSamples * channels * bytesPerSample);
var consumed = 0;
var written = 0;
uint frames = 0;
@ -162,6 +192,11 @@ internal sealed class Atrac9DecodeState
while (_compressedLength == config.SuperframeBytes ||
consumed < input.Length)
{
if (HasSampleLimit && _gapless.TotalSamples == 0)
{
break;
}
// Titles that stream whole .at9 files hand AJM the RIFF/WAVE
// container rather than a pointer into its `data` chunk, so the
// stream has to be advanced past the header before the first
@ -202,7 +237,15 @@ internal sealed class Atrac9DecodeState
break;
}
if (output.Length - written < outputBytesPerSuperframe)
var skip = Math.Min(config.SuperframeSamples, (int)_gapless.SkipSamples);
var samples = config.SuperframeSamples - skip;
if (HasSampleLimit)
{
samples = (int)Math.Min((uint)samples, _gapless.TotalSamples);
}
var outputBytes = checked(samples * channels * bytesPerSample);
if (output.Length - written < outputBytes)
{
status |= ResultNotEnoughRoom;
break;
@ -231,14 +274,19 @@ internal sealed class Atrac9DecodeState
WriteInterleaved(
_planarPcm,
output.Slice(written, outputBytesPerSuperframe),
config.SuperframeSamples,
output.Slice(written, outputBytes),
skip,
samples,
channels,
encoding);
written += outputBytesPerSuperframe;
_gapless = new Atrac9Gapless(
HasSampleLimit ? _gapless.TotalSamples - (uint)samples : _gapless.TotalSamples,
(ushort)(_gapless.SkipSamples - skip),
(ushort)Math.Min(ushort.MaxValue, _gapless.SkippedSamples + config.SuperframeSamples - samples));
written += outputBytes;
_compressedLength = 0;
_totalDecodedSamples += unchecked((uint)config.SuperframeSamples);
_totalDecodedSamples += unchecked((uint)samples);
frames += unchecked((uint)config.FramesPerSuperframe);
if (!multipleFrames)
@ -353,12 +401,13 @@ internal sealed class Atrac9DecodeState
private static void WriteInterleaved(
short[][] source,
Span<byte> destination,
int firstSample,
int samples,
int channels,
Atrac9PcmEncoding encoding)
{
var offset = 0;
for (var sample = 0; sample < samples; sample++)
for (var sample = firstSample; sample < firstSample + samples; sample++)
{
for (var channel = 0; channel < channels; channel++)
{

View File

@ -0,0 +1,18 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Audio;
public static class AudioInExports
{
[SysAbiExport(
Nid = "BohEAQ7DlUE",
ExportName = "sceAudioInGetSilentState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioIn")]
public static int GetSilentState(CpuContext ctx) =>
// No AudioIn ports can be opened until an input backend is implemented.
ctx.SetReturn(unchecked((int)0x80260101));
}

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,13 @@ namespace SharpEmu.Libs.Audio;
public static class AudioOutExports
{
// ABI: SceAudioOutOutputParam { int32_t handle; int32_t : 32; void *ptr; }.
private const int AudioOutOutputParamSize = 16;
private const int AudioOutMaximumOutputCount = 25;
// ABI: SceAudioOutPortState { u16 output; u8 channel; u8 reserved8_1[1];
// s16 volume; u16 rerouteCounter; u64 flag; u64 reserved64[2]; }.
private const int AudioOutPortStateSize = 0x20;
private const ushort AudioOutStateOutputConnectedPrimary = 1 << 0;
internal const int AudioOutErrorInvalidPort = unchecked((int)0x80260003);
internal const int AudioOutErrorInvalidPointer = unchecked((int)0x80260004);
@ -229,34 +234,30 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// Same rule as AudioOut2 PortGetState: never bulk-write onto the caller
// stack. Some titles place small locals next to the canary; a full
// SceAudioOutPortState write smashes it.
if (IsGuestStackAddress(stateAddress))
{
return ctx.SetReturn(0);
}
// SceAudioOutPortState: report a connected primary output at full volume
// so pacing/mixing code sees a live port. We do no host rerouting, so
// rerouteCounter and flag stay zero.
Span<byte> state = stackalloc byte[16];
// ABI: int32_t sceAudioOutGetPortState(int32_t handle,
// SceAudioOutPortState *state) — rsi is the only out pointer and the write
// is exactly sizeof(SceAudioOutPortState):
// +0x00 u16 output = STATE_OUTPUT_CONNECTED_PRIMARY
// +0x02 u8 channel = SceAudioOutStateChannel
// +0x03 u8 reserved8_1[1]
// +0x04 s16 volume = -1 (invalid outside PADSPK)
// +0x06 u16 rerouteCounter = 0 (the host mixer never reroutes)
// +0x08 u64 flag = SCE_AUDIO_OUT_STATE_FLAG_NONE
// +0x10 u64 reserved64[2]
Span<byte> state = stackalloc byte[AudioOutPortStateSize];
state.Clear();
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(state, 1);
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(
state[2..], (ushort)port.Channels);
state[7] = 127;
state, AudioOutStateOutputConnectedPrimary);
state[2] = (byte)Math.Clamp(port.Channels, 0, 8);
System.Buffers.Binary.BinaryPrimitives.WriteInt16LittleEndian(state[4..], -1);
if (!ctx.Memory.TryWrite(stateAddress, state))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return ctx.SetReturn(AudioOutErrorInvalidPointer);
}
return ctx.SetReturn(0);
}
private static bool IsGuestStackAddress(ulong value) =>
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
[SysAbiExport(
Nid = "w3PdaSTSwGE",
ExportName = "sceAudioOutOutputs",

View File

@ -17,9 +17,21 @@ public static class AudioPropagationExports
{
private const int Ok = 0;
// QueryMemory reports the working-set size the caller must allocate before
// SystemCreate. rsi points at the out size/alignment; write a modest,
// aligned block so the caller's allocation succeeds.
// ABI: int32_t sceAudioPropagationSystemQueryMemory(
// const SceAudioPropagationSystemOption *pOptions,
// SceAudioPropagationSystemMemory *pOutMemorySize).
// SceAudioPropagationSystemMemory is NOT a {size, alignment} pair: it is
// +0x00 SceAudioPropagationStructDescriptor desc { uint32 id; size_t size; }
// +0x10 void *pCpuMem (the caller allocates and fills this)
// +0x18 size_t sizeCpuMem
// +0x20 void *pGpuMem (the caller allocates and fills this)
// +0x28 size_t sizeGpuMem
// so only the two size fields belong to this call. Writing {size, alignment}
// at +0x00/+0x08 overwrote the descriptor the caller had already initialized.
private const int SystemMemoryCpuSizeOffset = 0x18;
private const int SystemMemoryGpuSizeOffset = 0x28;
private const ulong SystemMemoryCpuBytes = 0x10_0000;
[SysAbiExport(
Nid = "7xyAxrusLko",
ExportName = "sceAudioPropagationSystemQueryMemory",
@ -28,13 +40,15 @@ public static class AudioPropagationExports
public static int SystemQueryMemory(CpuContext ctx)
{
var outAddress = ctx[CpuRegister.Rsi];
if (outAddress != 0)
if (outAddress == 0)
{
// {size, alignment} — 1 MiB / 256 B covers the caller's allocation.
ctx.TryWriteUInt64(outAddress, 0x10_0000);
ctx.TryWriteUInt64(outAddress + sizeof(ulong), 0x100);
// SCE_AUDIO_PROPAGATION_ERROR_INVALID_POINTER.
return ctx.SetReturn(unchecked((int)0x8A70_0003));
}
// No GPU (Acm) propagation path exists here, so the GPU working set is 0.
ctx.TryWriteUInt64(outAddress + SystemMemoryCpuSizeOffset, SystemMemoryCpuBytes);
ctx.TryWriteUInt64(outAddress + SystemMemoryGpuSizeOffset, 0);
return ctx.SetReturn(Ok);
}

View File

@ -351,6 +351,8 @@ public static class AvPlayerExports
: VideoFrameReadResult.Pending;
}
public int QueuedFrameCount => _frames.Reader.Count;
private void DecodeFrames()
{
try
@ -437,6 +439,7 @@ public static class AvPlayerExports
public uint AvSyncMode { get; set; } = AvSyncModeDefault;
public bool IsGen5 { get; init; }
public bool Started { get; set; }
public bool Stopped { get; set; }
public bool Paused { get; set; }
public bool Looping { get; set; }
public bool EndOfStream { get; set; }
@ -456,6 +459,12 @@ public static class AvPlayerExports
public ulong LastVideoTimestamp { get; set; }
public ulong EventDataBuffer { get; set; }
public long NextFrameIndex { get; set; }
public long VideoCalls { get; set; }
public long VideoDelivered { get; set; }
public long VideoPending { get; set; }
public long VideoSyncWait { get; set; }
public long VideoWriteFailures { get; set; }
public long VideoStatsSecond { get; set; } = -1;
public ulong AudioBufferBase { get; set; }
public int NextAudioBuffer { get; set; }
public long NextAudioFrameIndex { get; set; }
@ -498,6 +507,12 @@ public static class AvPlayerExports
DisposePlaybackResources();
PlaybackClock.Reset();
NextFrameIndex = 0;
VideoCalls = 0;
VideoDelivered = 0;
VideoPending = 0;
VideoSyncWait = 0;
VideoWriteFailures = 0;
VideoStatsSecond = -1;
LastGuestBuffer = 0;
LastVideoTimestamp = 0;
SeekVideoFramePending = false;
@ -540,7 +555,9 @@ public static class AvPlayerExports
{
Handle = handle,
IsGen5 = IsGen5Target(ctx.TargetGeneration),
AutoStart = TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
AutoStart = StartsAutomatically(
TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
TryReadUInt64(ctx, initDataAddress + 88, out var eventCallbackForStart) ? eventCallbackForStart : 0),
GuestBuffers = new ulong[ReadOutputVideoFrameBufferCount(
ctx,
initDataAddress,
@ -614,7 +631,9 @@ public static class AvPlayerExports
{
Handle = handle,
IsGen5 = IsGen5Target(ctx.TargetGeneration),
AutoStart = TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
AutoStart = StartsAutomatically(
TryReadByte(ctx, initDataAddress + autoStartOffset, out var autoStart) && autoStart != 0,
TryReadUInt64(ctx, initDataAddress + 96, out var eventCallbackForStart) ? eventCallbackForStart : 0),
GuestBuffers = new ulong[ReadOutputVideoFrameBufferCount(
ctx,
initDataAddress,
@ -724,6 +743,7 @@ public static class AvPlayerExports
player = foundPlayer;
player.Started = true;
player.Stopped = false;
player.Paused = false;
player.EndOfStream = false;
Trace($"start handle=0x{player.Handle:X16}");
@ -767,6 +787,7 @@ public static class AvPlayerExports
player.ResetPlayback();
player.Started = false;
player.Stopped = true;
}
NotifyEvent(ctx, player, 1); // StateStop
@ -793,6 +814,7 @@ public static class AvPlayerExports
player.PlaybackClock.Stop();
player.FallbackPlayback?.Pause();
Console.Error.WriteLine($"[AVPLAYER][INFO] pause handle=0x{player.Handle:X16}");
TraceVideoStats(player);
}
@ -859,6 +881,32 @@ public static class AvPlayerExports
}
}
[SysAbiExport(
Nid = "N6Oy-EjduiY",
ExportName = "sceAvPlayerSetAvailableBandwidth",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
[SysAbiExport(
Nid = "+7xJ+lFxmkQ",
ExportName = "sceAvPlayerSetAvailableBW",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAvPlayer")]
public static int AvPlayerSetAvailableBandwidth(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var availableBandwidth = ctx[CpuRegister.Rsi];
lock (StateGate)
{
if (!Players.ContainsKey(handle))
{
return SetReturn(ctx, InvalidParameters);
}
Trace($"set_available_bandwidth handle=0x{handle:X16} bandwidth={availableBandwidth}");
return SetReturn(ctx, 0);
}
}
[SysAbiExport(
Nid = "ODJK2sn9w4A",
ExportName = "sceAvPlayerEnableStream",
@ -1001,7 +1049,9 @@ public static class AvPlayerExports
lock (StateGate)
{
var found = Players.TryGetValue(ctx[CpuRegister.Rdi], out var player);
var active = found && player!.Started && !player.EndOfStream;
// Active from the moment a source is ready until it stops, fails or reaches its end,
// whether or not playback has started (titles poll it before calling sceAvPlayerStart).
var active = found && player!.SourcePath is not null && !player.Stopped && !player.EndOfStream;
TraceOnce(
"is_active",
$"is_active found={found} started={(found && player!.Started)} " +
@ -1388,6 +1438,7 @@ public static class AvPlayerExports
player.DurationMilliseconds = duration;
player.HasAudio = hasAudio;
player.Started = player.AutoStart;
player.Stopped = false;
autoStart = player.AutoStart;
Trace(
$"source guest='{guestPath}' host='{hostPath}' {width}x{height} " +
@ -1619,6 +1670,14 @@ public static class AvPlayerExports
return SetReturn(ctx, 0);
}
player.VideoCalls++;
var statsSecond = player.PlaybackClock.ElapsedMilliseconds / 1000;
if (statsSecond > player.VideoStatsSecond)
{
player.VideoStatsSecond = statsSecond;
TraceVideoStats(player);
}
if (player.Paused)
{
if (player.SeekVideoFramePending &&
@ -1658,47 +1717,26 @@ public static class AvPlayerExports
}
var fps = Math.Max(1.0, player.FramesPerSecond);
var expectedFrame = player.NextFrameIndex;
if (player.AvSyncMode == AvSyncModeDefault)
{
var expectedFrame = CalculateExpectedDefaultSyncVideoFrame(
expectedFrame = CalculateExpectedDefaultSyncVideoFrame(
player.HasAudio,
player.NextAudioFrameIndex,
player.PlaybackClock.Elapsed.TotalSeconds,
fps);
if (player.NextFrameIndex > expectedFrame)
{
player.VideoSyncWait++;
return SetReturn(ctx, 0);
}
var behind = expectedFrame - player.NextFrameIndex;
if (behind > MaxCatchUpFrames)
{
// Limit this call's work without changing the shared playback clock.
expectedFrame = player.NextFrameIndex + MaxCatchUpFrames;
TraceOnce(
"catch_up_capped",
$"catch_up capped behind={behind} max={MaxCatchUpFrames} " +
$"fps={fps:F3} {player.Width}x{player.Height}");
}
while (player.NextFrameIndex < expectedFrame)
{
var skippedFrame = ReadFrame(player);
if (skippedFrame == VideoFrameReadResult.Pending)
{
return SetReturn(ctx, 0);
}
if (skippedFrame == VideoFrameReadResult.End)
{
return FinishStream(ctx, player);
}
player.NextFrameIndex++;
}
}
var frameResult = ReadFrame(player);
if (frameResult == VideoFrameReadResult.Pending)
{
player.VideoPending++;
return SetReturn(ctx, 0);
}
if (frameResult == VideoFrameReadResult.End)
@ -1707,9 +1745,23 @@ public static class AvPlayerExports
}
var frameIndex = player.NextFrameIndex;
// Only discard old frames when a replacement is already decoded.
for (var skipped = 0;
skipped < MaxCatchUpFrames &&
player.NextFrameIndex < expectedFrame &&
player.VideoDecoder!.QueuedFrameCount > 1;
skipped++)
{
player.NextFrameIndex++;
if (ReadFrame(player) != VideoFrameReadResult.Ready)
{
break;
}
frameIndex = player.NextFrameIndex;
}
var timestamp = player.StartTimeMilliseconds +
checked((ulong)Math.Round(frameIndex * 1000.0 / fps));
player.NextFrameIndex++;
player.NextFrameIndex = frameIndex + 1;
if (!WriteVideoFrame(
ctx,
player,
@ -1718,15 +1770,28 @@ public static class AvPlayerExports
frameIndex,
extended))
{
player.VideoWriteFailures++;
return SetReturn(ctx, 0);
}
player.LastVideoTimestamp = timestamp;
player.VideoDelivered++;
Trace($"video_frame handle=0x{player.Handle:X16} ex={extended} ts={timestamp} data=0x{player.LastGuestBuffer:X16}");
return SetReturn(ctx, 1);
}
}
private static void TraceVideoStats(PlayerState player)
{
Console.Error.WriteLine(
$"[AVPLAYER][INFO] video_stats handle=0x{player.Handle:X16} " +
$"clock_ms={player.PlaybackClock.ElapsedMilliseconds} audio_blocks={player.NextAudioFrameIndex} " +
$"calls={player.VideoCalls} delivered={player.VideoDelivered} " +
$"pending={player.VideoPending} sync_wait={player.VideoSyncWait} " +
$"write_fail={player.VideoWriteFailures} next_frame={player.NextFrameIndex} " +
$"queued={player.VideoDecoder?.QueuedFrameCount ?? 0} last_ts={player.LastVideoTimestamp}");
}
private static int FinishStream(CpuContext ctx, PlayerState player)
{
TraceOnce(
@ -2614,6 +2679,13 @@ public static class AvPlayerExports
internal static bool IsGen5Target(Generation generation) =>
(generation & Generation.Gen5) != 0;
// A player plays on its own once its source is ready when the title asks for autoStart or
// registers no event callback: without one nothing could observe the ready state and start
// it. Such a title never calls sceAvPlayerStart and waits for the player to go inactive at the
// end of the stream, so leaving it unstarted kept it active forever.
internal static bool StartsAutomatically(bool autoStartRequested, ulong eventCallback) =>
autoStartRequested || eventCallback == 0;
internal static ulong GetAutoStartOffset(Generation generation, bool extended) =>
IsGen5Target(generation)
? extended ? 116UL : 108UL

View File

@ -7,6 +7,21 @@ using SharpEmu.Libs.VideoOut;
namespace SharpEmu.Libs.Codec;
internal sealed record Videodec2DecodedFrame(
byte[] Nv12,
uint Width,
uint Height,
uint Pitch,
bool ErrorFrame)
{
public ulong Pts { get; init; } = ulong.MaxValue;
public ulong Dts { get; init; } = ulong.MaxValue;
public ulong AttachedData { get; init; }
public bool KeyFrame { get; init; }
public uint Profile { get; init; }
public uint Level { get; init; }
}
/// <summary>
/// Owns one FFmpeg H.264 decode session for a single sceVideodec2 decoder
/// handle, feeding it pre-demuxed Annex-B access units from guest memory.
@ -29,6 +44,8 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
// Enough lookahead to absorb decode jitter without adding visible latency.
private const int FrameQueueCapacity = 4;
private const int WorkQueueCapacity = 64;
// Fallback when the stream doesn't declare a usable framerate.
private const double FallbackFps = 30.0;
@ -45,12 +62,12 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
private AVPixelFormat _swsSourceFormat = AVPixelFormat.AV_PIX_FMT_NONE;
private bool _disposed;
// Unbounded: access units are small, backpressure lives on the frame queue below.
private readonly Channel<byte[]?> _workChannel =
Channel.CreateUnbounded<byte[]?>(new UnboundedChannelOptions
Channel.CreateBounded<byte[]?>(new BoundedChannelOptions(WorkQueueCapacity)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = true,
SingleWriter = false,
});
// Bounded and blocking-on-full: the backpressure that keeps decode paced to playback.
@ -74,6 +91,10 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
private uint _lastWidth;
private uint _lastHeight;
private long _workerInputCount;
private long _workerFrameCount;
private static readonly bool TraceEnabled = IsTraceEnabled();
private Videodec2Decoder(AVCodecContext* codecContext, AVFrame* frame, AVPacket* packet)
{
_codecContext = codecContext;
@ -106,18 +127,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
var codec = ffmpeg.avcodec_find_decoder(AVCodecID.AV_CODEC_ID_H264);
if (codec == null)
{
Trace("create failed: H.264 decoder not found");
return null;
}
codecContext = ffmpeg.avcodec_alloc_context3(codec);
if (codecContext == null)
{
Trace("create failed: avcodec_alloc_context3 returned null");
return null;
}
codecContext->flags |= ffmpeg.AV_CODEC_FLAG_COPY_OPAQUE;
if (ffmpeg.avcodec_open2(codecContext, codec, null) < 0)
{
ffmpeg.avcodec_free_context(&codecContext);
Trace("create failed: avcodec_open2 rejected H.264 decoder");
return null;
}
@ -136,14 +161,18 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
}
ffmpeg.avcodec_free_context(&codecContext);
Trace("create failed: frame or packet allocation returned null");
return null;
}
return new Videodec2Decoder(codecContext, frame, packet);
var decoder = new Videodec2Decoder(codecContext, frame, packet);
Trace("create succeeded: FFmpeg H.264 worker and scheduler started");
return decoder;
}
catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException or TypeInitializationException)
{
// FFmpeg's native libraries are optional; missing ones degrade to the stub, not a crash.
Trace($"create failed: {ex.GetType().Name}: {ex.Message}");
if (codecContext != null)
{
ffmpeg.avcodec_free_context(&codecContext);
@ -174,18 +203,252 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
}
}
/// <summary>Hands one Annex-B access unit to the decode worker and returns immediately.</summary>
public void EnqueueAccessUnit(byte[] accessUnit)
public bool EnqueueAccessUnit(byte[] accessUnit)
{
_workChannel.Writer.TryWrite(accessUnit);
return _workChannel.Writer.TryWrite(accessUnit);
}
/// <summary>Queues an end-of-stream drain: flush FFmpeg and emit one more buffered picture, if any.</summary>
public void RequestDrain()
public bool RequestDrain()
{
_workChannel.Writer.TryWrite(null);
return _workChannel.Writer.TryWrite(null);
}
public bool DecodeAccessUnit(
byte[] accessUnit,
ulong pts,
ulong dts,
ulong attachedData,
out Videodec2DecodedFrame? decodedFrame)
{
decodedFrame = null;
lock (_gate)
{
if (_disposed || accessUnit.Length == 0)
{
return false;
}
ffmpeg.av_packet_unref(_packet);
ffmpeg.av_frame_unref(_frame);
var packetResult = ffmpeg.av_new_packet(_packet, accessUnit.Length);
if (packetResult < 0)
{
Trace($"decode failed: av_new_packet={packetResult}");
return false;
}
try
{
fixed (byte* source = accessUnit)
{
Buffer.MemoryCopy(source, _packet->data, accessUnit.Length, accessUnit.Length);
}
_packet->pts = ToAvTimestamp(pts);
_packet->dts = ToAvTimestamp(dts);
var havePendingFrame = false;
var sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
if (sendResult == ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
var pendingResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
if (pendingResult < 0)
{
Trace($"decode failed: receive-before-resend={pendingResult}");
return false;
}
havePendingFrame = true;
sendResult = ffmpeg.avcodec_send_packet(_codecContext, _packet);
}
if (sendResult < 0)
{
Trace($"decode failed: avcodec_send_packet={sendResult}");
return false;
}
if (!havePendingFrame)
{
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
receiveResult == ffmpeg.AVERROR_EOF)
{
return true;
}
if (receiveResult < 0)
{
Trace($"decode failed: avcodec_receive_frame={receiveResult}");
return false;
}
}
decodedFrame = ConvertFrameToNv12Locked(pts, dts, attachedData);
return decodedFrame is not null;
}
finally
{
ffmpeg.av_frame_unref(_frame);
ffmpeg.av_packet_unref(_packet);
}
}
}
public bool FlushOutput(out Videodec2DecodedFrame? decodedFrame)
{
decodedFrame = null;
lock (_gate)
{
if (_disposed)
{
return false;
}
ffmpeg.av_frame_unref(_frame);
var receiveResult = ffmpeg.avcodec_receive_frame(_codecContext, _frame);
if (receiveResult == ffmpeg.AVERROR(ffmpeg.EAGAIN) ||
receiveResult == ffmpeg.AVERROR_EOF)
{
return true;
}
if (receiveResult < 0)
{
Trace($"flush failed: avcodec_receive_frame={receiveResult}");
return false;
}
try
{
decodedFrame = ConvertFrameToNv12Locked();
return decodedFrame is not null;
}
finally
{
ffmpeg.av_frame_unref(_frame);
}
}
}
public void Reset()
{
lock (_gate)
{
if (_disposed)
{
return;
}
ffmpeg.avcodec_flush_buffers(_codecContext);
ffmpeg.av_packet_unref(_packet);
ffmpeg.av_frame_unref(_frame);
}
}
private Videodec2DecodedFrame? ConvertFrameToNv12Locked(
ulong fallbackPts = ulong.MaxValue,
ulong fallbackDts = ulong.MaxValue,
ulong attachedData = 0)
{
if (_frame->width <= 0 || _frame->height <= 0)
{
return null;
}
var width = checked((uint)_frame->width);
var height = checked((uint)_frame->height);
uint pitch;
ulong lumaBytes;
ulong requiredBytes;
try
{
pitch = checked((width + 255u) & ~255u);
lumaBytes = checked((ulong)pitch * height);
var chromaRows = ((ulong)height + 1UL) / 2UL;
requiredBytes = checked(lumaBytes + ((ulong)pitch * chromaRows));
}
catch (OverflowException)
{
return null;
}
if (requiredBytes > int.MaxValue)
{
return null;
}
_swsContext = ffmpeg.sws_getCachedContext(
_swsContext,
_frame->width,
_frame->height,
(AVPixelFormat)_frame->format,
_frame->width,
_frame->height,
AVPixelFormat.AV_PIX_FMT_NV12,
ffmpeg.SWS_FAST_BILINEAR,
null,
null,
null);
if (_swsContext == null)
{
return null;
}
var nv12 = new byte[(int)requiredBytes];
fixed (byte* destination = nv12)
{
var destinationPlanes = new byte*[4]
{
destination,
destination + checked((int)lumaBytes),
null,
null,
};
var destinationStrides = new int[4]
{
checked((int)pitch),
checked((int)pitch),
0,
0,
};
var rows = ffmpeg.sws_scale(
_swsContext,
_frame->data,
_frame->linesize,
0,
_frame->height,
destinationPlanes,
destinationStrides);
if (rows != _frame->height)
{
return null;
}
}
return new Videodec2DecodedFrame(
nv12,
width,
height,
pitch,
(_frame->flags & ffmpeg.AV_FRAME_FLAG_CORRUPT) != 0)
{
Pts = FromAvTimestamp(_frame->pts, fallbackPts),
Dts = FromAvTimestamp(_frame->pkt_dts, fallbackDts),
AttachedData = attachedData,
KeyFrame = (_frame->flags & ffmpeg.AV_FRAME_FLAG_KEY) != 0,
Profile = _codecContext->profile > 0 ? (uint)_codecContext->profile : 0,
Level = _codecContext->level > 0 ? (uint)_codecContext->level : 0,
};
}
private static long ToAvTimestamp(ulong timestamp) =>
timestamp <= long.MaxValue ? (long)timestamp : ffmpeg.AV_NOPTS_VALUE;
private static ulong FromAvTimestamp(long timestamp, ulong fallback) =>
timestamp == ffmpeg.AV_NOPTS_VALUE || timestamp < 0 ? fallback : (ulong)timestamp;
/// <summary>Non-blocking: true exactly once per frame the worker has produced, in order.</summary>
public bool TryConsumeProtocolReadySignal(out uint width, out uint height)
{
@ -233,15 +496,28 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
return;
}
var inputOrdinal = Interlocked.Increment(ref _workerInputCount);
var decodedOk = item is null
? DrainCoreLocked(out var bgraFrame, out var hasPicture, out var width, out var height)
: DecodeCoreLocked(item, out bgraFrame, out hasPicture, out width, out height);
if (ShouldTrace(inputOrdinal))
{
Trace(
$"worker input={inputOrdinal} kind={(item is null ? "drain" : "au")} " +
$"bytes={item?.Length ?? 0} ok={decodedOk} picture={hasPicture} size={width}x{height}");
}
if (!decodedOk || !hasPicture || bgraFrame is null)
{
continue;
}
var frameOrdinal = Interlocked.Increment(ref _workerFrameCount);
if (ShouldTrace(frameOrdinal))
{
Trace($"worker frame={frameOrdinal} produced size={width}x{height} bytes={bgraFrame.Length}");
}
try
{
// Blocks if the scheduler hasn't kept up; deliberate backpressure.
@ -564,4 +840,22 @@ internal sealed unsafe class Videodec2Decoder : IDisposable
}
}
}
private static bool IsTraceEnabled()
{
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
return string.Equals(value, "1", StringComparison.Ordinal) ||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldTrace(long ordinal) =>
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
private static void Trace(string message)
{
if (TraceEnabled)
{
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using SharpEmu.HLE;
@ -15,10 +16,18 @@ public static class Videodec2Exports
{
private const int Ok = 0;
private const ulong MinimumMemoryBytes = 16UL * 1024 * 1024;
private const uint FrameBufferAlignment = 0x100;
// Null entry = TryCreate() failed; every export falls back to the stub for that handle.
private static readonly ConcurrentDictionary<ulong, Videodec2Decoder?> Decoders = new();
private static readonly ConcurrentDictionary<ulong, Videodec2DecodedFrame> PictureInfos = new();
private static long _nextDecoderHandle = unchecked((long)DecoderToken);
private static readonly bool TraceEnabled = IsTraceEnabled();
private static long _decodeCallCount;
private static long _decodedPictureCount;
[SysAbiExport(
Nid = "RnDibcGCPKw",
ExportName = "sceVideodec2QueryComputeMemoryInfo",
@ -27,24 +36,25 @@ public static class Videodec2Exports
public static int Videodec2QueryComputeMemoryInfo(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
if (paramAddress == 0 ||
!ctx.TryWriteUInt64(paramAddress + 0x08, MinimumMemoryBytes) ||
!ctx.TryWriteUInt64(paramAddress + 0x10, 0))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
// Success needs no memory writes; the game initializes from its own fields.
return SetReturn(ctx, Ok);
}
private const int VideodecErrorInvalidArg = unchecked((int)0x80620801);
private const int VideodecErrorStructSize = unchecked((int)0x811D0101);
private const int VideodecErrorOutputInfo = unchecked((int)0x811D010F);
private const int VideodecErrorComputeQueueId = unchecked((int)0x811D0202);
// Reject garbage/not-yet-primed struct reads before they reach `new byte[...]`.
private const ulong MaxPlausibleAuBytes = 32UL * 1024 * 1024;
private const ulong MaxPlausibleSlotBytes = 64UL * 1024 * 1024;
// Opaque token the game hands back unmodified to later Videodec2 calls.
private const ulong ComputeQueueToken = 0x56D2_C0DE_0001UL;
[SysAbiExport(
Nid = "eD+X2SmxUt4",
ExportName = "sceVideodec2AllocateComputeQueue",
@ -52,8 +62,14 @@ public static class Videodec2Exports
LibraryName = "libSceVideodec2")]
public static int Videodec2AllocateComputeQueue(CpuContext ctx)
{
var queueAddress = ctx[CpuRegister.Rdi];
if (queueAddress == 0 || !ctx.TryWriteUInt64(queueAddress, ComputeQueueToken))
var configAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
var queueAddress = ctx[CpuRegister.Rdx];
if (configAddress == 0 || memoryInfoAddress == 0 || queueAddress == 0 ||
!ctx.TryReadUInt64(memoryInfoAddress + 0x10, out var computeMemoryAddress) ||
computeMemoryAddress == 0 ||
!ctx.TryWriteUInt64(
queueAddress, computeMemoryAddress))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
@ -61,7 +77,18 @@ public static class Videodec2Exports
return SetReturn(ctx, Ok);
}
// A zero size at +0x08/+0x28 makes the game skip its own arena allocation cleanly.
[SysAbiExport(
Nid = "UvtA3FAiF4Y",
ExportName = "sceVideodec2ReleaseComputeQueue",
Target = Generation.Gen5,
LibraryName = "libSceVideodec2")]
public static int Videodec2ReleaseComputeQueue(CpuContext ctx)
{
return SetReturn(
ctx,
ctx[CpuRegister.Rdi] != 0 ? Ok : VideodecErrorComputeQueueId);
}
[SysAbiExport(
Nid = "qqMCwlULR+E",
ExportName = "sceVideodec2QueryDecoderMemoryInfo",
@ -69,12 +96,18 @@ public static class Videodec2Exports
LibraryName = "libSceVideodec2")]
public static int Videodec2QueryDecoderMemoryInfo(CpuContext ctx)
{
var configAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (memoryInfoAddress == 0 ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, 0) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, 0) ||
// Frame-slot size: must be nonzero or the game divides its arena by zero.
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, 0x1000))
if (configAddress == 0 || memoryInfoAddress == 0 ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x08, MinimumMemoryBytes) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x10, 0) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x18, MinimumMemoryBytes) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x20, 0) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x28, MinimumMemoryBytes) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x30, 0) ||
!ctx.TryWriteUInt64(memoryInfoAddress + 0x38, MinimumMemoryBytes) ||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x40, FrameBufferAlignment) ||
!ctx.TryWriteUInt32(memoryInfoAddress + 0x44, 0))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
@ -99,7 +132,9 @@ public static class Videodec2Exports
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextDecoderHandle));
Decoders[handle] = Videodec2Decoder.TryCreate();
var decoder = Videodec2Decoder.TryCreate();
Decoders[handle] = decoder;
Trace($"create handle=0x{handle:X16} real_decoder={decoder is not null}");
if (!ctx.TryWriteUInt64(decoderAddress, handle))
{
@ -111,7 +146,6 @@ public static class Videodec2Exports
return SetReturn(ctx, Ok);
}
// Clearing the picture-ready byte at [rdx] tells the player "no buffered pictures remain".
[SysAbiExport(
Nid = "l1hXwscLuCY",
ExportName = "sceVideodec2Flush",
@ -120,33 +154,25 @@ public static class Videodec2Exports
public static int Videodec2Flush(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var outputSlotObj = ctx[CpuRegister.Rsi];
var outputInfoAddress = ctx[CpuRegister.Rdx];
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
if (Decoders.TryGetValue(handle, out var decoder) &&
decoder is not null &&
decoder.FlushOutput(out var decodedFrame) &&
decodedFrame is not null)
{
// Drain in order: report an already-finished frame before queuing a new drain request.
if (decoder.TryConsumeProtocolReadySignal(out var width, out var height))
{
if (ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) &&
ctx.TryWriteUInt64(outputInfoAddress + 0x10, height))
{
_ = ctx.Memory.TryWrite(outputInfoAddress, PictureReady);
}
}
else
{
decoder.RequestDrain();
}
_ = TryWriteDecodedFrame(
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
}
return SetReturn(ctx, Ok);
}
// No state to reset.
[SysAbiExport(
Nid = "wJXikG6QFN8",
ExportName = "sceVideodec2Reset",
@ -154,6 +180,12 @@ public static class Videodec2Exports
LibraryName = "libSceVideodec2")]
public static int Videodec2Reset(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
if (Decoders.TryGetValue(handle, out var decoder) && decoder is not null)
{
decoder.Reset();
}
return SetReturn(ctx, Ok);
}
@ -173,8 +205,6 @@ public static class Videodec2Exports
return SetReturn(ctx, Ok);
}
// rcx[0] is the picture-ready flag (1 = frame published); it lives in
// uninitialized stack and must always be written explicitly.
[SysAbiExport(
Nid = "852F5+q6+iM",
ExportName = "sceVideodec2Decode",
@ -186,8 +216,9 @@ public static class Videodec2Exports
var inputAuStruct = ctx[CpuRegister.Rsi];
var outputSlotObj = ctx[CpuRegister.Rdx];
var outputInfoAddress = ctx[CpuRegister.Rcx];
var callOrdinal = Interlocked.Increment(ref _decodeCallCount);
if (outputInfoAddress == 0 || !ctx.Memory.TryWrite(outputInfoAddress, NoPicture))
if (!TryInitializeNoPicture(ctx, outputSlotObj, outputInfoAddress))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
@ -195,6 +226,7 @@ public static class Videodec2Exports
if (!Decoders.TryGetValue(handle, out var decoder) || decoder is null)
{
// No real decoder for this handle: stub behavior, "fed the AU, no picture".
TraceDecode(callOrdinal, $"stub handle=0x{handle:X16}");
return SetReturn(ctx, Ok);
}
@ -208,35 +240,235 @@ public static class Videodec2Exports
slotPtr == 0 || slotSize == 0 || slotSize > MaxPlausibleSlotBytes)
{
// Nothing sane to feed/fill this call; not an error.
TraceDecode(callOrdinal, "invalid access-unit or output-slot descriptor");
return SetReturn(ctx, Ok);
}
_ = ctx.TryReadUInt64(inputAuStruct + 0x18, out var pts);
_ = ctx.TryReadUInt64(inputAuStruct + 0x20, out var dts);
_ = ctx.TryReadUInt64(inputAuStruct + 0x28, out var attachedData);
var auBuffer = new byte[auDataSize];
if (!ctx.Memory.TryRead(auDataPtr, auBuffer))
{
TraceDecode(callOrdinal, $"AU read failed ptr=0x{auDataPtr:X16} bytes=0x{auDataSize:X}");
return SetReturn(ctx, Ok);
}
// Queues the AU and returns immediately; decode/present happen on Videodec2Decoder's own threads.
decoder.EnqueueAccessUnit(auBuffer);
var decodeOk = decoder.DecodeAccessUnit(
auBuffer, pts, dts, attachedData, out var decodedFrame);
if (ShouldTrace(callOrdinal))
{
var prefixBytes = Math.Min(auBuffer.Length, 8);
Trace(
$"decode call={callOrdinal} au=0x{auDataPtr:X16}+0x{auDataSize:X} " +
$"head={Convert.ToHexString(auBuffer.AsSpan(0, prefixBytes))} " +
$"slot=0x{slotPtr:X16}+0x{slotSize:X} ok={decodeOk} " +
$"picture={decodedFrame is not null}");
}
if (!decoder.TryConsumeProtocolReadySignal(out var width, out var height))
if (!decodeOk || decodedFrame is null)
{
return SetReturn(ctx, Ok);
}
if (!ctx.TryWriteUInt64(outputInfoAddress + 0x08, width) ||
!ctx.TryWriteUInt64(outputInfoAddress + 0x10, height) ||
!ctx.Memory.TryWrite(outputInfoAddress, PictureReady))
var copied = TryWriteDecodedFrame(
ctx, outputSlotObj, outputInfoAddress, decodedFrame);
if (copied)
{
return SetReturn(ctx, Ok);
Interlocked.Increment(ref _decodedPictureCount);
}
TraceDecode(callOrdinal, $"guest_copy={copied} size={decodedFrame.Width}x{decodedFrame.Height} pitch={decodedFrame.Pitch}");
return SetReturn(ctx, Ok);
}
private static readonly byte[] NoPicture = [0];
private static readonly byte[] PictureReady = [1];
[SysAbiExport(
Nid = "NtXRa3dRzU0",
ExportName = "sceVideodec2GetPictureInfo",
Target = Generation.Gen5,
LibraryName = "libSceVideodec2")]
public static int Videodec2GetPictureInfo(CpuContext ctx)
{
var outputInfoAddress = ctx[CpuRegister.Rdi];
var firstPictureInfoAddress = ctx[CpuRegister.Rsi];
var secondPictureInfoAddress = ctx[CpuRegister.Rdx];
if (outputInfoAddress == 0 || firstPictureInfoAddress == 0)
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
if (!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoSize) ||
(outputInfoSize != 0x30 && outputInfoSize != 0x38))
{
return SetReturn(ctx, VideodecErrorStructSize);
}
if (!TryReadByte(ctx, outputInfoAddress + 0x08, out var isValid) ||
!TryReadByte(ctx, outputInfoAddress + 0x0A, out var pictureCount) ||
!ctx.TryReadUInt32(outputInfoAddress + 0x0C, out var codecType) ||
!ctx.TryReadUInt64(outputInfoAddress + 0x20, out var pixelsAddress) ||
isValid == 0 || pictureCount == 0 || pixelsAddress == 0 || codecType != 1 ||
!PictureInfos.TryGetValue(pixelsAddress, out var decodedFrame))
{
return SetReturn(ctx, VideodecErrorOutputInfo);
}
if (!ctx.TryReadUInt64(firstPictureInfoAddress, out var requestedSize) ||
(requestedSize != 104 && requestedSize != 120))
{
return SetReturn(ctx, VideodecErrorStructSize);
}
var picture = new byte[(int)requestedSize];
BinaryPrimitives.WriteUInt64LittleEndian(picture, requestedSize);
picture[0x08] = 1;
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x10), decodedFrame.Pts);
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x18), decodedFrame.Dts);
BinaryPrimitives.WriteUInt64LittleEndian(picture.AsSpan(0x20), decodedFrame.AttachedData);
picture[0x28] = decodedFrame.KeyFrame ? (byte)1 : (byte)0;
picture[0x29] = (byte)Math.Min(decodedFrame.Profile, byte.MaxValue);
picture[0x2A] = (byte)Math.Min(decodedFrame.Level, byte.MaxValue);
BinaryPrimitives.WriteUInt32LittleEndian(
picture.AsSpan(0x2C), (decodedFrame.Width + 15u) / 16u - 1u);
BinaryPrimitives.WriteUInt32LittleEndian(
picture.AsSpan(0x30), (decodedFrame.Height + 15u) / 16u - 1u);
picture[0x34] = 1;
picture[0x4E] = 1;
picture[0x4F] = 5;
if (!ctx.Memory.TryWrite(firstPictureInfoAddress, picture))
{
return SetReturn(ctx, VideodecErrorInvalidArg);
}
if (secondPictureInfoAddress != 0 &&
!TryWriteEmptyPictureInfo(ctx, secondPictureInfoAddress))
{
return SetReturn(ctx, VideodecErrorStructSize);
}
Trace(
$"picture_info buffer=0x{pixelsAddress:X16} " +
$"pts=0x{decodedFrame.Pts:X16} size={requestedSize}");
return SetReturn(ctx, Ok);
}
private static readonly byte[] NotAccepted = [0];
private static readonly byte[] Accepted = [1];
private static readonly byte[] NoPicture = [0, 0, 0, 0];
private static readonly byte[] PictureReady = [1, 0, 1, 0];
private static readonly byte[] ErrorPictureReady = [1, 1, 1, 0];
private static bool TryInitializeNoPicture(
CpuContext ctx,
ulong frameBufferAddress,
ulong outputInfoAddress)
{
return frameBufferAddress != 0 &&
outputInfoAddress != 0 &&
ctx.Memory.TryWrite(frameBufferAddress + 0x18, NotAccepted) &&
ctx.Memory.TryWrite(outputInfoAddress + 0x08, NoPicture);
}
internal static bool TryWriteDecodedFrame(
CpuContext ctx,
ulong frameBufferAddress,
ulong outputInfoAddress,
Videodec2DecodedFrame decodedFrame)
{
if (!ctx.TryReadUInt64(frameBufferAddress + 0x08, out var pixelsAddress) ||
!ctx.TryReadUInt64(frameBufferAddress + 0x10, out var slotBytes) ||
!ctx.TryReadUInt64(outputInfoAddress, out var outputInfoBytes) ||
pixelsAddress == 0)
{
return false;
}
if ((ulong)decodedFrame.Nv12.Length > slotBytes ||
!ctx.Memory.TryWrite(pixelsAddress, decodedFrame.Nv12))
{
return false;
}
if (!ctx.TryWriteUInt32(outputInfoAddress + 0x0C, 1) ||
!ctx.TryWriteUInt32(outputInfoAddress + 0x10, decodedFrame.Width) ||
!ctx.TryWriteUInt32(outputInfoAddress + 0x14, decodedFrame.Pitch) ||
!ctx.TryWriteUInt32(outputInfoAddress + 0x18, decodedFrame.Height) ||
!ctx.TryWriteUInt64(outputInfoAddress + 0x20, pixelsAddress) ||
!ctx.TryWriteUInt64(outputInfoAddress + 0x28, slotBytes))
{
return false;
}
if (outputInfoBytes >= 0x38 &&
(!ctx.TryWriteUInt32(outputInfoAddress + 0x30, 0) ||
!ctx.TryWriteUInt32(outputInfoAddress + 0x34, decodedFrame.Pitch)))
{
return false;
}
if (!ctx.Memory.TryWrite(frameBufferAddress + 0x18, Accepted) ||
!ctx.Memory.TryWrite(
outputInfoAddress + 0x08,
decodedFrame.ErrorFrame ? ErrorPictureReady : PictureReady))
{
return false;
}
PictureInfos[pixelsAddress] = decodedFrame;
return true;
}
private static bool TryWriteEmptyPictureInfo(CpuContext ctx, ulong address)
{
if (!ctx.TryReadUInt64(address, out var size) || size < 40 || size > 256)
{
return false;
}
var output = new byte[(int)size];
BinaryPrimitives.WriteUInt64LittleEndian(output, size);
return ctx.Memory.TryWrite(address, output);
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool IsTraceEnabled()
{
var value = Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEODEC2");
return string.Equals(value, "1", StringComparison.Ordinal) ||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
}
private static bool ShouldTrace(long ordinal) =>
TraceEnabled && (ordinal <= 16 || ordinal % 120 == 0);
private static void TraceDecode(long ordinal, string message)
{
if (ShouldTrace(ordinal))
{
Trace($"decode call={ordinal} {message}");
}
}
private static void Trace(string message)
{
if (TraceEnabled)
{
Console.Error.WriteLine($"[VIDEODEC2][TRACE] {message}");
}
}
private static int SetReturn(CpuContext ctx, int result)
{

View File

@ -0,0 +1,171 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.CommonDialog;
/// <summary>
/// Headless lifecycle for the system web-browser dialog. SharpEmu does not yet
/// provide a host browser, so an opened dialog completes on its first status
/// poll instead of leaving games in an infinite RUNNING loop.
/// </summary>
public static class WebBrowserDialogExports
{
internal const int StatusNone = 0;
internal const int StatusInitialized = 1;
internal const int StatusRunning = 2;
internal const int StatusFinished = 3;
private const int ErrorOk = 0;
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
private const int ErrorNotFinished = unchecked((int)0x80B80005);
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
private const int ErrorArgNull = unchecked((int)0x80B8000D);
private static int _status;
[SysAbiExport(
Nid = "jqb7HntFQFc",
ExportName = "sceWebBrowserDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogInitialize(CpuContext ctx)
{
Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone);
return ctx.SetReturn(ErrorOk);
}
[SysAbiExport(
Nid = "FraP7debcdg",
ExportName = "sceWebBrowserDialogOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogOpen(CpuContext ctx) => Open(ctx);
[SysAbiExport(
Nid = "O7dIZQrwVFY",
ExportName = "sceWebBrowserDialogOpenForPredeterminedContent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogOpenForPredeterminedContent(CpuContext ctx) => Open(ctx);
private static int Open(CpuContext ctx)
{
if (ctx[CpuRegister.Rdi] == 0)
{
return ctx.SetReturn(ErrorArgNull);
}
if (Volatile.Read(ref _status) == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
}
Interlocked.Exchange(ref _status, StatusRunning);
return ctx.SetReturn(ErrorOk);
}
[SysAbiExport(
Nid = "CFTG6a8TjOU",
ExportName = "sceWebBrowserDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogGetStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
[SysAbiExport(
Nid = "h1dR-t5ISgg",
ExportName = "sceWebBrowserDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(PollStatus());
private static int PollStatus()
{
Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning);
return Volatile.Read(ref _status);
}
[SysAbiExport(
Nid = "PSK+Eik919Q",
ExportName = "sceWebBrowserDialogClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogClose(CpuContext ctx)
{
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
{
return ctx.SetReturn(ErrorNotRunning);
}
return ctx.SetReturn(ErrorOk);
}
[SysAbiExport(
Nid = "vCaW0fgVQmc",
ExportName = "sceWebBrowserDialogGetResult",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogGetResult(CpuContext ctx)
{
if (ctx[CpuRegister.Rdi] == 0)
{
return ctx.SetReturn(ErrorArgNull);
}
if (Volatile.Read(ref _status) != StatusFinished)
{
return ctx.SetReturn(ErrorNotFinished);
}
Span<byte> result = stackalloc byte[sizeof(int)];
result.Clear();
if (!ctx.Memory.TryWrite(ctx[CpuRegister.Rdi], result))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return ctx.SetReturn(ErrorOk);
}
[SysAbiExport(
Nid = "Wit4LjeoeX4",
ExportName = "sceWebBrowserDialogGetEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogGetEvent(CpuContext ctx) => ctx.SetReturn(ErrorOk);
[SysAbiExport(
Nid = "uYELOMVnmNQ",
ExportName = "sceWebBrowserDialogNavigate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogNavigate(CpuContext ctx) => ctx.SetReturn(ErrorOk);
[SysAbiExport(
Nid = "RLhKBOoNyXY",
ExportName = "sceWebBrowserDialogSetZoom",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogSetZoom(CpuContext ctx) => ctx.SetReturn(ErrorOk);
[SysAbiExport(
Nid = "ocHtyBwHfys",
ExportName = "sceWebBrowserDialogTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogTerminate(CpuContext ctx)
{
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
}
return ctx.SetReturn(ErrorOk);
}
internal static int StatusForTests => Volatile.Read(ref _status);
internal static void ResetForTests() => Interlocked.Exchange(ref _status, StatusNone);
}

View File

@ -0,0 +1,74 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
using System.Globalization;
using SharpEmu.Libs.Gpu.Buffers;
namespace SharpEmu.Libs.Diagnostics;
internal static class DccWriterTrace
{
private const int MaxReports = 256;
private const int MaxFrames = 12;
private static readonly bool DccEnabled =
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_DCC_WRITERS") == "1";
private static readonly ulong[] TargetAddresses = (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_WRITER_ADDRESS") ?? string.Empty)
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(ParseAddress)
.Where(address => address != 0)
.ToArray();
public static readonly bool Enabled = DccEnabled || TargetAddresses.Length != 0;
[ThreadStatic]
public static ulong CurrentProgram;
private static readonly HashSet<(ulong Address, ulong Size, ulong Program)> _reported = new();
private static readonly HashSet<(ulong Hash, string Reason)> _refusals = new();
private static ulong ParseAddress(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, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed) ? parsed : 0;
}
public static void Record(IGuestImageCache images, ulong address, ulong size)
{
var targeted = TargetAddresses.Any(target => target >= address && target - address < size);
if (_reported.Count >= MaxReports || (!targeted && !(DccEnabled && images.OverlapsDccMetadata(address, size))) ||
!_reported.Add((address, size, CurrentProgram)))
{
return;
}
var frames = new StackTrace(1, false).GetFrames();
var names = frames.Take(MaxFrames).Select(frame => frame.GetMethod() is { } method
? $"{method.DeclaringType?.Name}.{method.Name}"
: "?");
Console.Error.WriteLine(
$"[GPU][{(targeted ? "TARGET_WRITER" : "DCC_WRITER")}] address=0x{address:X16} size=0x{size:X} program=0x{CurrentProgram:X16} stack={string.Join(" < ", names)}");
}
public static void Refuse(ulong hash, string reason)
{
if (!Enabled || _refusals.Count >= MaxReports || !_refusals.Add((hash, reason)))
{
return;
}
Console.Error.WriteLine($"[GPU][BOUNDED_FILL_REFUSED] program=0x{hash:X16} reason={reason}");
}
}

View File

@ -0,0 +1,59 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Diagnostics;
internal static class GpuReadTrace
{
private const int MaxReports = 64;
public static readonly bool Enabled =
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GPU_READS") == "1";
[ThreadStatic]
public static ulong CurrentShader;
[ThreadStatic]
public static string? CurrentStage;
private static readonly HashSet<(ulong Shader, ulong Page, bool Table)> _reported = new();
private static readonly Dictionary<ulong, (long Reads, long Changes, uint Last, uint First, ulong Shader)> _values = new();
private static long _nextValueReport = System.Diagnostics.Stopwatch.GetTimestamp();
public static void RecordValue(ulong address, uint word)
{
lock (_values)
{
_values[address] = _values.TryGetValue(address, out var entry)
? (entry.Reads + 1, entry.Changes + (entry.Last != word ? 1 : 0), word, entry.First, entry.Shader)
: (1, 0, word, word, CurrentShader);
var now = System.Diagnostics.Stopwatch.GetTimestamp();
if (now < _nextValueReport)
{
return;
}
_nextValueReport = now + System.Diagnostics.Stopwatch.Frequency * 5;
foreach (var (key, value) in _values.OrderByDescending(pair => pair.Value.Reads).Take(16))
{
Console.Error.WriteLine(
$"[GPU][SYNC_READ_VALUE] address=0x{key:X16} reads={value.Reads} changes={value.Changes} first=0x{value.First:X8} last=0x{value.Last:X8} shader=0x{value.Shader:X16}");
}
}
}
public static void Record(ulong address, bool table)
{
var key = (CurrentShader, address & ~0xFFFUL, table);
lock (_reported)
{
if (_reported.Count >= MaxReports || !_reported.Add(key))
{
return;
}
}
Console.Error.WriteLine(
$"[GPU][SYNC_READ] shader=0x{CurrentShader:X16} stage={CurrentStage} address=0x{address:X16} table={table}");
}
}

View File

@ -0,0 +1,17 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Diagnostics;
public static class RazorCpuExports
{
[SysAbiExport(Nid = "zw+celG7zSI", ExportName = "sceRazorCpuPushMarker",
Target = Generation.Gen5, LibraryName = "libSceRazorCpu")]
public static int PushMarker(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(Nid = "YpkGsMXP3ew", ExportName = "sceRazorCpuPopMarker",
Target = Generation.Gen5, LibraryName = "libSceRazorCpu")]
public static int PopMarker(CpuContext ctx) => ctx.SetReturn(0);
}

View File

@ -49,6 +49,7 @@ public static class FiberExports
private const int FiberMagicEndOffset = 104;
private static int _contextSizeCheck;
private static readonly bool TraceEnabled = Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER") == "1";
private static readonly object _fiberGate = new();
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
@ -287,9 +288,10 @@ public static class FiberExports
_ = GuestThreadExecution.EnterFiber(0);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"return-to-thread fiber=0x{fiberAddress:X16} " +
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
if (TraceEnabled)
TraceFiber(
$"return-to-thread fiber=0x{fiberAddress:X16} " +
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
return SetReturn(ctx, 0);
}
@ -306,13 +308,7 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorNull);
}
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return SetReturn(ctx, FiberErrorPermission);
}
return TryWriteUInt64(ctx, outAddress, fiberAddress)
return TryWriteUInt64(ctx, outAddress, ResolveCurrentFiberAddress(ctx))
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
@ -341,7 +337,7 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorInvalid);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
if (!TryReadFiberFields(ctx, fiber, out var fields, readName: true))
{
return SetReturn(ctx, FiberErrorInvalid);
}
@ -527,9 +523,10 @@ public static class FiberExports
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
}
TraceFiber(
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
if (TraceEnabled)
TraceFiber(
$"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} " +
$"size=0x{contextSize:X} flags=0x{flags:X} build=0x{buildVersion:X8} name='{name}'");
return SetReturn(ctx, 0);
}
@ -589,7 +586,8 @@ public static class FiberExports
}
if (fields.State != FiberStateIdle)
{
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
if (TraceEnabled)
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return SetReturn(ctx, FiberErrorState);
}
@ -676,9 +674,10 @@ public static class FiberExports
_ = GuestThreadExecution.EnterFiber(fiber);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
if (TraceEnabled)
TraceFiber(
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
return SetReturn(ctx, 0);
}
@ -977,7 +976,7 @@ public static class FiberExports
return true;
}
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields, bool readName = false)
{
fields = default;
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
@ -986,7 +985,7 @@ public static class FiberExports
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name, decode: readName))
{
return false;
}
@ -1071,7 +1070,7 @@ public static class FiberExports
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadInlineName(CpuContext ctx, ulong address, out string value)
private static bool TryReadInlineName(CpuContext ctx, ulong address, out string value, bool decode)
{
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
if (!ctx.Memory.TryRead(address, buffer))
@ -1086,7 +1085,9 @@ public static class FiberExports
length = buffer.Length;
}
value = Encoding.UTF8.GetString(buffer[..length]);
// A switch validates the same bytes but does not need a managed name.
// Only sceFiberGetInfo exposes the decoded name to its caller.
value = decode ? Encoding.UTF8.GetString(buffer[..length]) : string.Empty;
return true;
}
@ -1123,10 +1124,7 @@ public static class FiberExports
private static void TraceFiber(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] fiber.{message}");
}
Console.Error.WriteLine($"[LOADER][TRACE] fiber.{message}");
}
private readonly record struct FiberFields(

View File

@ -2,7 +2,10 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
using SkiaSharp;
namespace SharpEmu.Libs.Font;
@ -16,6 +19,38 @@ public static class FontExports
private static readonly Stack<ulong> FreeGlyphs = new();
private static ulong _librarySelectionAddress;
private static ulong _rendererSelectionAddress;
private static readonly ConcurrentDictionary<ulong, (float Width, float Height)> FontScales = new();
private static readonly ConcurrentDictionary<ulong, FontFace> FontFaces = new();
private static readonly Lazy<FontFace> DefaultFont = new(() => new(SKTypeface.FromFamilyName(null)));
[SysAbiExport(
Nid = "oaJ1BpN2FQk",
ExportName = "sceFontTextSourceInit",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int TextSourceInit(CpuContext ctx)
{
var sourceAddress = ctx[CpuRegister.Rdi];
var textAddress = ctx[CpuRegister.Rsi];
var textSize = (uint)ctx[CpuRegister.Rdx];
if (sourceAddress == 0 || textAddress > ulong.MaxValue - textSize)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> source = stackalloc byte[0x60];
source.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(source[0x08..], textAddress);
BinaryPrimitives.WriteUInt64LittleEndian(source[0x10..], textAddress == 0 ? 0 : textAddress + textSize);
BinaryPrimitives.WriteUInt64LittleEndian(source[0x18..], textAddress);
BinaryPrimitives.WriteUInt64LittleEndian(source[0x20..], ctx[CpuRegister.Rcx]);
BinaryPrimitives.WriteUInt64LittleEndian(source[0x28..], ctx[CpuRegister.R8]);
BinaryPrimitives.WriteUInt64LittleEndian(source[0x38..], 0x10);
BinaryPrimitives.WriteUInt64LittleEndian(source[0x40..], textSize);
return ctx.Memory.TryWrite(sourceAddress, source)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "whrS4oksXc4",
@ -98,7 +133,7 @@ public static class FontExports
ExportName = "sceFontSetScalePixel",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SetScalePixel(CpuContext ctx) => SetSuccess(ctx);
public static int SetScalePixel(CpuContext ctx) => SetFontHeight(ctx);
[SysAbiExport(
Nid = "TMtqoFQjjbA",
@ -119,7 +154,7 @@ public static class FontExports
ExportName = "sceFontSetupRenderScalePixel",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int SetupRenderScalePixel(CpuContext ctx) => SetSuccess(ctx);
public static int SetupRenderScalePixel(CpuContext ctx) => SetFontHeight(ctx);
[SysAbiExport(
Nid = "lz9y9UFO2UU",
@ -144,13 +179,22 @@ public static class FontExports
{
var layoutAddress = ctx[CpuRegister.Rsi];
if (layoutAddress == 0)
{
layoutAddress = ctx[CpuRegister.Rdx];
}
if (layoutAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// Baseline, line advance, decoration extent: the same invented geometry
// as GetRenderCharGlyphMetrics.
var values = new[] { 12.0f, 16.0f, 0.0f };
var fontHandle = ctx[CpuRegister.Rdi];
var scale = GetFontScale(fontHandle);
float[] values = [scale.Height * 0.75f, scale.Height, 0];
if (FontFaces.TryGetValue(fontHandle, out var face))
{
var metrics = face.GetMetrics(scale.Width, scale.Height);
values = [-metrics.Ascent, metrics.Descent - metrics.Ascent + metrics.Leading, 0];
}
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
@ -174,6 +218,10 @@ public static class FontExports
{
var layoutAddress = ctx[CpuRegister.Rsi];
if (layoutAddress == 0)
{
layoutAddress = ctx[CpuRegister.Rdx];
}
if (layoutAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
@ -181,7 +229,10 @@ public static class FontExports
// Baseline (horizontal offset), line advance, decoration extent.
// Mirrors the same three-float layout as GetHorizontalLayout, but
// interpreted for vertical writing (e.g. CJK text rendered top-to-bottom).
var values = new[] { 8.0f, 16.0f, 0.0f };
// Scaled to the font's current pixel height; the default height of 16
// reproduces the original fixed geometry.
var scale = GetFontScale(ctx[CpuRegister.Rdi]);
var values = new[] { scale.Width * 0.5f, scale.Width, 0.0f };
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
@ -201,16 +252,64 @@ public static class FontExports
ExportName = "sceFontOpenFontSet",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int OpenFontSet(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
public static int OpenFontSet(CpuContext ctx)
{
var fontSet = (uint)ctx[CpuRegister.Rsi];
var bold = (fontSet & 0xf) >= 7;
var italic = ((fontSet >> 20) & 0xf) == 1;
var style = bold ? (italic ? SKFontStyle.BoldItalic : SKFontStyle.Bold) :
(italic ? SKFontStyle.Italic : SKFontStyle.Normal);
return OpenFace(ctx, new(SKTypeface.FromFamilyName(null, style)));
}
[SysAbiExport(
Nid = "KXUpebrFk1U",
ExportName = "sceFontOpenFontMemory",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int OpenFontMemory(CpuContext ctx) =>
CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
public static int OpenFontMemory(CpuContext ctx)
{
var address = ctx[CpuRegister.Rsi];
var size = (uint)ctx[CpuRegister.Rdx];
if (address == 0 || size is < 12 or > 64 * 1024 * 1024 || ctx[CpuRegister.R8] == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.Memory.CanRead(address, size))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var bytes = new byte[(int)size];
if (!ctx.Memory.TryRead(address, bytes))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
uint index = 0;
if (ctx[CpuRegister.Rcx] != 0 && !ctx.TryReadUInt32(ctx[CpuRegister.Rcx] + 8, out index))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (index > int.MaxValue)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
using var data = SKData.CreateCopy(bytes);
var typeface = SKTypeface.FromData(data, (int)index);
return typeface is null
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT)
: OpenFace(ctx, new(typeface));
}
private static int OpenFace(CpuContext ctx, FontFace face)
{
var result = CreateOpaqueHandle(ctx, ctx[CpuRegister.R8], 0x100, magic: 0x0F02);
if (result == 0 && ctx.TryReadUInt64(ctx[CpuRegister.R8], out var handle))
{
FontFaces[handle] = face;
FontScales.TryRemove(handle, out _);
}
return result;
}
[SysAbiExport(
Nid = "JzCH3SCFnAU",
@ -229,6 +328,8 @@ public static class FontExports
if (setupHandle != 0)
{
if (FontFaces.TryGetValue(sourceHandle, out var face)) FontFaces[setupHandle] = face;
FontScales[setupHandle] = GetFontScale(sourceHandle);
return ctx.TryWriteUInt64(outputAddress, setupHandle)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@ -249,6 +350,8 @@ public static class FontExports
}
_ = TryWriteUInt16(ctx, handle, 0x0F02);
if (FontFaces.TryGetValue(sourceHandle, out var sourceFace)) FontFaces[handle] = sourceFace;
FontScales[handle] = GetFontScale(sourceHandle);
return ctx.TryWriteUInt64(outputAddress, handle)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@ -288,19 +391,11 @@ public static class FontExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
ctx,
metricsAddress + (ulong)(index * sizeof(float)),
BitConverter.SingleToUInt32Bits(values[index])))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
return SetSuccess(ctx);
var code = (uint)ctx[CpuRegister.Rsi];
if (!IsValidCode(code)) return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return WriteGlyphMetrics(ctx, metricsAddress, GetGlyph(ctx[CpuRegister.Rdi], code), GetFontScale(ctx[CpuRegister.Rdi]).Height)
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@ -367,6 +462,26 @@ public static class FontExports
return SetSuccess(ctx);
}
[SysAbiExport(
Nid = "vRxf4d0ulPs",
ExportName = "sceFontRenderSurfaceSetScissor",
Target = Generation.Gen5, LibraryName = "libSceFont")]
public static int RenderSurfaceSetScissor(CpuContext ctx)
{
var surfaceAddress = ctx[CpuRegister.Rdi];
if (surfaceAddress == 0)
{
return SetSuccess(ctx);
}
return TryWriteUInt32(ctx, surfaceAddress + 0x18, (uint)ctx[CpuRegister.Rsi]) &&
TryWriteUInt32(ctx, surfaceAddress + 0x1C, (uint)ctx[CpuRegister.Rdx]) &&
TryWriteUInt32(ctx, surfaceAddress + 0x20, (uint)ctx[CpuRegister.Rcx]) &&
TryWriteUInt32(ctx, surfaceAddress + 0x24, (uint)ctx[CpuRegister.R8])
? SetSuccess(ctx)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "C-4Qw5Srlyw",
ExportName = "sceFontGenerateCharGlyph",
@ -434,43 +549,171 @@ public static class FontExports
LibraryName = "libSceFont")]
public static int RenderCharGlyphImageHorizontal(CpuContext ctx)
{
var surfaceAddress = ctx[CpuRegister.Rdx];
var metricsAddress = ctx[CpuRegister.Rcx];
var resultAddress = ctx[CpuRegister.R8];
var font = ctx[CpuRegister.Rdi];
var code = (uint)ctx[CpuRegister.Rsi];
if (!IsValidCode(code)) return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
var glyph = GetGlyph(font, code);
var width = glyph.Width;
var height = glyph.Height;
if (metricsAddress != 0)
if (metricsAddress != 0 && !WriteGlyphMetrics(ctx, metricsAddress, glyph, GetFontScale(font).Height))
{
var values = new[] { 8.0f, 16.0f, 0.0f, 12.0f, 8.0f, 0.0f, 0.0f, 16.0f };
for (var index = 0; index < values.Length; index++)
{
if (!TryWriteUInt32(
ctx,
metricsAddress + (ulong)(index * sizeof(float)),
BitConverter.SingleToUInt32Bits(values[index])))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
ctx.GetXmmRegister(0, out var xBits, out _);
ctx.GetXmmRegister(1, out var yBits, out _);
var x = BitConverter.UInt32BitsToSingle((uint)xBits);
var y = BitConverter.UInt32BitsToSingle((uint)yBits);
var left = float.IsFinite(x) ? (int)MathF.Floor(x + glyph.BearingX) : 0;
var top = float.IsFinite(y) ? (int)MathF.Floor(y - glyph.BearingY) : 0;
ulong buffer = 0;
uint pitch = 0;
uint pixelSize = 0;
if (resultAddress != 0)
{
Span<byte> cleared = stackalloc byte[RenderOutputSize];
cleared.Clear();
if (!ctx.Memory.TryWrite(resultAddress, cleared))
Span<byte> result = stackalloc byte[RenderOutputSize];
result.Clear();
if (surfaceAddress != 0 && ctx.TryReadUInt64(surfaceAddress, out buffer) &&
ctx.TryReadUInt32(surfaceAddress + 8, out pitch) &&
ctx.TryReadUInt32(surfaceAddress + 12, out pixelSize))
{
BinaryPrimitives.WriteUInt64LittleEndian(result[8..], buffer);
BinaryPrimitives.WriteUInt32LittleEndian(result[16..], pitch);
result[20] = (byte)pixelSize;
}
BinaryPrimitives.WriteUInt32LittleEndian(result[24..], (uint)Math.Max(left, 0));
BinaryPrimitives.WriteUInt32LittleEndian(result[28..], (uint)Math.Max(top, 0));
BinaryPrimitives.WriteUInt32LittleEndian(result[32..], (uint)Math.Max(width + Math.Min(left, 0), 0));
BinaryPrimitives.WriteUInt32LittleEndian(result[36..], (uint)Math.Max(height + Math.Min(top, 0), 0));
BinaryPrimitives.WriteSingleLittleEndian(result[40..], glyph.BearingX);
BinaryPrimitives.WriteSingleLittleEndian(result[44..], glyph.BearingY);
BinaryPrimitives.WriteSingleLittleEndian(result[48..], glyph.Advance);
BinaryPrimitives.WriteSingleLittleEndian(result[52..], glyph.Advance);
BinaryPrimitives.WriteUInt32LittleEndian(result[56..], (uint)width);
BinaryPrimitives.WriteUInt32LittleEndian(result[60..], (uint)height);
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
if (surfaceAddress != 0 && !DrawGlyph(ctx, surfaceAddress, left, top, width, height, glyph.Coverage))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetSuccess(ctx);
}
private static int SetFontHeight(CpuContext ctx)
{
ctx.GetXmmRegister(0, out var widthBits, out _);
ctx.GetXmmRegister(1, out var heightBits, out _);
var width = BitConverter.UInt32BitsToSingle((uint)widthBits);
var height = BitConverter.UInt32BitsToSingle((uint)heightBits);
if (float.IsFinite(height) && height > 1)
{
FontScales[ctx[CpuRegister.Rdi]] = (
float.IsFinite(width) && width > 1 ? Math.Clamp(width, 8, 128) : Math.Clamp(height, 8, 128),
Math.Clamp(height, 8, 128));
}
return SetSuccess(ctx);
}
private static (float Width, float Height) GetFontScale(ulong font) =>
FontScales.TryGetValue(font, out var scale) ? scale : (16, 16);
private static bool IsValidCode(uint code) => (code & 0xffff0000) == 0x80000000 || Rune.IsValid((int)code);
private static FontGlyph GetGlyph(ulong font, uint code)
{
var scale = GetFontScale(font);
return (FontFaces.TryGetValue(font, out var face) ? face : DefaultFont.Value).GetGlyph(code, scale.Width, scale.Height);
}
private static bool WriteGlyphMetrics(CpuContext ctx, ulong address, FontGlyph glyph, float height)
{
Span<byte> metrics = stackalloc byte[32];
float[] values = [glyph.Width, glyph.Height, glyph.BearingX, glyph.BearingY, glyph.Advance, 0, 0, height];
for (var index = 0; index < values.Length; index++)
{
BinaryPrimitives.WriteSingleLittleEndian(metrics[(index * 4)..], values[index]);
}
return ctx.Memory.TryWrite(address, metrics);
}
internal static bool DrawGlyph(CpuContext ctx, ulong surface, int left, int top,
int glyphWidth, int glyphHeight, byte[] coverage)
{
if (!ctx.TryReadUInt64(surface, out var buffer) ||
!ctx.TryReadUInt32(surface + 8, out var pitch) ||
!ctx.TryReadUInt32(surface + 12, out var pixelSize) ||
!ctx.TryReadUInt32(surface + 16, out var width) ||
!ctx.TryReadUInt32(surface + 20, out var height) ||
!ctx.TryReadUInt32(surface + 24, out var sx0) ||
!ctx.TryReadUInt32(surface + 28, out var sy0) ||
!ctx.TryReadUInt32(surface + 32, out var sx1) ||
!ctx.TryReadUInt32(surface + 36, out var sy1))
{
return false;
}
pixelSize &= 0xff;
if (buffer == 0 || pixelSize is < 1 or > 4 || pitch == 0 || width == 0 || height == 0)
{
return true;
}
var x0 = Math.Max(left, (int)Math.Min(sx0, width));
var y0 = Math.Max(top, (int)Math.Min(sy0, height));
var x1 = Math.Min(left + glyphWidth, (int)Math.Min(sx1, width));
var y1 = Math.Min(top + glyphHeight, (int)Math.Min(sy1, height));
if (x0 >= x1 || y0 >= y1 || (ulong)x1 * pixelSize > pitch)
{
return true;
}
var line = new byte[(x1 - x0) * pixelSize];
for (var yy = y0; yy < y1; yy++)
{
var address = buffer + (ulong)yy * pitch + (ulong)x0 * pixelSize;
if (!ctx.Memory.TryRead(address, line))
{
return false;
}
for (var xx = x0; xx < x1; xx++)
{
var alpha = coverage[(yy - top) * glyphWidth + xx - left];
if (alpha != 0)
{
var pixel = line.AsSpan((xx - x0) * (int)pixelSize, (int)pixelSize);
for (var component = 0; component < pixel.Length; component++)
pixel[component] = (byte)(alpha + (pixel[component] * (255 - alpha) + 127) / 255);
}
}
if (!ctx.Memory.TryWrite(address, line))
{
return false;
}
}
return true;
}
[SysAbiExport(
Nid = "vzHs3C8lWJk",
ExportName = "sceFontCloseFont",
Target = Generation.Gen5,
LibraryName = "libSceFont")]
public static int CloseFont(CpuContext ctx) => SetSuccess(ctx);
public static int CloseFont(CpuContext ctx)
{
FontFaces.TryRemove(ctx[CpuRegister.Rdi], out _);
FontScales.TryRemove(ctx[CpuRegister.Rdi], out _);
return SetSuccess(ctx);
}
[SysAbiExport(
Nid = "1QjhKxrsOB8",

View File

@ -0,0 +1,74 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using SkiaSharp;
namespace SharpEmu.Libs.Font;
internal sealed class FontFace(SKTypeface typeface)
{
private readonly ConcurrentDictionary<(uint Code, float Width, float Height), FontGlyph> _glyphs = new();
public SKFontMetrics GetMetrics(float width, float height)
{
using var font = CreateFont(width, height);
return font.Metrics;
}
public FontGlyph GetGlyph(uint code, float width, float height)
{
// ponytail: bounded working set; clearing avoids retaining every glyph at every scale.
if (_glyphs.Count >= 1024) _glyphs.Clear();
return _glyphs.GetOrAdd((code, width, height), key => Rasterize(key.Code, key.Width, key.Height));
}
private SKFont CreateFont(float width, float height) => new(typeface, height, width / height)
{
Edging = SKFontEdging.Antialias,
Subpixel = false,
};
private FontGlyph Rasterize(uint code, float width, float height)
{
using var font = CreateFont(width, height);
var glyph = (code & 0x80000000) != 0 ? (ushort)code : font.GetGlyph((int)code);
if (glyph == 0 && (code & 0x80000000) == 0)
{
// System font sets span multiple faces. Resolve by Unicode coverage, not language.
var fallback = SKFontManager.Default.MatchCharacter(typeface.FamilyName, typeface.FontStyle, null, (int)code);
if (fallback is not null)
{
font.Typeface = fallback;
glyph = font.GetGlyph((int)code);
}
}
ReadOnlySpan<ushort> glyphs = [glyph];
var advance = font.MeasureText(glyphs, out var bounds);
var left = (int)MathF.Floor(bounds.Left);
var top = (int)MathF.Floor(bounds.Top);
var bitmapWidth = (int)MathF.Ceiling(bounds.Right) - left;
var bitmapHeight = (int)MathF.Ceiling(bounds.Bottom) - top;
if (bitmapWidth <= 0 || bitmapHeight <= 0)
{
return new(0, 0, left, -top, advance, []);
}
using var bitmap = new SKBitmap(bitmapWidth, bitmapHeight, SKColorType.Alpha8, SKAlphaType.Premul);
using var canvas = new SKCanvas(bitmap);
using var paint = new SKPaint { Color = SKColors.White, IsAntialias = true };
using var blob = SKTextBlob.Create(MemoryMarshal.AsBytes(glyphs), SKTextEncoding.GlyphId, font);
canvas.Clear(SKColors.Transparent);
canvas.DrawText(blob!, -left, -top, paint);
var coverage = new byte[bitmapWidth * bitmapHeight];
for (var row = 0; row < bitmapHeight; row++)
{
Marshal.Copy(bitmap.GetPixels() + row * bitmap.RowBytes, coverage, row * bitmapWidth, bitmapWidth);
}
return new(bitmapWidth, bitmapHeight, left, -top, advance, coverage);
}
}
internal sealed record FontGlyph(int Width, int Height, float BearingX, float BearingY, float Advance, byte[] Coverage);

View File

@ -17,6 +17,7 @@ public sealed unsafe class BdaFaultProcessor : IDisposable
private const uint MaxPageFaults = 1024;
private const ulong PageFaultAreaSize = MaxPageFaults * sizeof(ulong);
private static long _reportedFaults;
private readonly GpuDeviceInfo _device;
private readonly SubmissionScheduler _scheduler;
private readonly GuestBufferCache _cache;
@ -249,7 +250,11 @@ public sealed unsafe class BdaFaultProcessor : IDisposable
if (SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.Traces(faults[index], _pageSize))
SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.Trace(faults[index], _pageSize,
$"device-address-fault scan_tick={scanTick} callback_tick={_scheduler.CurrentTick} registered={_cache.IsRegionRegistered(faults[index], _pageSize)} reported_count={(uint)faults[0]} retained_count={count}");
Console.Error.WriteLine($"[GPU][INFO] Accessed non-GPU cached memory at 0x{faults[index]:X16}");
var reported = Interlocked.Increment(ref _reportedFaults);
if (reported <= 16 || (reported & (reported - 1)) == 0)
{
Console.Error.WriteLine($"[GPU][INFO] Accessed non-GPU cached memory at 0x{faults[index]:X16} count={reported}");
}
}
_faultRanges.ForEach((start, size) =>

View File

@ -9,7 +9,8 @@ using VkBuffer = Silk.NET.Vulkan.Buffer;
namespace SharpEmu.Libs.Gpu.Buffers;
// One Vulkan buffer with its own dedicated allocation, mapped when host-visible.
// One Vulkan buffer, mapped when host-visible. Small buffers share slab chunks (GpuMemorySlabs);
// larger ones get a dedicated allocation.
public unsafe class GpuBuffer : IDisposable
{
public const BufferUsageFlags ReadFlags =
@ -28,8 +29,12 @@ public unsafe class GpuBuffer : IDisposable
private readonly ulong _deviceAddress;
private VkBuffer _handle;
private DeviceMemory _memory;
private readonly ulong _memoryOffset;
private readonly GpuMemorySlabs.Block? _slab;
public GpuBuffer(GpuDeviceInfo device, SubmissionScheduler scheduler, GpuBufferUsage usage, ulong cpuAddress, BufferUsageFlags flags, ulong size)
// allowSlab: the caller records on the scheduler's current command buffer, which clears a recycled slab block.
public GpuBuffer(GpuDeviceInfo device, SubmissionScheduler scheduler, GpuBufferUsage usage, ulong cpuAddress, BufferUsageFlags flags, ulong size,
bool allowSlab = false)
{
if (size == 0)
{
@ -43,14 +48,23 @@ public unsafe class GpuBuffer : IDisposable
Size = size;
var vk = device.Vk;
var bufferInfo = new BufferCreateInfo
var sharedFamilies = device.SharedQueueFamilies;
fixed (uint* families = sharedFamilies)
{
SType = StructureType.BufferCreateInfo,
Size = size,
Usage = flags,
SharingMode = SharingMode.Exclusive,
};
RequireSuccess(vk.CreateBuffer(device.Device, &bufferInfo, null, out _handle), "vkCreateBuffer");
// Buffers carry no layout, so concurrent sharing lets the readback queue copy
// them without queue-family ownership transfers at no cost to other queues.
var bufferInfo = new BufferCreateInfo
{
SType = StructureType.BufferCreateInfo,
Size = size,
Usage = flags,
SharingMode = sharedFamilies is { Length: > 1 } ? SharingMode.Concurrent : SharingMode.Exclusive,
QueueFamilyIndexCount = sharedFamilies is { Length: > 1 } ? (uint)sharedFamilies.Length : 0,
PQueueFamilyIndices = sharedFamilies is { Length: > 1 } ? families : null,
};
RequireSuccess(vk.CreateBuffer(device.Device, &bufferInfo, null, out _handle), "vkCreateBuffer");
}
vk.GetBufferMemoryRequirements(device.Device, _handle, out var requirements);
var withAddress = (flags & BufferUsageFlags.ShaderDeviceAddressBit) != 0;
var flagsInfo = new MemoryAllocateFlagsInfo
@ -67,23 +81,43 @@ public unsafe class GpuBuffer : IDisposable
// The best-scored type first; a full heap falls through to the next candidate.
var result = Result.ErrorOutOfDeviceMemory;
foreach (var candidate in RankMemoryTypes(device, requirements.MemoryTypeBits, usage))
var candidates = RankMemoryTypes(device, requirements.MemoryTypeBits, usage).ToArray();
if (allowSlab && GpuMemorySlabs.Fits(requirements))
{
allocateInfo.MemoryTypeIndex = candidate;
result = device.AllocateMemory(allocateInfo, out _memory);
if (result == Result.Success)
foreach (var candidate in candidates)
{
break;
if (device.Slabs.TryAllocate(candidate, withAddress, requirements, out var block))
{
_slab = block;
_memory = block.Memory;
_memoryOffset = block.Offset;
_mapped = block.Mapped;
allocateInfo.MemoryTypeIndex = candidate;
result = Result.Success;
break;
}
}
}
if (_slab is null)
{
foreach (var candidate in candidates)
{
allocateInfo.MemoryTypeIndex = candidate;
result = device.AllocateMemory(allocateInfo, out _memory);
if (result == Result.Success)
{
break;
}
}
}
RequireSuccess(result, $"vkAllocateMemory({usage}, 0x{size:X} bytes)");
RequireSuccess(vk.BindBufferMemory(device.Device, _handle, _memory, 0), "vkBindBufferMemory");
_allocationSize = requirements.Size;
RequireSuccess(vk.BindBufferMemory(device.Device, _handle, _memory, _memoryOffset), "vkBindBufferMemory");
_allocationSize = _slab?.Size ?? requirements.Size;
var properties = device.GetMemoryTypeFlags(allocateInfo.MemoryTypeIndex);
IsCoherent = (properties & MemoryPropertyFlags.HostCoherentBit) != 0;
if ((properties & MemoryPropertyFlags.HostVisibleBit) != 0)
if (_slab is null && (properties & MemoryPropertyFlags.HostVisibleBit) != 0)
{
void* pointer;
RequireSuccess(vk.MapMemory(device.Device, _memory, 0, Vk.WholeSize, 0, &pointer), "vkMapMemory");
@ -99,6 +133,12 @@ public unsafe class GpuBuffer : IDisposable
throw SubmissionScheduler.Fatal("The buffer device address is unavailable.");
}
}
// A dedicated allocation arrives zeroed; keep that for a block another buffer used.
if (_slab is { Recycled: true } && (Size & ~3UL) != 0)
{
Fill(0, Size & ~3UL, 0);
}
}
public VkBuffer Handle => _handle;
@ -128,6 +168,20 @@ public unsafe class GpuBuffer : IDisposable
public void AddStreamScore(int score) => StreamScore += score;
// The highest scheduler tick whose command buffer may write this buffer on the GPU.
// A readback of it only has to wait for that tick, not for all queued work.
public ulong LastGpuWriteTick { get; private set; }
// Called by every path that records a GPU write into this buffer.
public void NoteGpuWrite()
{
var tick = _scheduler.CurrentTick;
if (tick > LastGpuWriteTick)
{
LastGpuWriteTick = tick;
}
}
public void Write(ulong offset, ReadOnlySpan<byte> source)
{
if (_mapped == null || offset > Size || (ulong)source.Length > Size - offset)
@ -189,6 +243,7 @@ public unsafe class GpuBuffer : IDisposable
}
command.EndRendering();
NoteGpuWrite();
var vk = _device.Vk;
var native = new CommandBuffer(command.Handle);
var before = stackalloc BufferMemoryBarrier2[2];
@ -216,6 +271,7 @@ public unsafe class GpuBuffer : IDisposable
var command = _scheduler.Current;
command.EndRendering();
NoteGpuWrite();
var vk = _device.Vk;
var native = new CommandBuffer(command.Handle);
var before = CreateBarrier(offset, size, MemoryAccess, AccessFlags.TransferWriteBit);
@ -229,6 +285,19 @@ public unsafe class GpuBuffer : IDisposable
0, null, 1, &after, 0, null);
}
private int _foreignReads;
private int _disposeDeferred;
public void RetainForeignRead() => Interlocked.Increment(ref _foreignReads);
public void ReleaseForeignRead()
{
if (Interlocked.Decrement(ref _foreignReads) == 0 && Interlocked.Exchange(ref _disposeDeferred, 0) != 0)
{
Dispose();
}
}
public void Dispose()
{
if (_handle.Handle == 0)
@ -236,8 +305,25 @@ public unsafe class GpuBuffer : IDisposable
return;
}
if (Volatile.Read(ref _foreignReads) != 0)
{
Volatile.Write(ref _disposeDeferred, 1);
if (Volatile.Read(ref _foreignReads) != 0 || Interlocked.Exchange(ref _disposeDeferred, 0) == 0)
{
return;
}
}
_device.Vk.DestroyBuffer(_device.Device, _handle, null);
_device.FreeMemory(_memory);
if (_slab is { } slab)
{
_device.Slabs.Release(slab);
}
else
{
_device.FreeMemory(_memory);
}
_handle = default;
_memory = default;
}
@ -280,7 +366,7 @@ public unsafe class GpuBuffer : IDisposable
{
SType = StructureType.MappedMemoryRange,
Memory = _memory,
Offset = begin,
Offset = _memoryOffset + begin,
Size = end - begin,
};
}

View File

@ -10,12 +10,16 @@ namespace SharpEmu.Libs.Gpu.Buffers;
public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
{
private PhysicalDeviceMemoryProperties _memoryProperties;
private readonly Dictionary<Format, FormatProperties> _formatProperties = new();
private readonly Dictionary<(Format, ImageType, ImageTiling, ImageUsageFlags, ImageCreateFlags), (Result Result, ImageFormatProperties Properties)> _imageFormatProperties = new();
private readonly object _gate = new();
// Queried for every draw target from the render thread and from other threads; a hit takes no lock.
// Two threads may query the same missing key once each; the driver answers identically.
private readonly System.Collections.Concurrent.ConcurrentDictionary<Format, FormatProperties> _formatProperties = new();
private readonly System.Collections.Concurrent.ConcurrentDictionary<(Format, ImageType, ImageTiling, ImageUsageFlags, ImageCreateFlags), (Result Result, ImageFormatProperties Properties)> _imageFormatProperties = new();
private int _liveAllocations;
private int _peakAllocations;
// VK_EXT_image_view_min_lod is enabled, so a view can clamp to a texture descriptor's MIN_LOD.
public bool ImageViewMinLodSupported { get; init; }
public GpuDeviceInfo(Vk vk, PhysicalDevice physicalDevice, Device device)
{
Vk = vk;
@ -29,10 +33,18 @@ public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
MaxStorageBufferRange = properties.Limits.MaxStorageBufferRange;
MaxMemoryAllocationCount = properties.Limits.MaxMemoryAllocationCount;
MaxComputeWorkGroupCount = (properties.Limits.MaxComputeWorkGroupCount[0], properties.Limits.MaxComputeWorkGroupCount[1], properties.Limits.MaxComputeWorkGroupCount[2]);
Slabs = new GpuMemorySlabs(this);
}
// Shared chunks the small buffers are carved from; freed at device teardown.
internal GpuMemorySlabs Slabs { get; }
public Vk Vk { get; }
// Queue families that may access every buffer; set before the first buffer is
// created when a second queue family (the async readback queue) reads them.
public uint[]? SharedQueueFamilies { get; set; }
public PhysicalDevice PhysicalDevice { get; }
public Device Device { get; }
@ -65,33 +77,27 @@ public sealed unsafe class GpuDeviceInfo : IImageFormatSupport
public FormatProperties GetFormatProperties(Format format)
{
lock (_gate)
if (!_formatProperties.TryGetValue(format, out var properties))
{
if (!_formatProperties.TryGetValue(format, out var properties))
{
Vk.GetPhysicalDeviceFormatProperties(PhysicalDevice, format, out properties);
_formatProperties[format] = properties;
}
return properties;
Vk.GetPhysicalDeviceFormatProperties(PhysicalDevice, format, out properties);
_formatProperties[format] = properties;
}
return properties;
}
public bool TryGetImageFormatProperties(Format format, ImageType type, ImageTiling tiling, ImageUsageFlags usage, ImageCreateFlags flags, out ImageFormatProperties properties)
{
lock (_gate)
var key = (format, type, tiling, usage, flags);
if (!_imageFormatProperties.TryGetValue(key, out var entry))
{
var key = (format, type, tiling, usage, flags);
if (!_imageFormatProperties.TryGetValue(key, out var entry))
{
var result = Vk.GetPhysicalDeviceImageFormatProperties(PhysicalDevice, format, type, tiling, usage, flags, out var found);
entry = (result, found);
_imageFormatProperties[key] = entry;
}
properties = entry.Properties;
return entry.Result == Result.Success;
var result = Vk.GetPhysicalDeviceImageFormatProperties(PhysicalDevice, format, type, tiling, usage, flags, out var found);
entry = (result, found);
_imageFormatProperties[key] = entry;
}
properties = entry.Properties;
return entry.Result == Result.Success;
}
// Every device-memory allocation goes through here so the live count stays exact.

View File

@ -0,0 +1,171 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Numerics;
using Silk.NET.Vulkan;
namespace SharpEmu.Libs.Gpu.Buffers;
// Small buffers take a power-of-two block out of a shared, persistently mapped chunk
// instead of a dedicated allocation. Games stream constants through fresh ranges every
// frame, and the buffer cache created and freed a device allocation for each of them;
// vkAllocateMemory/vkMapMemory/vkFreeMemory then cost ~15% of the render thread in busy
// Astro Bot scenes, and every live allocation counts against maxMemoryAllocationCount.
internal sealed unsafe class GpuMemorySlabs
{
public const ulong MinBlockSize = 4096;
public const ulong MaxBlockSize = 1024 * 1024;
private const ulong ChunkSize = 8 * 1024 * 1024;
private static readonly int ClassCount = BitOperations.Log2(MaxBlockSize) - BitOperations.Log2(MinBlockSize) + 1;
private readonly GpuDeviceInfo _device;
private readonly object _gate = new();
private readonly Dictionary<(uint MemoryType, bool DeviceAddress), Pool> _pools = [];
private readonly List<DeviceMemory> _chunks = [];
private readonly List<Block> _quarantine = [];
private int _foreignReaders;
public GpuMemorySlabs(GpuDeviceInfo device) => _device = device;
// Recycled blocks still hold their previous buffer's bytes; fresh ones come zeroed with their chunk.
public readonly record struct Block(DeviceMemory Memory, ulong Offset, ulong Size, nint MappedAddress, uint MemoryType, bool DeviceAddress, bool Recycled)
{
public byte* Mapped => (byte*)MappedAddress;
}
private sealed class Pool
{
public readonly Stack<(DeviceMemory Memory, ulong Offset, nint Mapped, bool Recycled)>[] Free =
Enumerable.Range(0, ClassCount).Select(_ => new Stack<(DeviceMemory, ulong, nint, bool)>()).ToArray();
}
// A readback on the second queue copies from buffers the render thread may retire (and
// recycle) while the copy is still pending. A dedicated allocation kept its old bytes
// after vkFreeMemory, which hid that race; a recycled block does not. Released blocks
// therefore wait until no readback that might have picked their buffer is running.
public ForeignReadScope BeginForeignRead()
{
Interlocked.Increment(ref _foreignReaders);
return new ForeignReadScope(this);
}
public readonly struct ForeignReadScope(GpuMemorySlabs owner) : IDisposable
{
public void Dispose() => Interlocked.Decrement(ref owner._foreignReaders);
}
public static bool Fits(in MemoryRequirements requirements) =>
requirements.Size <= MaxBlockSize && requirements.Alignment <= MaxBlockSize;
public bool TryAllocate(uint memoryType, bool deviceAddress, in MemoryRequirements requirements, out Block block)
{
block = default;
var blockSize = Math.Max(MinBlockSize, BitOperations.RoundUpToPowerOf2(Math.Max(requirements.Size, requirements.Alignment)));
if (blockSize > MaxBlockSize)
{
return false;
}
var sizeClass = BitOperations.Log2(blockSize) - BitOperations.Log2(MinBlockSize);
lock (_gate)
{
if (_quarantine.Count != 0 && Volatile.Read(ref _foreignReaders) == 0)
{
foreach (var released in _quarantine)
{
var releasedClass = BitOperations.Log2(released.Size) - BitOperations.Log2(MinBlockSize);
_pools[(released.MemoryType, released.DeviceAddress)].Free[releasedClass]
.Push((released.Memory, released.Offset, released.MappedAddress, true));
}
_quarantine.Clear();
}
if (!_pools.TryGetValue((memoryType, deviceAddress), out var pool))
{
pool = new Pool();
_pools.Add((memoryType, deviceAddress), pool);
}
var free = pool.Free[sizeClass];
if (free.Count == 0 && !TryAddChunk(memoryType, deviceAddress, blockSize, free))
{
return false;
}
var (memory, offset, mapped, recycled) = free.Pop();
block = new Block(memory, offset, blockSize, mapped, memoryType, deviceAddress, recycled);
return true;
}
}
public void Release(in Block block)
{
lock (_gate)
{
_quarantine.Add(block);
}
}
// Frees every chunk; only valid once no buffer still uses one (device teardown).
public void Destroy()
{
lock (_gate)
{
foreach (var chunk in _chunks)
{
_device.FreeMemory(chunk);
}
_chunks.Clear();
_pools.Clear();
_quarantine.Clear();
}
}
private bool TryAddChunk(uint memoryType, bool deviceAddress, ulong blockSize, Stack<(DeviceMemory, ulong, nint, bool)> free)
{
var flagsInfo = new MemoryAllocateFlagsInfo
{
SType = StructureType.MemoryAllocateFlagsInfo,
Flags = MemoryAllocateFlags.DeviceAddressBit,
};
var allocateInfo = new MemoryAllocateInfo
{
SType = StructureType.MemoryAllocateInfo,
PNext = deviceAddress ? &flagsInfo : null,
AllocationSize = ChunkSize,
MemoryTypeIndex = memoryType,
};
if (_device.AllocateMemory(allocateInfo, out var memory) != Result.Success)
{
return false;
}
byte* mapped = null;
if ((_device.GetMemoryTypeFlags(memoryType) & MemoryPropertyFlags.HostVisibleBit) != 0)
{
void* pointer;
if (_device.Vk.MapMemory(_device.Device, memory, 0, Vk.WholeSize, 0, &pointer) != Result.Success)
{
_device.FreeMemory(memory);
return false;
}
mapped = (byte*)pointer;
}
_chunks.Add(memory);
// Push in reverse so blocks come out in address order.
for (var offset = ChunkSize - blockSize; ; offset -= blockSize)
{
free.Push((memory, offset, mapped == null ? 0 : (nint)(mapped + offset), false));
if (offset == 0)
{
break;
}
}
return true;
}
}

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