Compare commits

...

95 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
173 changed files with 10696 additions and 756 deletions

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: 431 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-release.2</SharpEmuVersion>
<SharpEmuVersion>0.0.5-nexus</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>

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 | Astro Bot |
| Demon's Souls Remake | ASTRO BOT |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![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 | Tomb Raider V Remastered |
| God of War: Sons of Sparta | Tomb Raider V Remastered |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Pac-Man](./.github/images/pac-man-world-re-pac.jpg) | ![Tomb Raider V](./.github/images/tomb-raider-v-remastered.jpg) |
| ![GoW SOS](./.github/images/gow-sos.jpg) | ![Tomb Raider V](./.github/images/tomb-raider-v-remastered.jpg) |
## Status

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 268 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
@ -30,7 +30,7 @@ The descriptions come from the code that reads each variable. The emulator was n
| Group | Variables |
| --- | --- |
| Behavior switches | 19 |
| GPU and Vulkan | 58 |
| GPU and Vulkan | 59 |
| Metal | 10 |
| Audio and video | 6 |
| Input | 5 |
@ -127,6 +127,7 @@ These variables change the GPU path or the Vulkan presenter.
| `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` |

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)

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;
@ -176,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

@ -1925,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;
@ -3978,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)
@ -5344,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,
@ -5355,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]);
@ -5378,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);

View File

@ -19,6 +19,44 @@ internal static class GuestRedZonePatcher
private const ulong PageSize = 0x1000;
private const ulong AllocationAlignment = 0x10000;
private const ulong MaximumRelativeJumpDistance = 0x7FFF_FFFF;
private const int MaximumJumpTableEntries = 4096;
private const int MaximumJumpTableSetupInstructions = 6;
private const ulong LargeExecutableBytes = 32UL << 20;
private static readonly TimeSpan ProgressInterval = TimeSpan.FromSeconds(2);
private sealed class Progress(string hostName, string phase, int total)
{
private readonly long _started = System.Diagnostics.Stopwatch.GetTimestamp();
private long _lastReport = System.Diagnostics.Stopwatch.GetTimestamp();
public void Report(int done)
{
var now = System.Diagnostics.Stopwatch.GetTimestamp();
if (System.Diagnostics.Stopwatch.GetElapsedTime(_lastReport, now) < ProgressInterval)
{
return;
}
_lastReport = now;
Console.Error.WriteLine(
$"[LOADER] {hostName} red-zone {phase}: {done}/{total} ({(total == 0 ? 100 : done * 100L / total)}%) " +
$"after {System.Diagnostics.Stopwatch.GetElapsedTime(_started, now).TotalSeconds:F0} s");
}
}
private static ulong ExecutableBytes(IReadOnlyList<ProgramHeader> programHeaders)
{
var bytes = 0UL;
foreach (var header in programHeaders)
{
if (header.HeaderType == ProgramHeaderType.Load && (header.Flags & ProgramHeaderFlags.Execute) != 0)
{
bytes += header.FileSize;
}
}
return bytes;
}
internal enum SpanRefusal
{
@ -27,6 +65,7 @@ internal static class GuestRedZonePatcher
BranchTargetAfter,
StackAfter,
TooShort,
IndirectBranch,
}
public static PatchResult Patch(
@ -62,8 +101,19 @@ internal static class GuestRedZonePatcher
return default;
}
var executableBytes = ExecutableBytes(programHeaders);
var large = executableBytes >= LargeExecutableBytes;
if (large)
{
Console.Error.WriteLine(
$"[LOADER] {hostName} red-zone patch: scanning {functionStarts.Length} functions in {executableBytes >> 20} MB of code; " +
"a large executable takes a while before the game window opens.");
}
var sites = CollectPatchSites(
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan);
memory, programHeaders, imageBase, functionStarts, protectRedZone, splitVectorStores, rewriteSha, out var scan,
large ? new Progress(hostName, "scan", functionStarts.Length) : null);
sites.Sort(static (left, right) => left.Address.CompareTo(right.Address));
if (sites.Count == 0)
{
Console.Error.WriteLine(
@ -94,8 +144,10 @@ internal static class GuestRedZonePatcher
var patched = 0;
var failed = 0;
var previousEnd = 0UL;
var patchProgress = large ? new Progress(hostName, "patch", sites.Count) : null;
foreach (var site in sites)
{
patchProgress?.Report(patched + failed);
// Sites are produced in increasing address order. Patching two that
// overlap would write one jump over another and send the trampoline
// into whatever follows, so refuse rather than corrupt guest code.
@ -128,6 +180,7 @@ internal static class GuestRedZonePatcher
BranchTargetRefusals = scan.BranchTargetRefusals,
StackRefusals = scan.StackRefusals,
TooShortRefusals = scan.TooShortRefusals,
IndirectBranchRefusals = scan.IndirectBranchRefusals,
TrampolineBytes = trampolineCursor - trampolineBase,
};
Console.Error.WriteLine(
@ -135,7 +188,8 @@ internal static class GuestRedZonePatcher
$"sites={result.PatchedSites}/{result.CandidateSites} failed={result.FailedSites} " +
$"unrelocatable={result.UnrelocatableSites} " +
$"(control_flow={result.ControlFlowRefusals} branch_target_after={result.BranchTargetRefusals} " +
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals}) " +
$"stack_after={result.StackRefusals} too_short={result.TooShortRefusals} " +
$"indirect_branch={result.IndirectBranchRefusals}) " +
$"rosetta_vector_stores={result.VectorStoreCount} sha_rewrites={result.ShaInstructionCount} " +
$"trampolines=0x{trampolineBase:X16}+0x{result.TrampolineBytes:X}.");
return result;
@ -149,15 +203,17 @@ internal static class GuestRedZonePatcher
bool protectRedZone,
bool splitVectorStores,
bool rewriteSha,
out PatchResult result)
out PatchResult result,
Progress? progress = null)
{
var sites = new List<PatchSite>();
var scannedFunctions = 0;
var functionCount = 0;
var redZoneFunctionCount = 0;
var instructionCount = 0;
var vectorStoreCount = 0;
var unrelocatableSites = 0;
var refusalCounts = new int[5];
var refusalCounts = new int[6];
var shaInstructionCount = 0;
foreach (var header in programHeaders)
@ -190,6 +246,7 @@ internal static class GuestRedZonePatcher
for (var functionIndex = 0; functionIndex < segmentFunctions.Length; functionIndex++)
{
progress?.Report(scannedFunctions++);
var functionStart = segmentFunctions[functionIndex];
var functionEnd = functionIndex + 1 < segmentFunctions.Length
? segmentFunctions[functionIndex + 1]
@ -222,6 +279,14 @@ internal static class GuestRedZonePatcher
redZoneFunctionCount++;
}
var branchTargets = CollectBranchTargets(decoded);
var canBorrowFollowingInstructions = TryCollectJumpTableTargets(
memory,
programHeaders,
imageBase,
decoded,
functionStart,
functionEnd,
branchTargets);
var lastSiteEnd = 0UL;
@ -236,27 +301,32 @@ internal static class GuestRedZonePatcher
continue;
}
// A backward span reaches instructions that an earlier site
// may already have replaced with its own jump. Two jumps
// written over each other corrupt control flow, so a span is
// only usable when it starts at or after the end of the last
// one. Forward spans cannot overlap by construction; the
// check costs nothing and documents the invariant.
var backward = false;
if (!TryBuildPatchSpan(decoded, instructionIndex, branchTargets, out var span, out var refusal))
var span = default(PatchSite);
var refusal = SpanRefusal.TooShort;
var forward = TryBuildPatchSpan(
decoded,
instructionIndex,
branchTargets,
canBorrowFollowingInstructions,
out span,
out refusal);
var backward = !forward &&
canBorrowFollowingInstructions &&
TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
if (!forward && !backward)
{
backward = TryBuildEnclosingPatchSpan(decoded, instructionIndex, branchTargets, out span);
}
if ((!backward && refusal != SpanRefusal.None) || span.Address < lastSiteEnd)
{
// Refused spans never enter the site list, so they are
// invisible in FailedSites. Count them separately.
unrelocatableSites++;
refusalCounts[(int)refusal]++;
continue;
}
if (span.Address < lastSiteEnd)
{
unrelocatableSites++;
refusalCounts[(int)SpanRefusal.TooShort]++;
continue;
}
sites.Add(span);
lastSiteEnd = span.Address + (ulong)span.ByteLength;
if (splitVectorStores)
@ -268,8 +338,6 @@ internal static class GuestRedZonePatcher
shaInstructionCount += span.Instructions.Count(static instruction => ShaInstructionRewrite.CanRewrite(instruction));
}
// A backward span already ends at the current instruction, so
// only a forward one consumes the instructions that follow.
if (!backward)
{
instructionIndex += span.Instructions.Count - 1;
@ -290,6 +358,7 @@ internal static class GuestRedZonePatcher
BranchTargetRefusals = refusalCounts[(int)SpanRefusal.BranchTargetAfter],
StackRefusals = refusalCounts[(int)SpanRefusal.StackAfter],
TooShortRefusals = refusalCounts[(int)SpanRefusal.TooShort],
IndirectBranchRefusals = refusalCounts[(int)SpanRefusal.IndirectBranch],
ShaInstructionCount = shaInstructionCount,
};
return sites;
@ -347,6 +416,321 @@ internal static class GuestRedZonePatcher
return targets;
}
private static bool TryCollectJumpTableTargets(
IVirtualMemory memory,
IReadOnlyList<ProgramHeader> programHeaders,
ulong imageBase,
IReadOnlyList<DecodedInstruction> decoded,
ulong functionStart,
ulong functionEnd,
HashSet<ulong> branchTargets)
{
List<ulong>? resolvedTargets = null;
HashSet<ulong>? instructionBoundaries = null;
for (var instructionIndex = 0; instructionIndex < decoded.Count; instructionIndex++)
{
if (decoded[instructionIndex].Instruction.FlowControl != FlowControl.IndirectBranch)
{
continue;
}
instructionBoundaries ??= decoded
.Select(static entry => entry.Instruction.IP)
.ToHashSet();
resolvedTargets ??= new List<ulong>();
if (!TryResolveJumpTable(
memory,
programHeaders,
imageBase,
decoded,
instructionIndex,
functionStart,
functionEnd,
instructionBoundaries,
branchTargets,
resolvedTargets))
{
return false;
}
}
if (resolvedTargets is not null)
{
branchTargets.UnionWith(resolvedTargets);
}
return true;
}
private static bool TryResolveJumpTable(
IVirtualMemory memory,
IReadOnlyList<ProgramHeader> programHeaders,
ulong imageBase,
IReadOnlyList<DecodedInstruction> decoded,
int jumpIndex,
ulong functionStart,
ulong functionEnd,
IReadOnlySet<ulong> instructionBoundaries,
IReadOnlySet<ulong> knownBranchTargets,
List<ulong> targets)
{
if (jumpIndex < 4)
{
return false;
}
var jump = decoded[jumpIndex].Instruction;
var add = decoded[jumpIndex - 1].Instruction;
var load = decoded[jumpIndex - 2].Instruction;
if (jump.OpCount != 1 || jump.GetOpKind(0) != OpKind.Register ||
add.Mnemonic != Mnemonic.Add || add.OpCount != 2 ||
add.GetOpKind(0) != OpKind.Register || add.GetOpKind(1) != OpKind.Register ||
load.Mnemonic != Mnemonic.Movsxd || load.OpCount != 2 ||
load.GetOpKind(0) != OpKind.Register || load.GetOpKind(1) != OpKind.Memory ||
load.MemorySize.GetSize() != sizeof(int) || load.MemoryIndexScale != sizeof(int) ||
load.MemoryBase == Register.None || load.MemoryIndex == Register.None ||
load.MemoryDisplacement64 != 0)
{
return false;
}
var targetRegister = jump.GetOpRegister(0).GetFullRegister();
var tableRegister = add.GetOpRegister(1).GetFullRegister();
var selectorRegister = load.MemoryIndex.GetFullRegister();
if (targetRegister != add.GetOpRegister(0).GetFullRegister() ||
targetRegister != load.GetOpRegister(0).GetFullRegister() ||
tableRegister != load.MemoryBase.GetFullRegister() ||
targetRegister.GetSize() != sizeof(ulong) ||
tableRegister.GetSize() != sizeof(ulong) ||
selectorRegister.GetSize() != sizeof(ulong) ||
tableRegister == selectorRegister)
{
return false;
}
var branchIndex = -1;
for (var index = jumpIndex - 3;
index >= 1 && index >= jumpIndex - MaximumJumpTableSetupInstructions;
index--)
{
var candidate = decoded[index].Instruction;
if (candidate.Mnemonic == Mnemonic.Ja)
{
branchIndex = index;
break;
}
if (candidate.Mnemonic != Mnemonic.Lea)
{
return false;
}
}
if (branchIndex < 1)
{
return false;
}
for (var index = branchIndex + 1; index <= jumpIndex; index++)
{
if (knownBranchTargets.Contains(decoded[index].Instruction.IP))
{
return false;
}
}
var boundsBranch = decoded[branchIndex].Instruction;
var boundsCompare = decoded[branchIndex - 1].Instruction;
if (boundsBranch.FlowControl != FlowControl.ConditionalBranch ||
!TryGetNearBranchTarget(boundsBranch, out var defaultTarget) ||
defaultTarget < functionStart || defaultTarget >= functionEnd ||
boundsCompare.Mnemonic != Mnemonic.Cmp || boundsCompare.OpCount != 2 ||
boundsCompare.GetOpKind(0) != OpKind.Register ||
boundsCompare.GetOpRegister(0).GetFullRegister() != selectorRegister ||
boundsCompare.GetOpRegister(0).GetSize() != sizeof(int) ||
!TryGetNonNegativeImmediate(boundsCompare, 1, out var maximumIndex) ||
maximumIndex >= MaximumJumpTableEntries)
{
return false;
}
var tableAddress = 0UL;
var tableDefinitionFound = false;
for (var index = branchIndex + 1; index <= jumpIndex - 3; index++)
{
var candidate = decoded[index].Instruction;
if (candidate.OpCount < 1 || candidate.GetOpKind(0) != OpKind.Register)
{
return false;
}
var writtenRegister = candidate.GetOpRegister(0).GetFullRegister();
if (writtenRegister == selectorRegister)
{
return false;
}
if (writtenRegister != tableRegister)
{
continue;
}
if (tableDefinitionFound || candidate.OpCount != 2 ||
candidate.GetOpKind(1) != OpKind.Memory || !candidate.IsIPRelativeMemoryOperand)
{
return false;
}
tableAddress = candidate.IPRelativeMemoryAddress;
tableDefinitionFound = true;
}
if (!tableDefinitionFound)
{
return false;
}
var entryCount = checked((int)maximumIndex + 1);
var tableBytes = checked(entryCount * sizeof(int));
if (!IsRangeInReadableLoad(programHeaders, imageBase, tableAddress, (ulong)tableBytes))
{
return false;
}
var entries = GC.AllocateUninitializedArray<byte>(tableBytes);
if (!memory.TryRead(tableAddress, entries))
{
return false;
}
var firstTarget = targets.Count;
for (var index = 0; index < entryCount; index++)
{
var relative = BinaryPrimitives.ReadInt32LittleEndian(entries.AsSpan(index * sizeof(int)));
if (!TryAddSignedOffset(tableAddress, relative, out var target) ||
target < functionStart || target >= functionEnd ||
!instructionBoundaries.Contains(target))
{
targets.RemoveRange(firstTarget, targets.Count - firstTarget);
return false;
}
targets.Add(target);
}
return true;
}
private static bool IsRangeInReadableLoad(
IReadOnlyList<ProgramHeader> programHeaders,
ulong imageBase,
ulong address,
ulong length)
{
foreach (var header in programHeaders)
{
if (header.HeaderType != ProgramHeaderType.Load ||
(header.Flags & ProgramHeaderFlags.Read) == 0 ||
header.FileSize == 0 ||
header.VirtualAddress > ulong.MaxValue - imageBase)
{
continue;
}
var start = imageBase + header.VirtualAddress;
if (address < start || address - start > header.FileSize ||
length > header.FileSize - (address - start))
{
continue;
}
return true;
}
return false;
}
private static bool TryAddSignedOffset(ulong address, int offset, out ulong result)
{
if (offset >= 0)
{
var positive = (ulong)offset;
if (positive <= ulong.MaxValue - address)
{
result = address + positive;
return true;
}
}
else
{
var negative = (ulong)-(long)offset;
if (negative <= address)
{
result = address - negative;
return true;
}
}
result = 0;
return false;
}
private static bool TryGetNearBranchTarget(in Instruction instruction, out ulong target)
{
for (var operand = 0; operand < instruction.OpCount; operand++)
{
switch (instruction.GetOpKind(operand))
{
case OpKind.NearBranch16:
target = instruction.NearBranch16;
return true;
case OpKind.NearBranch32:
target = instruction.NearBranch32;
return true;
case OpKind.NearBranch64:
target = instruction.NearBranch64;
return true;
}
}
target = 0;
return false;
}
private static bool TryGetNonNegativeImmediate(in Instruction instruction, int operand, out ulong value)
{
switch (instruction.GetOpKind(operand))
{
case OpKind.Immediate8:
value = instruction.Immediate8;
return true;
case OpKind.Immediate16:
value = instruction.Immediate16;
return true;
case OpKind.Immediate32:
value = instruction.Immediate32;
return true;
case OpKind.Immediate64:
value = instruction.Immediate64;
return true;
case OpKind.Immediate8to16 when instruction.Immediate8to16 >= 0:
value = (ulong)instruction.Immediate8to16;
return true;
case OpKind.Immediate8to32 when instruction.Immediate8to32 >= 0:
value = (ulong)instruction.Immediate8to32;
return true;
case OpKind.Immediate8to64 when instruction.Immediate8to64 >= 0:
value = (ulong)instruction.Immediate8to64;
return true;
case OpKind.Immediate32to64 when instruction.Immediate32to64 >= 0:
value = (ulong)instruction.Immediate32to64;
return true;
default:
value = 0;
return false;
}
}
/// <summary>
/// Runs the enclosing-span search over a raw instruction range, so the
/// behaviour can be pinned on the byte sequences that occur in real guest
@ -376,7 +760,7 @@ internal static class GuestRedZonePatcher
}
if (siteIndex < 0 ||
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), out var site, out _))
!TryBuildPatchSpan(decoded, siteIndex, CollectBranchTargets(decoded), true, out var site, out _))
{
return false;
}
@ -417,7 +801,10 @@ internal static class GuestRedZonePatcher
return false;
}
if (!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
var hasIndirectBranch = decoded.Any(static entry =>
entry.Instruction.FlowControl == FlowControl.IndirectBranch);
if (hasIndirectBranch ||
!TryBuildEnclosingPatchSpan(decoded, faultIndex, CollectBranchTargets(decoded), out var site))
{
return false;
}
@ -551,12 +938,13 @@ internal static class GuestRedZonePatcher
int startIndex,
IReadOnlySet<ulong> branchTargets,
out PatchSite site) =>
TryBuildPatchSpan(decoded, startIndex, branchTargets, out site, out _);
TryBuildPatchSpan(decoded, startIndex, branchTargets, true, out site, out _);
private static bool TryBuildPatchSpan(
IReadOnlyList<DecodedInstruction> decoded,
int startIndex,
IReadOnlySet<ulong> branchTargets,
bool canBorrowFollowingInstructions,
out PatchSite site,
out SpanRefusal refusal)
{
@ -568,6 +956,7 @@ internal static class GuestRedZonePatcher
{
var instruction = decoded[index].Instruction;
if (index != startIndex &&
canBorrowFollowingInstructions &&
!branchTargets.Contains(instruction.IP) &&
IsRelocatableDirectBranch(instruction))
{
@ -578,7 +967,11 @@ internal static class GuestRedZonePatcher
break;
}
if (instruction.FlowControl != FlowControl.Next)
if (index != startIndex && !canBorrowFollowingInstructions)
{
refusal = SpanRefusal.IndirectBranch;
}
else if (instruction.FlowControl != FlowControl.Next)
{
refusal = SpanRefusal.ControlFlow;
}
@ -1089,6 +1482,8 @@ internal static class GuestRedZonePatcher
public int TooShortRefusals { get; init; }
public int IndirectBranchRefusals { get; init; }
public ulong TrampolineBytes { get; init; }
}

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);

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();
@ -204,6 +207,7 @@ public sealed class GuestGpuMemory : IDisposable
try
{
_spans.Add(address, size);
Interlocked.Increment(ref _spanVersion);
}
finally
{
@ -276,6 +280,7 @@ public sealed class GuestGpuMemory : IDisposable
try
{
_spans.Remove(address, size);
Interlocked.Increment(ref _spanVersion);
}
finally
{

View File

@ -50,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(

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

@ -12,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;
@ -99,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",

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;
}

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

@ -22,7 +22,6 @@ public static class AudioOut2Exports
// SceAudioOut2ContextParam: 6 x uint32 + uint32 reserved[10].
private const int AudioOut2ContextParamSize = 0x40;
private const int AudioOut2ContextMemorySize = 0x4000;
private const int AudioOut2ContextMemoryAlignment = 0x100;
// Exact object body size. Do not page-align to 64K — the RAGE Main Thread
// stack-allocates this and a 64K VLA is what planted 0x10000 on the canary.
private const int SpeakerArrayHeaderSize = 0x40;
@ -36,6 +35,17 @@ public static class AudioOut2Exports
private const int SpeakerArrayDivisorFieldOffset = 0x34;
private const int SpeakerArrayResultFieldOffset = 0x3C;
private const uint SpeakerArrayDefaultDivisor = 1;
private const int SpeakerArrayCoefficientBytes = 0x400;
private static readonly string _stackOutBufferModes =
Environment.GetEnvironmentVariable("SHARPEMU_AUDIO_OUT2_STACK_WRITES") ?? "1";
private static bool AllowStackOut(string which) =>
string.Equals(_stackOutBufferModes, "1", StringComparison.Ordinal) ||
_stackOutBufferModes
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Contains(which, StringComparer.OrdinalIgnoreCase);
// SceAudioOut2SpeakerArrayParam: Position* (0x00), uint uiNumSpeakers (0x08),
// uint8 ucIs3d (0x0C), void* pBuffer (0x10), size_t szSize (0x18),
// SceAudioOut2VbapCorrectionParam sVbapCorrection (0x20, 0x20 bytes).
@ -60,6 +70,15 @@ public static class AudioOut2Exports
// SceAudioOut2Attribute: u32 attributeId, 4 pad, const void* value, size_t valueSize.
private const int AttributeEntrySize = 0x18;
private const uint PortAttributeIdPcm = 0;
private const ushort AudioOut2PortTypeVibration = 6;
private const uint PortAttributeIdGain = 1;
private const uint PortAttributeIdPassthrough = 5;
private const uint PortAttributeIdAmbisonics = 8;
internal const uint AmbisonicsNone = uint.MaxValue;
internal const uint AmbisonicsAcnBase = 64;
internal const uint PassthroughLeft = 1;
internal const uint PassthroughRight = 2;
private const float HalfPower = 0.70710677f;
private const ushort PortStateOutputConnectedPrimary = 0x01;
// A host device that already holds this much audio is pacing the context; anything beyond the
@ -80,6 +99,11 @@ public static class AudioOut2Exports
private static long _pushTraceCount;
private static long _submitTraceCount;
private static long _submitSkipTraceCount;
private static long _bedWriteTraceCount;
private static long _portCreateTraceCount;
private static long _portDestroyTraceCount;
private static long _portStateTraceCount;
private static long _attributeCallTraceCount;
private static long _attributePcmTraceCount;
private static readonly ConcurrentDictionary<ulong, byte> SpeakerArrays = new();
@ -131,6 +155,9 @@ public static class AudioOut2Exports
return ahead <= 0 ? 0 : (uint)Math.Min((ahead + GrainTicks - 1) / GrainTicks, QueueDepth);
}
public double DeviceSlotFreeMilliseconds =>
DeviceSlotFreeThreshold(GrainSamples, Frequency, QueueDepth);
// Grains the device holds beyond the cushion, at most QueueDepth.
public uint DeviceQueuedGrains(int queuedMilliseconds)
{
@ -197,8 +224,11 @@ public static class AudioOut2Exports
public uint SamplingFrequency { get; }
public uint GrainSamples { get; }
public ulong PcmAddress;
public float Gain = 1f;
public uint Ambisonics = AmbisonicsNone;
public uint Passthrough;
public int PcmPending;
public byte[]? PcmData;
}
@ -212,6 +242,7 @@ public static class AudioOut2Exports
private static string SecondaryBackendName = "none";
private static ulong PrimaryContextHandle;
private static readonly object HostSubmitGate = new();
private static Func<uint, IHostAudioStream?>? _streamFactoryForTests;
[SysAbiExport(
Nid = "g2tViFIohHE",
@ -238,6 +269,16 @@ public static class AudioOut2Exports
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "v8iOE+j8a5o",
ExportName = "sceAudioOut2MasteringSetParam",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2MasteringSetParam(CpuContext ctx)
{
return SetReturn(ctx, 0);
}
// 3D-audio object latency hint; the host mixer has no object pipeline to
// tune, but failure here makes Yotei tear down its whole ACM context and
// abort audio arena bring-up.
@ -291,8 +332,8 @@ public static class AudioOut2Exports
// ABI: int32_t sceAudioOut2ContextQueryMemory(const SceAudioOut2ContextParam *pParams,
// size_t *pMemorySize) — rdi is the param block, rsi is the one and only out pointer.
var paramAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
var memorySizeAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memorySizeAddress == 0)
{
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
}
@ -310,13 +351,12 @@ public static class AudioOut2Exports
contextMemorySize = checked(0x10000UL + (queueDepth * 0x590UL));
}
// The output is a single size_t wherever it lives. Titles keep other locals right after it
// (Octopath Traveler II divides by the dword at +12), so nothing else may be written.
Span<byte> memorySize = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(memorySize, contextMemorySize);
Console.Error.WriteLine(
$"[LOADER][TRACE] audio_out2.context-query-memory out=0x{memoryInfoAddress:X} size=0x{contextMemorySize:X}");
return ctx.Memory.TryWrite(memoryInfoAddress, memorySize)
$"[LOADER][TRACE] audio_out2.context-query-memory " +
$"out=0x{memorySizeAddress:X} size=0x{contextMemorySize:X}");
return ctx.Memory.TryWrite(memorySizeAddress, memorySize)
? SetReturn(ctx, 0)
: SetReturn(ctx, AudioOut2ErrorInvalidPointer);
}
@ -377,8 +417,20 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx)
{
// Shared backend lifetime is process-wide; just drop the context entry.
Contexts.TryRemove(ctx[CpuRegister.Rdi], out _);
var contextHandle = ctx[CpuRegister.Rdi];
Contexts.TryRemove(contextHandle, out _);
foreach (var (portHandle, port) in Ports)
{
if (port.ContextHandle != contextHandle ||
!Ports.TryRemove(portHandle, out var removedPort))
{
continue;
}
Interlocked.Exchange(ref removedPort.PcmData, null);
removedPort.PcmAddress = 0;
}
return SetReturn(ctx, 0);
}
@ -387,7 +439,38 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2ContextBedWrite",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextBedWrite(CpuContext ctx) => SetReturn(ctx, 0);
public static int AudioOut2ContextBedWrite(CpuContext ctx)
{
var n = Interlocked.Increment(ref _bedWriteTraceCount);
if (n <= 8 || n % 500 == 0)
{
TraceAudioOut2(
$"context-bed-write#{n} context=0x{ctx[CpuRegister.Rdi]:X} " +
$"rsi=0x{ctx[CpuRegister.Rsi]:X} rdx=0x{ctx[CpuRegister.Rdx]:X} " +
$"rcx=0x{ctx[CpuRegister.Rcx]:X} r8=0x{ctx[CpuRegister.R8]:X} " +
$"r9=0x{ctx[CpuRegister.R9]:X}");
Span<byte> probe = stackalloc byte[0x40];
foreach (var (name, address) in new[]
{
("rsi", ctx[CpuRegister.Rsi]),
("rdx", ctx[CpuRegister.Rdx]),
("rcx", ctx[CpuRegister.Rcx]),
("r8", ctx[CpuRegister.R8]),
("r9", ctx[CpuRegister.R9]),
})
{
if (address != 0 && ctx.Memory.TryRead(address, probe))
{
TraceAudioOut2(
$"context-bed-write-probe#{n} {name}=0x{address:X} " +
$"bytes={Convert.ToHexString(probe)}");
}
}
}
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "aII9h5nli9U",
@ -419,6 +502,14 @@ public static class AudioOut2Exports
{
context.DevicePaced = true;
}
if (submitted || advancedGrain == 1)
{
if (blocking != 0)
{
WaitForDeviceSlot(context);
}
}
else
{
// Push is the blocking point of a grain: it waits for the grain an Advance claimed, or
@ -472,11 +563,6 @@ public static class AudioOut2Exports
// [rbp-0x10] zeroed the canary low half and killed Bink Snd @ eboot+0xAE36.
var outLevelAddress = ctx[CpuRegister.Rsi];
var outAvailableAddress = ctx[CpuRegister.Rdx];
if (outLevelAddress == 0)
{
outLevelAddress = outAvailableAddress;
outAvailableAddress = 0;
}
// Titles size their rendering by these: Wwise renders exactly puiAvailableQueues grains per
// update and pushes them non-blocking, so a queue always reported empty made it render a
@ -528,6 +614,41 @@ public static class AudioOut2Exports
var portHandle = ctx[CpuRegister.Rdi];
var attributesAddress = ctx[CpuRegister.Rsi];
var attributeCount = unchecked((uint)ctx[CpuRegister.Rdx]);
var attributeStride = unchecked((uint)ctx[CpuRegister.Rcx]);
var traceCall = Interlocked.Increment(ref _attributeCallTraceCount);
if (traceCall <= 8 && attributesAddress != 0)
{
Span<byte> descriptorProbe = stackalloc byte[0x40];
if (ctx.Memory.TryRead(attributesAddress, descriptorProbe))
{
TraceAudioOut2(
$"port-set-attributes-raw#{traceCall} port=0x{portHandle:X} " +
$"count={attributeCount} stride={attributeStride} address=0x{attributesAddress:X} " +
$"bytes={Convert.ToHexString(descriptorProbe)}");
Span<byte> targetProbe = stackalloc byte[0x20];
for (var offset = 0; offset <= descriptorProbe.Length - sizeof(ulong); offset += sizeof(ulong))
{
var candidate = BinaryPrimitives.ReadUInt64LittleEndian(descriptorProbe[offset..]);
if (!IsWritableOutBuffer(candidate) ||
!ctx.Memory.TryRead(candidate, targetProbe))
{
continue;
}
TraceAudioOut2(
$"port-set-attributes-target#{traceCall} offset=0x{offset:X} " +
$"address=0x{candidate:X} bytes={Convert.ToHexString(targetProbe)}");
}
}
else
{
TraceAudioOut2(
$"port-set-attributes-raw#{traceCall} read-failed address=0x{attributesAddress:X} " +
$"count={attributeCount} stride={attributeStride}");
}
}
if (!Ports.TryGetValue(portHandle, out var port))
{
return SetReturn(ctx, 0);
@ -545,6 +666,7 @@ public static class AudioOut2Exports
Span<byte> entry = stackalloc byte[AttributeEntrySize];
Span<byte> pcm = stackalloc byte[8];
Span<byte> word = stackalloc byte[sizeof(uint)];
for (uint i = 0; i < attributeCount; i++)
{
if (!ctx.Memory.TryRead(attributesAddress + (i * AttributeEntrySize), entry))
@ -555,7 +677,41 @@ public static class AudioOut2Exports
var attributeId = BinaryPrimitives.ReadUInt32LittleEndian(entry);
var valueAddress = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x08..]);
var valueSize = BinaryPrimitives.ReadUInt64LittleEndian(entry[0x10..]);
if (attributeId != PortAttributeIdPcm || valueAddress == 0 || valueSize < 8)
if (valueAddress == 0)
{
continue;
}
if (attributeId is PortAttributeIdGain or PortAttributeIdPassthrough or PortAttributeIdAmbisonics)
{
if (valueSize < sizeof(uint) || !ctx.Memory.TryRead(valueAddress, word))
{
continue;
}
var value = BinaryPrimitives.ReadUInt32LittleEndian(word);
switch (attributeId)
{
case PortAttributeIdGain:
var gain = BitConverter.UInt32BitsToSingle(value);
if (float.IsFinite(gain))
{
port.Gain = gain;
}
break;
case PortAttributeIdPassthrough:
port.Passthrough = value;
break;
default:
port.Ambisonics = value;
break;
}
continue;
}
if (attributeId != PortAttributeIdPcm || valueSize < 8)
{
continue;
}
@ -565,14 +721,38 @@ public static class AudioOut2Exports
continue;
}
port.PcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
Volatile.Write(ref port.PcmPending, port.PcmAddress != 0 ? 1 : 0);
var pcmAddress = BinaryPrimitives.ReadUInt64LittleEndian(pcm);
byte[]? pcmData = null;
if (pcmAddress != 0 &&
TryDecodeDataFormat(port.DataFormat, out var channels, out var bytesPerSample, out _) &&
IsSnapshotPcmPort(port.PortType, channels))
{
int byteLength;
try
{
byteLength = checked((int)port.GrainSamples * channels * bytesPerSample);
}
catch (OverflowException)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
pcmData = new byte[byteLength];
if (!ctx.Memory.TryRead(pcmAddress, pcmData))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
port.PcmAddress = pcmAddress;
Volatile.Write(ref port.PcmData, pcmData);
var n = Interlocked.Increment(ref _attributePcmTraceCount);
if (n <= 8 || n % 500 == 0)
{
TraceAudioOut2(
$"port-set-pcm#{n} port=0x{portHandle:X} pcm=0x{port.PcmAddress:X} " +
$"format=0x{port.DataFormat:X} grains={port.GrainSamples}");
$"bytes={pcmData?.Length ?? 0} format=0x{port.DataFormat:X} " +
$"grains={port.GrainSamples}");
}
}
@ -588,10 +768,10 @@ public static class AudioOut2Exports
{
// ABI: sceAudioOut2PortCreate(SceAudioOut2ContextHandle hCtx,
// const SceAudioOut2PortParam *pParams, SceAudioOut2PortHandle *phVPort) —
// rdx is the only out pointer.
var contextHandle = ctx[CpuRegister.Rdi];
var paramAddress = ctx[CpuRegister.Rsi];
var outPortAddress = ctx[CpuRegister.Rdx];
var outPortAddress = ResolveGuestOutBuffer(ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
var traceCreate = Interlocked.Increment(ref _portCreateTraceCount);
if (outPortAddress == 0)
{
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
@ -612,6 +792,14 @@ public static class AudioOut2Exports
Span<byte> param = stackalloc byte[PortParamSize];
if (ctx.Memory.TryRead(paramAddress, param))
{
if (traceCreate <= 8 || traceCreate % 500 == 0)
{
TraceAudioOut2(
$"port-create-param#{traceCreate} context=0x{contextHandle:X} " +
$"address=0x{paramAddress:X} out=0x{outPortAddress:X} " +
$"bytes={Convert.ToHexString(param)}");
}
portType = BinaryPrimitives.ReadUInt16LittleEndian(param);
dataFormat = BinaryPrimitives.ReadUInt32LittleEndian(param[0x04..]);
var freq = BinaryPrimitives.ReadUInt32LittleEndian(param[0x08..]);
@ -662,13 +850,16 @@ public static class AudioOut2Exports
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
}
if (IsGuestStackAddress(stateAddress) &&
!(AllowStackOut("portstate") && Ports.ContainsKey(portHandle)))
{
TraceAudioOut2(
$"port-get-state skip-stack handle=0x{portHandle:X} state=0x{stateAddress:X}");
return SetReturn(ctx, 0);
}
Span<byte> state = stackalloc byte[PortStateSize];
state.Clear();
// +0x00 u16 output = OUTPUT_PRIMARY (1)
// +0x02 u8 numChannels = from port format when known, else 2
// +0x04 s16 volume = -1 (not applicable outside PADSPK)
// +0x06 u16 rerouteCounter = 0 (the host mixer never reroutes)
// +0x08 u32 flags = 0 (no 3D, not mono-forced)
byte channels = 2;
if (Ports.TryGetValue(portHandle, out var port) &&
TryDecodeDataFormat(port.DataFormat, out var decodedChannels, out _, out _))
@ -678,15 +869,21 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], PortStateOutputConnectedPrimary);
state[0x02] = channels;
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], 127);
if (!ctx.Memory.TryWrite(stateAddress, state))
{
return SetReturn(ctx, AudioOut2ErrorInvalidPointer);
}
TraceAudioOut2(
$"port-get-state handle=0x{portHandle:X} state=0x{stateAddress:X} bytes=0x{PortStateSize:X}");
var traceState = Interlocked.Increment(ref _portStateTraceCount);
if (traceState <= 8 || traceState % 500 == 0)
{
TraceAudioOut2(
$"port-get-state#{traceState} handle=0x{portHandle:X} state=0x{stateAddress:X} " +
$"bytes={Convert.ToHexString(state)}");
}
return SetReturn(ctx, 0);
}
@ -915,7 +1112,17 @@ public static class AudioOut2Exports
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx)
{
Ports.TryRemove(ctx[CpuRegister.Rdi], out _);
var portHandle = ctx[CpuRegister.Rdi];
var removed = Ports.TryRemove(portHandle, out var port);
var n = Interlocked.Increment(ref _portDestroyTraceCount);
if (n <= 8 || n % 500 == 0)
{
TraceAudioOut2(
$"port-destroy#{n} handle=0x{portHandle:X} removed={removed} " +
$"context=0x{port?.ContextHandle ?? 0:X} pcm=0x{port?.PcmAddress ?? 0:X} " +
$"remaining={Ports.Count}");
}
return SetReturn(ctx, 0);
}
@ -1007,13 +1214,20 @@ public static class AudioOut2Exports
{
try
{
var audio = HostPlatform.Current.Audio;
// Deeper host queue than classic AudioOut: FMOD's bursty
// AudioOut2 Push pattern underran a 32 KiB (~171 ms) bed.
PrimaryBackend = audio.OpenStereoPcm16Stream(
context.Frequency,
maxQueuedPcmBytes: 128 * 1024);
PrimaryBackendName = audio.BackendName + "-primary";
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
if (streamFactory is not null)
{
PrimaryBackend = streamFactory(context.Frequency);
PrimaryBackendName = PrimaryBackend is null ? "silent" : "test-primary";
}
else
{
var audio = HostPlatform.Current.Audio;
PrimaryBackend = audio.OpenStereoPcm16Stream(
context.Frequency,
maxQueuedPcmBytes: 128 * 1024);
PrimaryBackendName = audio.BackendName + "-primary";
}
}
catch (Exception exception)
{
@ -1031,11 +1245,20 @@ public static class AudioOut2Exports
{
try
{
var audio = HostPlatform.Current.Audio;
SecondaryBackend = audio.OpenStereoPcm16Stream(
context.Frequency,
maxQueuedPcmBytes: 128 * 1024);
SecondaryBackendName = audio.BackendName + "-secondary";
var streamFactory = Volatile.Read(ref _streamFactoryForTests);
if (streamFactory is not null)
{
SecondaryBackend = streamFactory(context.Frequency);
SecondaryBackendName = SecondaryBackend is null ? "silent" : "test-secondary";
}
else
{
var audio = HostPlatform.Current.Audio;
SecondaryBackend = audio.OpenStereoPcm16Stream(
context.Frequency,
maxQueuedPcmBytes: 128 * 1024);
SecondaryBackendName = audio.BackendName + "-secondary";
}
}
catch (Exception exception)
{
@ -1050,6 +1273,28 @@ public static class AudioOut2Exports
}
}
internal static double DeviceSlotFreeThreshold(uint grainSamples, uint frequency, uint queueDepth) =>
DeviceCushionMilliseconds + (grainSamples * 1000.0 / frequency * Math.Max((long)queueDepth - 1, 0));
private static void WaitForDeviceSlot(ContextState context)
{
var backend = ResolveContextBackend(context, out _);
if (backend is null)
{
return;
}
var limit = context.DeviceSlotFreeMilliseconds;
var deadline = Stopwatch.GetTimestamp() + (Stopwatch.Frequency / 4);
int queued;
while ((queued = backend.QueuedMilliseconds) > limit &&
Stopwatch.GetTimestamp() < deadline &&
!HostSessionControl.IsShutdownRequested)
{
Thread.Sleep((int)Math.Clamp(Math.Ceiling(queued - limit), 1, 50));
}
}
private static uint QueuedGrains(ContextState context)
{
if (context.DevicePaced)
@ -1076,7 +1321,6 @@ public static class AudioOut2Exports
lock (HostSubmitGate)
{
var mix = ArrayPool<float>.Shared.Rent(frames * 2);
var source = ArrayPool<byte>.Shared.Rent(frames * 16 * sizeof(float));
var output = ArrayPool<byte>.Shared.Rent(frames * AudioPcmConversion.OutputFrameSize);
try
{
@ -1085,33 +1329,49 @@ public static class AudioOut2Exports
foreach (var port in Ports.Values)
{
if (port.ContextHandle != context.Handle ||
port.PcmAddress == 0 ||
Interlocked.Exchange(ref port.PcmPending, 0) == 0 ||
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat))
{
continue;
}
var byteLength = checked(frames * ch * bps);
if (byteLength <= 0 || byteLength > source.Length)
var pcmData = TakePortPcmSnapshot(port);
if (pcmData is null)
{
continue;
}
var sourceSpan = source.AsSpan(0, byteLength);
if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan))
if (!TryGetStereoGains(
port.PortType,
ch,
port.Gain,
port.Ambisonics,
port.Passthrough,
out var leftGain,
out var rightGain))
{
continue;
}
var byteLength = checked(frames * ch * bps);
if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float)))
{
continue;
}
if (pcmData.Length < byteLength)
{
continue;
}
MixPortIntoStereo(
sourceSpan,
pcmData.AsSpan(0, byteLength),
mix.AsSpan(0, frames * 2),
frames,
ch,
bps,
isFloat,
additive: mixedPorts > 0);
leftGain,
rightGain);
mixedPorts++;
}
@ -1156,26 +1416,138 @@ public static class AudioOut2Exports
finally
{
ArrayPool<float>.Shared.Return(mix);
ArrayPool<byte>.Shared.Return(source);
ArrayPool<byte>.Shared.Return(output);
}
}
}
internal static byte[]? GetPortPcmSnapshotForTests(ulong portHandle)
{
if (!Ports.TryGetValue(portHandle, out var port))
{
return null;
}
var snapshot = Volatile.Read(ref port.PcmData);
return snapshot?.ToArray();
}
internal static bool HasPortForTests(ulong portHandle) => Ports.ContainsKey(portHandle);
internal static byte[]? TakePortPcmSnapshotForTests(ulong portHandle)
{
return Ports.TryGetValue(portHandle, out var port)
? TakePortPcmSnapshot(port)
: null;
}
private static byte[]? TakePortPcmSnapshot(PortState port) =>
Interlocked.Exchange(ref port.PcmData, null);
internal static void SetStreamFactoryForTests(Func<uint, IHostAudioStream?>? streamFactory) =>
Volatile.Write(ref _streamFactoryForTests, streamFactory);
internal static void ResetForTests()
{
Contexts.Clear();
Ports.Clear();
SpeakerArrays.Clear();
Interlocked.Exchange(ref _nextContextHandle, 1);
Interlocked.Exchange(ref _nextUserHandle, 1);
Interlocked.Exchange(ref _nextPortId, 0);
Interlocked.Exchange(ref _pushTraceCount, 0);
Interlocked.Exchange(ref _submitTraceCount, 0);
Interlocked.Exchange(ref _submitSkipTraceCount, 0);
Interlocked.Exchange(ref _attributePcmTraceCount, 0);
lock (HostBackendGate)
{
PrimaryBackend?.Dispose();
SecondaryBackend?.Dispose();
PrimaryBackend = null;
SecondaryBackend = null;
PrimaryBackendName = "none";
SecondaryBackendName = "none";
PrimaryContextHandle = 0;
}
Volatile.Write(ref _streamFactoryForTests, null);
}
private static bool IsMainOrBgmPort(ushort portType)
{
var kind = portType & 0xFF;
return kind is 0 or 1;
}
private static void MixPortIntoStereo(
internal static bool RoutesToSpeakers(ushort portType) => (portType & 0xFF) is 0 or 1 or 2;
internal static (float Gain, uint Ambisonics, uint Passthrough)? PortMixAttributes(ulong portHandle) =>
Ports.TryGetValue(portHandle, out var port) ? (port.Gain, port.Ambisonics, port.Passthrough) : null;
internal static bool TryGetStereoGains(
ushort portType,
int channels,
float gain,
uint ambisonics,
uint passthrough,
out float left,
out float right)
{
left = 0f;
right = 0f;
if (!RoutesToSpeakers(portType) || gain == 0f)
{
return false;
}
if (!IsObjectPort(portType) || channels != 1)
{
left = gain;
right = gain;
return true;
}
if (ambisonics == AmbisonicsNone)
{
left = passthrough == PassthroughRight ? 0f : passthrough == PassthroughLeft ? gain : gain * HalfPower;
right = passthrough == PassthroughLeft ? 0f : passthrough == PassthroughRight ? gain : gain * HalfPower;
return true;
}
var acn = ambisonics >= AmbisonicsAcnBase;
var component = acn ? ambisonics - AmbisonicsAcnBase : ambisonics;
if (component == 0)
{
left = acn ? gain * HalfPower : gain;
right = left;
return true;
}
if (component == (acn ? 1u : 2u))
{
left = gain * HalfPower;
right = -left;
return true;
}
return false;
}
private static bool IsSnapshotPcmPort(ushort portType, int channels) =>
RoutesToSpeakers(portType) &&
(portType & 0xFF) != AudioOut2PortTypeVibration &&
(!IsObjectPort(portType) || channels == 1);
internal static void MixPortIntoStereo(
ReadOnlySpan<byte> source,
Span<float> mix,
int frames,
int channels,
int bytesPerSample,
bool isFloat,
bool additive)
float leftGain,
float rightGain)
{
var frameSize = channels * bytesPerSample;
for (var frame = 0; frame < frames; frame++)
@ -1204,16 +1576,8 @@ public static class AudioOut2Exports
: ReadNormalizedSample(frameBytes, 1, bytesPerSample, isFloat);
}
if (additive)
{
mix[frame * 2] += left;
mix[(frame * 2) + 1] += right;
}
else
{
mix[frame * 2] = left;
mix[(frame * 2) + 1] = right;
}
mix[frame * 2] += left * leftGain;
mix[(frame * 2) + 1] += right * rightGain;
}
}
@ -1330,13 +1694,19 @@ public static class AudioOut2Exports
private static bool IsDirectMemoryWindowAddress(ulong value) =>
value >= 0x0000_1400_0000_0000UL && value < 0x0000_1800_0000_0000UL;
// Only sceAudioOut2ContextQueryMemory and sceAudioOut2ContextGetQueueLevel still
// pick an out buffer by host address range; both are left untouched here so they do
// not collide with the changes already in flight for them, and these two predicates
// go away with the last of their callers.
private static bool IsGuestStackAddress(ulong value) =>
value >= 0x0000_7FF0_0000_0000UL && value <= 0x0000_7FFF_FFFF_FFFFUL;
private static ulong ResolveGuestOutBuffer(ulong primary, ulong secondary)
{
if (IsWritableOutBuffer(primary))
{
return primary;
}
return IsWritableOutBuffer(secondary) ? secondary : 0;
}
private static bool IsWritableOutBuffer(ulong value) =>
value != 0 &&
value != 0x10000UL &&

View File

@ -881,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",

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

@ -9,15 +9,19 @@ namespace SharpEmu.Libs.Diagnostics;
internal static class DccWriterTrace
{
private const int MaxReports = 64;
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 TargetAddress = ParseAddress(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_WRITER_ADDRESS"));
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 || TargetAddress != 0;
public static readonly bool Enabled = DccEnabled || TargetAddresses.Length != 0;
[ThreadStatic]
public static ulong CurrentProgram;
@ -43,7 +47,7 @@ internal static class DccWriterTrace
public static void Record(IGuestImageCache images, ulong address, ulong size)
{
var targeted = TargetAddress != 0 && TargetAddress >= address && TargetAddress - address < 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)))
{

View File

@ -17,6 +17,30 @@ internal static class GpuReadTrace
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)
{

View File

@ -460,6 +460,72 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
return true;
}
public const ulong MaxHostCopyWords = 64 * 1024;
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
{
if (destination == 0 || source == 0 || sourceWords == 0 || words == 0 || ((destination | source) & 3) != 0 ||
words > MaxHostCopyWords || sourceWords > MaxHostCopyWords)
{
return false;
}
var size = words * sizeof(uint);
var sourceSize = Math.Min(sourceWords, words) * sizeof(uint);
if (size > ulong.MaxValue - destination || sourceSize > ulong.MaxValue - source ||
(source < destination + size && destination < source + sourceSize))
{
return false;
}
var images = RequireImageCache();
var sourceRegion = images.QueryRegion(source, sourceSize);
var destinationRegion = images.QueryRegion(destination, size);
if (HasGpuDirtyBytes(source, sourceSize) || HasGpuDirtyBytes(destination, size) ||
sourceRegion.GpuImageBytes || destinationRegion.GpuImageBytes)
{
return false;
}
var pattern = System.Buffers.ArrayPool<byte>.Shared.Rent((int)sourceSize);
var chunk = System.Buffers.ArrayPool<byte>.Shared.Rent((int)Math.Min(size, 64UL * 1024));
try
{
if (!_backing.TryReadBacking(source, pattern.AsSpan(0, (int)sourceSize)))
{
return false;
}
if (destinationRegion.ImageBytes)
{
images.InvalidateMemory(destination, size);
}
var chunkLength = (ulong)(chunk.Length - chunk.Length % sizeof(uint));
for (ulong offset = 0; offset < size;)
{
var length = (int)Math.Min(size - offset, chunkLength);
for (var filled = 0; filled < length;)
{
var patternOffset = (int)((offset + (ulong)filled) % sourceSize);
var copied = Math.Min(length - filled, (int)sourceSize - patternOffset);
pattern.AsSpan(patternOffset, copied).CopyTo(chunk.AsSpan(filled, copied));
filled += copied;
}
WriteHostMemory(destination + offset, chunk.AsSpan(0, length));
offset += (ulong)length;
}
return true;
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(pattern);
System.Buffers.ArrayPool<byte>.Shared.Return(chunk);
}
}
public void CopyBuffer(ulong dstVaddr, ulong srcVaddr, ulong size, bool dstGds, bool srcGds)
{
var dstMemory = !dstGds;
@ -557,7 +623,21 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
private ulong _bdaTouchTick = ulong.MaxValue;
private ulong _bdaTouchMapping;
public static ulong MappingKey(IReadOnlyCollection<GuestSpan> spans)
{
var mapping = (ulong)spans.Count;
foreach (var span in spans)
mapping = (mapping ^ span.Address ^ (span.Size << 17)) * 0x100000001B3UL;
return mapping;
}
public void PrepareBda(IEnumerable<GuestSpan> mapped)
{
var spans = mapped as IReadOnlyCollection<GuestSpan> ?? mapped.ToList();
PrepareBda(spans, MappingKey(spans));
}
public void PrepareBda(IReadOnlyCollection<GuestSpan> mapped, ulong mapping)
{
var traceAddress = GuestGpuMemoryHook.TraceAddress;
var traceCovered = false;
@ -572,10 +652,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
}
else
{
var spans = mapped as IReadOnlyCollection<GuestSpan> ?? mapped.ToList();
var mapping = (ulong)spans.Count;
foreach (var span in spans)
mapping = (mapping ^ span.Address ^ (span.Size << 17)) * 0x100000001B3UL;
var spans = mapped;
if (_retirementPolicy.CurrentTick != _bdaTouchTick || mapping != _bdaTouchMapping)
{
foreach (var span in spans)

View File

@ -26,6 +26,8 @@ public enum SliceResult
// Submissions of every queue; one thread runs the slices, other threads enqueue and wait.
public sealed class CommandStreamQueue
{
private sealed record ControlBarrier(ulong CutoffOrdinal, Action Work, Action Cancel);
public const int ComputeQueueCount = 56;
public const int QueueCount = 1 + ComputeQueueCount;
public const int AllBlockedRetryMilliseconds = 100;
@ -37,8 +39,10 @@ public sealed class CommandStreamQueue
private readonly object _gate = new();
private readonly LinkedList<CommandSubmission>[] _queues = new LinkedList<CommandSubmission>[QueueCount];
private readonly GpuCommandInterpreter?[] _interpreters = new GpuCommandInterpreter?[QueueCount];
private readonly Queue<ControlBarrier> _controlBarriers = new();
private int _nextQueue;
private int _submissionCount;
private ulong _admissionOrdinal;
private bool _accepting = true;
private bool _stopping;
private bool _processing;
@ -91,7 +95,7 @@ public sealed class CommandStreamQueue
{
lock (_gate)
{
return _submissionCount != 0;
return _submissionCount != 0 || _controlBarriers.Count != 0;
}
}
}
@ -113,7 +117,7 @@ public sealed class CommandStreamQueue
{
lock (_gate)
{
return SelectQueueLocked() >= 0;
return HasRunnableWorkLocked();
}
}
}
@ -200,6 +204,7 @@ public sealed class CommandStreamQueue
throw _host.Fatal($"The command stream no longer accepts submissions: queue={submission.QueueId} address=0x{submission.Address:X16}.");
}
submission.AdmissionOrdinal = ++_admissionOrdinal;
_queues[submission.QueueId].AddLast(submission);
_submissionCount++;
if (submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary))
@ -208,6 +213,23 @@ public sealed class CommandStreamQueue
Monitor.PulseAll(_gate);
}
public bool TryEnqueueControlBarrier(Action work, Action cancel)
{
ArgumentNullException.ThrowIfNull(work);
ArgumentNullException.ThrowIfNull(cancel);
lock (_gate)
{
if (!_accepting)
{
return false;
}
_controlBarriers.Enqueue(new ControlBarrier(_admissionOrdinal, work, cancel));
Monitor.PulseAll(_gate);
return true;
}
}
public IdleOutcome Done()
{
if (_processingThread == Thread.CurrentThread)
@ -257,7 +279,8 @@ public sealed class CommandStreamQueue
{
lock (_gate)
{
while (_outcome == IdleOutcome.Completed && (_processing || _submissionCount != 0))
while (_outcome == IdleOutcome.Completed &&
(_processing || _submissionCount != 0 || _controlBarriers.Count != 0))
{
Monitor.Wait(_gate);
}
@ -279,13 +302,13 @@ public sealed class CommandStreamQueue
{
lock (_gate)
{
if (SelectQueueLocked() >= 0 || _stopping)
if (HasRunnableWorkLocked() || _stopping)
{
return SelectQueueLocked() >= 0;
return HasRunnableWorkLocked();
}
Monitor.Wait(_gate, timeoutMilliseconds);
return SelectQueueLocked() >= 0;
return HasRunnableWorkLocked();
}
}
@ -295,7 +318,7 @@ public sealed class CommandStreamQueue
lock (_gate)
{
var deadline = Environment.TickCount64 + intervalMilliseconds;
while (SelectQueueLocked() < 0)
while (!HasRunnableWorkLocked())
{
var remaining = deadline - Environment.TickCount64;
if (remaining <= 0)
@ -313,10 +336,15 @@ public sealed class CommandStreamQueue
// Publishes a failure: nothing runs any more and every waiter is released with Failed.
public void Fail()
{
Action[] cancellations;
lock (_gate)
{
FailLocked();
cancellations = CancelControlBarriersLocked();
}
foreach (var cancel in cancellations)
cancel();
}
private void FailLocked()
@ -364,7 +392,8 @@ public sealed class CommandStreamQueue
// Runs one slice of the next unblocked submission on the calling thread.
public SliceResult ProcessOne()
{
CommandSubmission submission;
CommandSubmission? submission = null;
ControlBarrier? control = null;
lock (_gate)
{
if (_processing)
@ -372,35 +401,73 @@ public sealed class CommandStreamQueue
throw _host.Fatal("The command stream queue is already processing a slice.");
}
var selected = SelectQueueLocked();
if (selected < 0)
if (IsControlBarrierReadyLocked())
{
return _submissionCount != 0 ? SliceResult.AllBlocked : SliceResult.NoWork;
control = _controlBarriers.Dequeue();
}
else
{
var selected = SelectQueueLocked();
if (selected < 0)
{
return _submissionCount != 0 ? SliceResult.AllBlocked : SliceResult.NoWork;
}
var queue = _queues[selected];
submission = queue.First!.Value;
queue.RemoveFirst();
_submissionCount--;
_nextQueue = (selected + 1) % QueueCount;
}
var queue = _queues[selected];
submission = queue.First!.Value;
queue.RemoveFirst();
_submissionCount--;
_nextQueue = (selected + 1) % QueueCount;
_processing = true;
_processingThread = Thread.CurrentThread;
if (!submission.Started && submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary))
if (submission is { Started: false } &&
submission.Kind is not (CommandSubmissionKind.FlipPreparation or CommandSubmissionKind.FrameBoundary))
SubmissionFlowProfile.Record(SubmissionFlowProfile.EventKind.ProcessingStarted, submission.QueueId,
submission.SubmissionId, submission.Address, submission.DwordCount, _submissionCount);
}
if (control is not null)
{
try
{
control.Work();
}
catch (Exception exception)
{
try
{
Console.Error.WriteLine($"[GPU][ERROR] Command stream control barrier failed. {exception}");
}
catch (Exception)
{
}
Fail();
throw;
}
lock (_gate)
{
_processing = false;
Monitor.PulseAll(_gate);
}
return SliceResult.Completed;
}
bool complete;
try
{
complete = RunSlice(submission);
complete = RunSlice(submission!);
}
catch (Exception exception)
{
try
{
// Report the cause before another submitting thread observes the failed queue.
Console.Error.WriteLine($"[GPU][ERROR] Command stream slice failed: queue={submission.QueueId} address=0x{submission.Address:X16}. {exception}");
Console.Error.WriteLine($"[GPU][ERROR] Command stream slice failed: queue={submission!.QueueId} address=0x{submission.Address:X16}. {exception}");
}
catch (Exception)
{
@ -416,7 +483,7 @@ public sealed class CommandStreamQueue
SliceResult result;
if (!complete)
{
submission.Blocked = true;
submission!.Blocked = true;
submission.BlockedSinceTicks = System.Diagnostics.Stopwatch.GetTimestamp();
submission.BlockedPacketAddress = submission.Commands.CurrentPacketAddress;
_queues[submission.QueueId].AddFirst(submission);
@ -597,12 +664,17 @@ public sealed class CommandStreamQueue
// Drops every submission without stopping admission; the presenter uses it after device loss.
public void DiscardAll()
{
Action[] cancellations;
lock (_gate)
{
_outcome = IdleOutcome.Failed;
DropAllLocked();
cancellations = CancelControlBarriersLocked();
Monitor.PulseAll(_gate);
}
foreach (var cancel in cancellations)
cancel();
}
// A diagnostic view of the blocked heads: their count, the oldest age and one sample.
@ -669,6 +741,20 @@ public sealed class CommandStreamQueue
private int SelectQueueLocked()
{
if (_controlBarriers.TryPeek(out var control))
{
for (var offset = 0; offset < QueueCount; offset++)
{
var queueId = (_nextQueue + offset) % QueueCount;
if (_queues[queueId].First is { } head &&
head.Value.AdmissionOrdinal <= control.CutoffOrdinal &&
!head.Value.Blocked)
{
return queueId;
}
}
}
for (var offset = 0; offset < QueueCount; offset++)
{
var queueId = (_nextQueue + offset) % QueueCount;
@ -681,6 +767,42 @@ public sealed class CommandStreamQueue
return -1;
}
private bool HasRunnableWorkLocked() => IsControlBarrierReadyLocked() || SelectQueueLocked() >= 0;
private bool IsControlBarrierReadyLocked()
{
if (!_controlBarriers.TryPeek(out var control))
{
return false;
}
foreach (var queue in _queues)
{
if (queue.First is { } head && head.Value.AdmissionOrdinal <= control.CutoffOrdinal)
{
return false;
}
}
return true;
}
private Action[] CancelControlBarriersLocked()
{
if (_controlBarriers.Count == 0)
{
return [];
}
var cancellations = new Action[_controlBarriers.Count];
for (var index = 0; index < cancellations.Length; index++)
{
cancellations[index] = _controlBarriers.Dequeue().Cancel;
}
return cancellations;
}
private void DropAllLocked()
{
foreach (var queue in _queues)

View File

@ -101,6 +101,8 @@ public sealed class CommandSubmission
// The submitter's own sequence number, kept for diagnostics and the translation state.
public ulong SubmissionId { get; }
internal ulong AdmissionOrdinal { get; set; }
// Snapshots captured at submit time; the processor never reads them.
public object? GeometrySnapshots { get; }

View File

@ -3,6 +3,7 @@
using System.Runtime.InteropServices;
using SharpEmu.Libs.Gpu.GpuCommands.Packets;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Scheduling;
namespace SharpEmu.Libs.Gpu.GpuCommands;
@ -10,6 +11,7 @@ namespace SharpEmu.Libs.Gpu.GpuCommands;
public sealed partial class GpuCommandInterpreter
{
private const uint DmaSkippedDestination = 0x3022C;
private const uint LodStatsWithoutCounters = 1;
// Destination selectors 1, 2, 4 and 5 are memory; registers and GDS are not supported.
internal void WriteData(ulong destination, ReadOnlySpan<uint> source, uint writeControl)
@ -367,12 +369,12 @@ public sealed partial class GpuCommandInterpreter
var destination = (payload[1] & 0xFFFF_FFC0u) | ((ulong)payload[2] << 32);
if (destination != 0 && bufferSize != 0)
{
// The statistics buffer is zeroed and its first dword marks it as valid.
var zeros = new byte[bufferSize];
WriteBytes(destination, zeros);
var report = new byte[bufferSize];
var hasCounters = MipStatistics.Shared.TryWriteReport(report);
WriteBytes(destination, report);
if (bufferSize >= sizeof(uint))
{
WriteDword(destination, 1);
WriteDword(destination, hasCounters ? MipStatistics.FirstCounterOffset : LodStatsWithoutCounters);
}
}

View File

@ -57,6 +57,11 @@ internal interface IGuestGpuBackend
// Marks the frame boundary; off the worker it first waits for the accepted submissions.
IdleOutcome SubmitDone(ICpuMemory memory);
void RunAfterPendingCommandStreams(Action work)
{
work();
}
/// <summary>Registers a display buffer with its guest texture format tag.</summary>
void RegisterKnownDisplayBuffer(ulong address, uint guestFormat);

View File

@ -63,7 +63,7 @@ public struct TileTransferArguments
// Runs the tiling, detiling, depth conversion and channel swap compute passes.
public sealed unsafe class GpuTiler : IDisposable
{
private const uint KindCount = 9;
private const uint KindCount = 10;
private const uint ElementSizeCount = 5;
private const uint DirectionCount = 2;
private const uint PipelineCount = KindCount * ElementSizeCount * DirectionCount;

View File

@ -1,13 +1,271 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Scheduling;
using Silk.NET.Vulkan;
namespace SharpEmu.Libs.Gpu.Images;
// Surface metadata (HTile, DCC, CMask, FMask) keyed by its guest address.
public sealed partial class GuestImageCache
{
private unsafe void MaterializeDccClear(
ResourceSlotIdentifier imageIdentifier,
in ImageRequest request,
uint metadataBaseLayer)
{
if (request.Description.Metadata.Kind != MetadataKind.Dcc)
{
return;
}
var description = request.Description;
var range = description.Metadata.Range;
using (var held = _lock.Hold())
{
var image = _slots.TryGet(imageIdentifier);
if (image == null)
{
return;
}
image.Description.Metadata = description.Metadata;
if (range.Size == 0 || description.Resources.Levels != 1 ||
image.Description.Resources.Levels != 1)
{
return;
}
_surfaceMetadata.Remove(range.Address);
}
var layers = description.TransferLayers;
const ulong metadataBlockSize = 0x1000;
if (!ImageDescription.IsValidRange(range) ||
range.Address % metadataBlockSize != 0 ||
layers == 0 ||
range.Size % layers != 0 ||
(range.Size / layers) % metadataBlockSize != 0)
{
throw SubmissionScheduler.Fatal(
$"DCC slices must contain aligned 4 KiB blocks: address=0x{range.Address:X16} size=0x{range.Size:X} layers={layers}.");
}
var view = request.View;
var volume = description.IsVolume && view.Type == ImageViewType.Type3D;
var first = volume ? 0u : metadataBaseLayer;
var imageFirst = volume ? 0u : view.BaseLayer;
var count = volume ? description.Extent.Depth : view.LayerCount;
if (first >= layers || count == 0 || count > layers - first)
{
throw SubmissionScheduler.Fatal(
$"The DCC view exceeds its metadata slices: first={first} count={count} layers={layers}.");
}
var sliceSize = range.Size / layers;
var synchronizedAddress = range.Address + sliceSize * first;
var synchronizedSize = sliceSize * count;
if (_bufferCache.HasGpuDirtyBytes(synchronizedAddress, synchronizedSize))
{
_bufferCache.ReadMemory(synchronizedAddress, synchronizedSize, isWrite: false);
}
const int metadataScanChunkSize = 64 * 1024;
var bytes = System.Buffers.ArrayPool<byte>.Shared.Rent(
checked((int)Math.Min(sliceSize, (ulong)metadataScanChunkSize)));
try
{
Span<byte> firstByte = stackalloc byte[1];
for (uint slice = 0; slice < count; slice++)
{
var address = range.Address + sliceSize * (first + slice);
if (!_backing.TryReadBacking(address, firstByte))
{
throw SubmissionScheduler.Fatal(
$"Could not read DCC metadata key: address=0x{address:X16}.");
}
var code = firstByte[0];
if (!PackedClearValue.TryDecodeDccColor(
request.View.Format,
code * 0x01010101u,
description.Metadata,
allowClearToRegister: request.Role == ImageRole.ColorTarget,
out var clear))
{
continue;
}
var uniform = true;
for (ulong position = 0; position < sliceSize && uniform;)
{
var length = checked((int)Math.Min(
sliceSize - position,
(ulong)Math.Min(bytes.Length, metadataScanChunkSize)));
var chunk = bytes.AsSpan(0, length);
if (!_backing.TryReadBacking(address + position, chunk))
{
throw SubmissionScheduler.Fatal(
$"Could not read DCC metadata backing: address=0x{address + position:X16} size=0x{length:X}.");
}
for (var index = 0; index < chunk.Length; index++)
{
if (chunk[index] != code)
{
uniform = false;
break;
}
}
position += (ulong)length;
}
if (!uniform)
{
continue;
}
using (var held = _lock.Hold())
{
if (_slots.TryGet(imageIdentifier) == null)
{
return;
}
ClearDccImage(
imageIdentifier,
request,
new SubresourceRange(view.BaseLevel, view.LevelCount, imageFirst + slice, 1),
clear);
}
if (request.Role != ImageRole.DisplaySurface)
{
_bufferCache.FillBuffer(address, sliceSize, uint.MaxValue, isGds: false);
}
}
}
finally
{
System.Buffers.ArrayPool<byte>.Shared.Return(bytes);
}
}
private unsafe void ClearDccImage(
ResourceSlotIdentifier imageIdentifier,
in ImageRequest request,
in SubresourceRange range,
in ClearColorValue clear)
{
var image = _slots[imageIdentifier];
ref readonly var description = ref image.Description;
if (range.BaseLevel >= description.Resources.Levels || range.LevelCount == 0 ||
range.LevelCount > description.Resources.Levels - range.BaseLevel)
{
throw SubmissionScheduler.Fatal(
$"The DCC clear mip range is invalid: base={range.BaseLevel} count={range.LevelCount} levels={description.Resources.Levels}.");
}
var layers = description.IsVolume
? Math.Max(description.Extent.Depth >> (int)range.BaseLevel, 1)
: image.Backing.Layers;
if (range.LayerCount == 0 || range.BaseLayer >= layers || range.LayerCount > layers - range.BaseLayer)
{
throw SubmissionScheduler.Fatal(
$"The DCC clear layer range is invalid: base={range.BaseLayer} count={range.LayerCount} layers={layers}.");
}
var fullImage = range.BaseLevel == 0 && range.LevelCount == description.Resources.Levels &&
range.BaseLayer == 0 && range.LayerCount == layers;
WatchImage(imageIdentifier);
if (!fullImage && (image.IsBufferModified || image.IsCpuDirty))
{
PopulateFromGuest(imageIdentifier, RefreshRequest(image), "before-dcc-clear");
if (image.Description.Samples == 1 && (image.IsBufferModified || image.IsCpuDirty))
{
throw SubmissionScheduler.Fatal(
$"The DCC clear left guest ownership in place: address=0x{description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty}.");
}
}
var scheduled = _scheduler.Current;
if (scheduled.IsInvalid)
{
throw SubmissionScheduler.Fatal("A DCC clear needs a command buffer that is recording.");
}
scheduled.EndRendering();
var command = new CommandBuffer(scheduled.Handle);
var needsAttachmentClear = request.View.Format != image.Backing.Format ||
(description.IsVolume && !fullImage);
if (needsAttachmentClear)
{
var viewDescription = new ImageViewDescription(
request.View.Format,
range.LayerCount == 1 ? ImageViewType.Type2D : ImageViewType.Type2DArray,
ImageAspectFlags.ColorBit,
range.BaseLevel,
range.LevelCount,
range.BaseLayer,
range.LayerCount,
default,
ImageUsageFlags.ColorAttachmentBit);
var view = image.GetOrCreateView(viewDescription);
image.Transition(
ImageLayout.ColorAttachmentOptimal,
AccessFlags.ColorAttachmentWriteBit,
range,
command);
var attachment = new RenderingAttachmentInfo
{
SType = StructureType.RenderingAttachmentInfo,
ImageView = view,
ImageLayout = ImageLayout.ColorAttachmentOptimal,
LoadOp = AttachmentLoadOp.Clear,
StoreOp = AttachmentStoreOp.Store,
ClearValue = new ClearValue { Color = clear },
};
var rendering = new RenderingInfo
{
SType = StructureType.RenderingInfo,
RenderArea = new Rect2D(
new Offset2D(0, 0),
new Extent2D(
Math.Max(description.Extent.Width >> (int)range.BaseLevel, 1),
Math.Max(description.Extent.Height >> (int)range.BaseLevel, 1))),
LayerCount = range.LayerCount,
ColorAttachmentCount = 1,
PColorAttachments = &attachment,
};
_device.Vk.CmdBeginRendering(command, &rendering);
_device.Vk.CmdEndRendering(command);
}
else
{
image.Transition(ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit, range, command);
var nativeRange = new ImageSubresourceRange(
ImageAspectFlags.ColorBit,
range.BaseLevel,
range.LevelCount,
description.IsVolume ? 0u : range.BaseLayer,
description.IsVolume ? 1u : range.LayerCount);
var nativeClear = clear;
_device.Vk.CmdClearColorImage(
command,
image.Backing.Handle,
ImageLayout.TransferDstOptimal,
&nativeClear,
1,
&nativeRange);
}
TakeGpuOwnership(image);
}
public bool IsMetadata(ulong address)
{
using var held = _lock.Hold();
@ -57,7 +315,6 @@ public sealed partial class GuestImageCache
return true;
}
// True when registered DCC absorbed the fill and the guest dispatch can be skipped.
public bool TryAbsorbDccFill(ulong address, ulong size, uint fillValue)
{
if (!IsValidRange(address, size))

View File

@ -25,7 +25,7 @@ public sealed partial class GuestImageCache
private static ColorTransferPlan? PieceHashPlan(CachedImage image, in ImageRequest request)
{
ref readonly var info = ref image.Description;
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || info.IsVolume || info.IsDepth ||
if (request.Role is not (ImageRole.Texture or ImageRole.StorageImage) || image.DepthOwner.IsValid || info.IsVolume || info.IsDepth ||
info.Data.Size < PieceHashMinimumImageSize)
{
return null;

View File

@ -725,15 +725,20 @@ public sealed unsafe partial class GuestImageCache
}
var range = image.Description.Data;
var download = _bufferCache.GetUtilityBuffer(GpuBufferUsage.Download);
if (!download.TryMap(range.Size, out var offset, Math.Max(image.Description.BytesPerBlock, 4u)))
var ring = _bufferCache.GetUtilityBuffer(GpuBufferUsage.Download);
GpuBuffer download = ring;
if (ring.TryMap(range.Size, out var offset, Math.Max(image.Description.BytesPerBlock, 4u)))
{
throw SubmissionScheduler.Fatal($"The reusable download ring cannot map the image: address=0x{range.Address:X16} size=0x{range.Size:X}.");
ring.Commit();
}
else
{
download = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, range.Size);
}
download.Commit();
if (!_backing.TryReadBacking(range.Address, download.Mapped.Slice((int)offset, (int)range.Size)))
{
if (download != ring) download.Dispose();
return false;
}
@ -761,6 +766,8 @@ public sealed unsafe partial class GuestImageCache
throw SubmissionScheduler.Fatal($"The image readback could not be written to guest memory: address=0x{range.Address:X16} size=0x{range.Size:X}.");
}
});
// Completion actions wait for the priority readback before freeing spill buffers.
if (download != ring) _scheduler.QueueCompletionAction(download.Dispose);
return true;
}

View File

@ -123,6 +123,52 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
return GetNullImage(request);
}
var metadataBaseLayer = request.View.BaseLayer;
var result = LookUpImage(ref request, exactFormat);
if (request.Role is ImageRole.Texture or ImageRole.StorageImage)
{
ulong metadataAddress;
ulong sliceSize;
using (var held = _lock.Hold())
{
var image = _slots[result];
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0 &&
image.Description.Metadata.Kind == MetadataKind.None)
{
image.Description.Metadata = request.Description.Metadata;
}
ref readonly var description = ref image.Description;
metadataAddress = description.Metadata.Range.Address;
sliceSize = description.DccSliceSize;
}
if (sliceSize != 0)
{
SynchronizeGuestDccMetadata(metadataAddress, sliceSize, request.View.BaseLayer, request.View.LayerCount);
}
}
MaterializeDccClear(result, request, metadataBaseLayer);
if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed)
{
using var held = _lock.Hold();
var image = _slots[result];
var guestDirty = image.IsBufferModified || image.IsCpuDirty;
var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty;
if (!nativeCurrent)
{
throw SubmissionScheduler.Fatal(
$"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}.");
}
}
return result;
}
private ResourceSlotIdentifier LookUpImage(ref ImageRequest request, bool exactFormat)
{
using var held = _lock.Hold();
if (TryReuseLookup(ref request, exactFormat, out var reused))
{
@ -205,17 +251,6 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
}
var image = _slots[result];
if (request.Role == ImageRole.DisplaySurface && request.Description.Metadata.Compression != DisplayCompression.Uncompressed)
{
var guestDirty = image.IsBufferModified || image.IsCpuDirty;
var nativeCurrent = (image.Uses.RenderTarget || image.IsGpuModified) && !guestDirty;
if (!nativeCurrent)
{
throw SubmissionScheduler.Fatal(
$"A compressed display surface can only be read from clean native GPU contents: address=0x{image.Description.Data.Address:X16} bufferModified={image.IsBufferModified} cpuDirty={image.IsCpuDirty} gpuModified={image.IsGpuModified}.");
}
}
if (viewMip >= 0)
{
request.View = request.View with { BaseLevel = (uint)viewMip };
@ -327,8 +362,7 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
image.MarkGpuModified();
image.Uses.RenderTarget = true;
RefreshFromGuest(imageIdentifier, request);
// DCC lives in its own allocation; it is registered at bind time, keeping a pending fill.
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
if (request.Description.Metadata.Kind == MetadataKind.Dcc && request.Description.Metadata.Range.Size == 0)
{
image.Description.Metadata = request.Description.Metadata;
var address = request.Description.Metadata.Range.Address;

View File

@ -28,8 +28,11 @@ public struct MetadataDescription
public GuestSpan Range;
public MetadataKind Kind;
public uint Control;
public uint DccClearWord;
public DisplayCompression Compression;
public bool StencilCompressed;
public bool DccClearRegisterValid;
public bool DccAlphaMsb;
}
public readonly record struct SubresourceCount(uint Levels, uint Layers) : IComparable<SubresourceCount>
@ -366,7 +369,8 @@ public struct ImageDescription
{
case MetadataKind.None:
if (!IsEmptyRange(Metadata.Range) || Metadata.Control != 0 ||
Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed)
Metadata.DccClearWord != 0 || Metadata.Compression != DisplayCompression.Uncompressed || Metadata.StencilCompressed ||
Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
{
throw SubmissionScheduler.Fatal("An image without metadata carries metadata state.");
}
@ -378,6 +382,11 @@ public struct ImageDescription
throw SubmissionScheduler.Fatal($"The HTile metadata is invalid: address=0x{Metadata.Range.Address:X16} size=0x{Metadata.Range.Size:X16}.");
}
if (Metadata.DccClearWord != 0 || Metadata.DccClearRegisterValid || Metadata.DccAlphaMsb)
{
throw SubmissionScheduler.Fatal("HTile metadata carries DCC clear state.");
}
break;
case MetadataKind.Dcc:
if (Metadata.Range.Address == 0 || Metadata.Range.Address >= TrackerLayout.SpaceBytes ||
@ -507,6 +516,87 @@ public static class DisplayFormatRule
// Decoders for the packed 32-bit clear values guest compute fills carry.
public static class PackedClearValue
{
public static bool SupportsDccFixedColor(Format format) => DccComponentCount(format) != 0;
public static bool TryDecodeDccColor(
Format format,
uint metadataFill,
in MetadataDescription metadata,
bool allowClearToRegister,
out ClearColorValue clear)
{
clear = default;
var code = (byte)metadataFill;
if (metadataFill != code * 0x01010101u)
{
return false;
}
if (code == 0x20)
{
return allowClearToRegister && metadata.DccClearRegisterValid &&
TryDecodeColor(format, metadata.DccClearWord, out clear);
}
if (code == 0x00)
{
return true;
}
var components = DccComponentCount(format);
if (components == 0 || code is not (0x40 or 0x80 or 0xc0))
{
return false;
}
var rgb = (code & 0x80) != 0 ? 1.0f : 0.0f;
var alpha = (code & 0x40) != 0 ? 1.0f : 0.0f;
Span<float> channels = stackalloc float[4] { rgb, rgb, rgb, alpha };
if (!metadata.DccAlphaMsb)
{
(channels[0], channels[3]) = (channels[3], channels[0]);
}
if (components == 1)
{
channels[0] = channels[3];
}
else if (components == 2)
{
channels[1] = channels[3];
}
switch (format)
{
case Format.B8G8R8A8Unorm:
case Format.B8G8R8A8Srgb:
case Format.A2R10G10B10UnormPack32:
case Format.A1R5G5B5UnormPack16:
case Format.R5G6B5UnormPack16:
(channels[0], channels[2]) = (channels[2], channels[0]);
break;
case Format.R4G4B4A4UnormPack16:
(channels[0], channels[3]) = (channels[3], channels[0]);
(channels[1], channels[2]) = (channels[2], channels[1]);
break;
}
clear = new ClearColorValue(channels[0], channels[1], channels[2], channels[3]);
return true;
}
private static uint DccComponentCount(Format format) => format switch
{
Format.R8Unorm or Format.R16Unorm or Format.R16Sfloat or Format.R32Sfloat => 1,
Format.R8G8Unorm or Format.R16G16Unorm or Format.R16G16Sfloat or Format.R32G32Sfloat => 2,
Format.R5G6B5UnormPack16 or Format.B10G11R11UfloatPack32 => 3,
Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb or Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or
Format.A2B10G10R10UnormPack32 or Format.A2R10G10B10UnormPack32 or Format.A1R5G5B5UnormPack16 or
Format.R4G4B4A4UnormPack16 or Format.R16G16B16A16Unorm or Format.R16G16B16A16Sfloat or
Format.R32G32B32A32Sfloat => 4,
_ => 0,
};
public static bool TryDecodeColor(Format format, uint packed, out ClearColorValue clear)
{
static float Unorm8(uint value) => (value & 0xff) / 255.0f;

View File

@ -43,24 +43,38 @@ public static partial class ImageRequestBuilders
public static uint SampleCount(uint encodedLog2) => encodedLog2 <= 3 ? 1u << (int)encodedLog2 : 0;
private static bool DccAlphaOnMsb(in ColorTargetWords words)
{
switch (words.Layout)
{
case ChannelLayout.Bits10_10_10_2:
case ChannelLayout.Bits10_10_10_2Float:
case ChannelLayout.Bits5_5_5_1:
return true;
case ChannelLayout.Bits2_10_10_10:
case ChannelLayout.Bits1_5_5_5:
return false;
}
var components = GuestPixelFormats.ResolveRenderTargetEncoding(words.Layout, words.NumberType).Components;
if (components == 1)
{
return words.Order != ChannelOrder.Standard;
}
return components == 3 || words.Order is ChannelOrder.Standard or ChannelOrder.Alternate;
}
// DCC clears use the target's packed clear word; the fixed-clear set is a format allow-list.
private static (bool Supported, bool FixedSupported, ClearColorValue Value) DccClearInfo(Format format, bool hasDcc, uint packedClear)
{
ClearColorValue value = default;
var supported = hasDcc && PackedClearValue.TryDecodeColor(format, packedClear, out value);
var fixedSupported = hasDcc && SupportsDccFixedClear(format);
var fixedSupported = hasDcc && PackedClearValue.SupportsDccFixedColor(format);
return (supported, fixedSupported, supported ? value : default);
}
public static bool SupportsDccFixedClear(Format format) => format switch
{
Format.R8Unorm or Format.R8G8Unorm or Format.R8G8B8A8Unorm or Format.R8G8B8A8Srgb or Format.B8G8R8A8Unorm or Format.B8G8R8A8Srgb or
Format.A2B10G10R10UnormPack32 or Format.A2R10G10B10UnormPack32 or Format.R5G6B5UnormPack16 or Format.A1R5G5B5UnormPack16 or
Format.R4G4B4A4UnormPack16 or Format.R16Unorm or Format.R16G16Unorm or Format.R16G16B16A16Unorm or Format.R16Sfloat or
Format.R16G16Sfloat or Format.R16G16B16A16Sfloat or Format.R32Sfloat or Format.R32G32Sfloat or Format.R32G32B32A32Sfloat or
Format.B10G11R11UfloatPack32 => true,
_ => false,
};
public static bool SupportsDccFixedClear(Format format) => PackedClearValue.SupportsDccFixedColor(format);
// Builds the request for a bound color target. Null when the slot carries no target.
public static ColorTargetResolution? ColorTarget(in ColorTargetWords words, uint targetMask, uint drawLayerOffset, bool ignoreTargetMask)
@ -324,8 +338,14 @@ public static partial class ImageRequestBuilders
if (hasDcc)
{
// DCC lives in its own allocation; the address lets the cache match fills seen before the target.
_ = TileGeometry.TryGetDccSize(
width, height, volume ? depth : view.ImageLayers, bytesPerElement, levels, tileMode,
out var metadataSize, words.FragmentsLog2);
description.Metadata.Kind = MetadataKind.Dcc;
description.Metadata.Range = new GuestSpan(words.DccAddress, 0);
description.Metadata.Range = new GuestSpan(words.DccAddress, metadataSize.Size);
description.Metadata.DccClearWord = words.ClearWord0;
description.Metadata.DccClearRegisterValid = true;
description.Metadata.DccAlphaMsb = DccAlphaOnMsb(words);
}
for (var level = 0; level < levels; level++)

View File

@ -58,8 +58,6 @@ public static partial class ImageRequestBuilders
return Format.Undefined;
}
private static bool HtileStencilCompatible(bool hasStencil, bool hasHtile, bool htileStencilDisabled) => !hasStencil || !hasHtile || htileStencilDisabled;
// Builds the request for the bound depth target. Null when no depth or stencil state is active.
public static DepthTargetResolution? DepthTarget(in DepthTargetWords depthWords, IImageFormatSupport device) =>
DepthTargetCore(in depthWords, device, copyMode: false, writeBuffer: false);
@ -127,7 +125,7 @@ public static partial class ImageRequestBuilders
if (hasStencil)
{
if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt || !HtileStencilCompatible(hasStencil, hasHtile, depthWords.HtileStencilDisabled) ||
if (depthWords.StencilFormat != GuestStencilFormat.Stencil8UInt ||
stencilAddress == 0 || (!copyMode && !depthWords.StencilWriteDisabled && depthWords.StencilWriteBase != depthWords.StencilReadBase) || (stencilAddress & 0xFFFF) != 0)
{
throw SubmissionScheduler.Fatal(

View File

@ -300,6 +300,17 @@ public static partial class ImageRequestBuilders
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
description.Samples = samples;
description.TileMode = tile;
if (!shape.R128 && words.Length >= 8 && descriptor.MetadataCompress &&
tile != GuestTileMode.Depth && !description.IsDepth)
{
_ = TileGeometry.TryGetDccSize(
width, height, volume ? depth : imageLayers, description.BytesPerBlock, levels, tile,
out var metadataSize, (uint)System.Numerics.BitOperations.TrailingZeroCount(samples));
description.Metadata.Kind = MetadataKind.Dcc;
description.Metadata.Range = new GuestSpan(descriptor.MetadataAddress << 8, metadataSize.Size);
description.Metadata.DccAlphaMsb = descriptor.DccAlphaMsb;
}
if (samples > 1)
{
description.MipLayout[0] = new MipLevelLayout { Offset = 0, Size = size.Size, Pitch = pitch, Height = height };

View File

@ -0,0 +1,53 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
namespace SharpEmu.Libs.Gpu.Images;
public sealed class MipStatistics
{
public const int SlotCount = 256;
public const uint FirstCounterOffset = 64;
public const int ReportBytes = (int)FirstCounterOffset + SlotCount * CounterBytes;
public const uint NoMipSampled = 15;
private const int CounterBytes = 2 * sizeof(uint);
private const uint FinestMip = 0;
private const uint SamplesPerBinding = 0x10000;
private const uint MaxSamples = 0xFFFFFF;
public static MipStatistics Shared { get; } = new();
private readonly uint[] _bindingsBySlot = new uint[SlotCount];
public void NoteSampled(in TextureDescriptorWords texture)
{
if (texture.MipStatsEnabled && !texture.IsNull)
{
_bindingsBySlot[texture.MipStatsSlot]++;
}
}
public bool TryWriteReport(Span<byte> report)
{
if (report.Length < ReportBytes)
{
return false;
}
for (var slot = 0; slot < SlotCount; slot++)
{
var samples = (uint)Math.Min((ulong)_bindingsBySlot[slot] * SamplesPerBinding, MaxSamples);
var finestMipSampled = samples == 0 ? NoMipSampled : FinestMip;
var clampedSamplesAndSlot = samples | ((uint)slot << 24);
var unclampedSamplesAndFinestMip = samples | (finestMipSampled << 24);
var counter = report.Slice((int)FirstCounterOffset + slot * CounterBytes, CounterBytes);
BinaryPrimitives.WriteUInt32LittleEndian(counter, clampedSamplesAndSlot);
BinaryPrimitives.WriteUInt32LittleEndian(counter[sizeof(uint)..], unclampedSamplesAndFinestMip);
}
Array.Clear(_bindingsBySlot);
return true;
}
}

View File

@ -68,12 +68,18 @@ public readonly struct TextureDescriptorWords
public uint MinLodWarning => (Fields[5] >> 8) & 0xFFF;
public bool MipStatsEnabled => ((Fields[5] >> 25) & 0x1) == 1;
public uint MipStatsSlot => (Fields[6] >> 0) & 0xFF;
public bool MsaaDepth => ((Fields[6] >> 10) & 0x1) == 1;
public bool WriteCompress => ((Fields[6] >> 20) & 0x1) == 1;
public bool MetadataCompress => ((Fields[6] >> 21) & 0x1) == 1;
public bool DccAlphaMsb => ((Fields[6] >> 22) & 0x1) == 1;
public ulong MetadataAddress => ((Fields[6] >> 24) & 0xFF) | ((ulong)Fields[7] << 8);
}

View File

@ -452,7 +452,9 @@ public sealed class TextureTransferLayout
transfer.Width = Math.Max((region.ImageExtent.Width + texture.TexelWidth - 1) / texture.TexelWidth, 1);
transfer.Height = Math.Max((logicalHeight + texture.TexelHeight - 1) / texture.TexelHeight, 1);
transfer.Depth = 1;
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB ? region.ImageSubresource.BaseArrayLayer : 0;
transfer.SurfaceZ = block.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB
? region.ImageSubresource.BaseArrayLayer
: 0;
transfer.Pitch = Math.Max((pitch + texture.TexelWidth - 1) / texture.TexelWidth, 1);
transfer.Tail = tail;
transfer.TailX = tail ? mip.TailX : 0;

View File

@ -230,6 +230,75 @@ public static partial class TileGeometry
}
}
private static uint Gen5RenderTargetOffset(uint x, uint y, uint bytesPerElement)
{
uint offset = 0;
switch (bytesPerElement)
{
case 1:
offset ^= (y << 2) & 0x0008;
offset ^= (y << 4) & 0x0010;
offset ^= (y << 3) & 0x00a0;
offset ^= (y << 5) & 0x0f00;
offset ^= (y << 6) & 0x1000;
offset ^= (y << 7) & 0x4000;
offset ^= x & 0x0007;
offset ^= (x << 3) & 0x0040;
offset ^= (x << 5) & 0x0300;
offset ^= (x << 4) & 0x0400;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 7) & 0x2000;
offset ^= (x << 8) & 0x8000;
return offset;
case 2:
offset ^= (y << 4) & 0x0070;
offset ^= (y << 5) & 0x0f00;
offset ^= (y << 8) & 0x5000;
offset ^= (x << 1) & 0x000e;
offset ^= (x << 4) & 0x0480;
offset ^= (x << 5) & 0x0300;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 7) & 0x2000;
offset ^= (x << 8) & 0x8000;
return offset;
case 4:
offset ^= (y << 4) & 0x0070;
offset ^= (y << 5) & 0x0f00;
offset ^= (y << 9) & 0x1000;
offset ^= (y << 8) & 0x4000;
offset ^= (x << 2) & 0x000c;
offset ^= (x << 5) & 0x0380;
offset ^= (x << 4) & 0x0400;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 9) & 0xa000;
return offset;
case 8:
offset ^= (y << 4) & 0x0010;
offset ^= (y << 6) & 0x0080;
offset ^= (y << 5) & 0x0f00;
offset ^= (y << 10) & 0x5000;
offset ^= (x << 3) & 0x0008;
offset ^= (x << 4) & 0x0460;
offset ^= (x << 5) & 0x0300;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 10) & 0x2000;
offset ^= (x << 9) & 0x8000;
return offset;
case 16:
offset ^= (x << 4) & 0x0410;
offset ^= (x << 5) & 0x0340;
offset ^= (x << 6) & 0x0800;
offset ^= (x << 11) & 0xa000;
offset ^= (y << 5) & 0x0f20;
offset ^= (y << 6) & 0x0080;
offset ^= (y << 10) & 0x1000;
offset ^= (y << 11) & 0x4000;
return offset;
default:
throw SubmissionScheduler.Fatal($"The Gen5 render-target tile block does not support this element size: bytes={bytesPerElement}.");
}
}
private static uint Depth64KB8X(uint x) =>
(x & 0x0001) ^ ((x << 1) & 0x0004) ^ ((x << 2) & 0x0010) ^ ((x << 3) & 0x0040) ^ ((x << 5) & 0x0300) ^
((x << 4) & 0x0400) ^ ((x << 6) & 0x0800) ^ ((x << 7) & 0x2000) ^ ((x << 8) & 0x8000);
@ -319,6 +388,14 @@ public static partial class TileGeometry
offset = RenderTargetOffset(x, y, bytes);
break;
case TileBlockKind.RenderTarget64KBGen5:
if (bytes > 16)
{
return false;
}
offset = Gen5RenderTargetOffset(x, y, bytes);
break;
case TileBlockKind.Depth64KB:
switch (bytes)
{
@ -364,17 +441,19 @@ public static partial class TileGeometry
byteOffset = Depth64KB64X(blockX * layout.BlockWidth) ^ Depth64KB64Y(blockY * layout.BlockHeight);
}
else if (layout.Kind == TileBlockKind.RenderTarget64KB)
else if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5)
{
if (blockX > uint.MaxValue / layout.BlockWidth || blockY > uint.MaxValue / layout.BlockHeight || layout.BytesPerElement > 16)
{
return false;
}
byteOffset = RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
byteOffset = layout.Kind == TileBlockKind.RenderTarget64KBGen5
? Gen5RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement)
: RenderTargetOffset(blockX * layout.BlockWidth, blockY * layout.BlockHeight, layout.BytesPerElement);
}
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.Depth64KB)
if (layout.Kind is TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 or TileBlockKind.Depth64KB)
{
byteOffset ^= ((blockZ & 8) << 5) ^ ((blockZ & 4) << 7) ^ ((blockZ & 2) << 9) ^ ((blockZ & 1) << 11);
}

View File

@ -32,6 +32,7 @@ public enum TileBlockKind : uint
Prt64KB3D,
RenderTarget64KB,
Depth64KB,
RenderTarget64KBGen5,
}
public readonly record struct TileBlockLayout(TileBlockKind Kind, uint BytesPerElement, uint BlockSize, uint BlockWidth, uint BlockHeight, uint BlockDepth)
@ -109,6 +110,7 @@ public static partial class TileGeometry
new(65536, Thick64KB, 16),
new(65536, Thin64KB, 16),
new(65536, Thin64KB, 8),
new(65536, Thin64KB, 16),
];
private static readonly Log2BlockDimensions[][] MsaaBlocks =
@ -247,6 +249,7 @@ public static partial class TileGeometry
case TileBlockKind.Standard64KB:
case TileBlockKind.Prt64KB:
case TileBlockKind.RenderTarget64KB:
case TileBlockKind.RenderTarget64KBGen5:
rule = new MipTailRule(TailThin64KB[index], block.BlockWidth >> 1, block.BlockHeight);
return true;
default:
@ -297,7 +300,7 @@ public static partial class TileGeometry
kind = TileBlockKind.Depth64KB;
break;
case GuestTileMode.RenderTarget:
kind = TileBlockKind.RenderTarget64KB;
kind = TileBlockKind.RenderTarget64KBGen5;
break;
default:
return false;
@ -308,7 +311,7 @@ public static partial class TileGeometry
return false;
}
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB &&
if (kind is TileBlockKind.Depth64KB or TileBlockKind.RenderTarget64KB or TileBlockKind.RenderTarget64KBGen5 &&
GuestPixelFormats.RenderTargetBytesPerElement(format) != element.Bytes)
{
return false;
@ -605,6 +608,39 @@ public static partial class TileGeometry
return true;
}
public static bool TryGetDccSize(
uint width,
uint height,
uint slices,
uint bytesPerElement,
uint levels,
GuestTileMode tile,
out TileSizeAndAlignment totalSize,
uint fragmentsLog2 = 0)
{
totalSize = default;
if (width == 0 || height == 0 || slices == 0 || levels != 1 || fragmentsLog2 != 0 ||
!BitOperations.IsPow2(bytesPerElement) || bytesPerElement > 16 ||
tile is not (GuestTileMode.RenderTarget or GuestTileMode.Depth))
{
return false;
}
var coverageBits = 20 - BitOperations.TrailingZeroCount(bytesPerElement);
var blockWidth = 1u << ((coverageBits + 1) / 2);
var blockHeight = 1u << (coverageBits / 2);
var blocksX = ((ulong)width + blockWidth - 1) / blockWidth;
var blocksY = ((ulong)height + blockHeight - 1) / blockHeight;
var blocks = blocksX * blocksY;
if (blocks == 0 || blocks > uint.MaxValue / 4096u / slices)
{
return false;
}
totalSize = new TileSizeAndAlignment((uint)(blocks * slices * 4096u), 4096);
return true;
}
public static bool TryGetRenderTargetMipLayout(uint width, uint height, uint pitch, uint bytesPerElement, uint levels, out TileSizeAndAlignment totalSize, TileLevelSpan[]? levelSpans, TilePaddedSize[]? paddedSizes)
{
totalSize = default;

View File

@ -1196,4 +1196,10 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
_ = (address, size, fillValue);
return false;
}
bool IRenderHost.TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words)
{
_ = (destination, source, sourceWords, words);
return false;
}
}

View File

@ -124,6 +124,57 @@ internal sealed class MetalGuestGpuBackend : IGuestGpuBackend, IGuestImageSnapsh
public IdleOutcome SubmitDone(ICpuMemory memory) => EnsureCommandStream(memory).Done();
public void RunAfterPendingCommandStreams(Action work)
{
ArgumentNullException.ThrowIfNull(work);
CommandStreamQueue? commandStream;
CommandStreamWorker? worker;
lock (_commandStreamGate)
{
commandStream = _commandStream;
worker = _commandStreamWorker;
}
if (commandStream is null || worker?.IsWorkerThread == true)
{
work();
return;
}
using var done = new SemaphoreSlim(0);
System.Runtime.ExceptionServices.ExceptionDispatchInfo? failure = null;
if (!commandStream.TryEnqueueControlBarrier(
() =>
{
try
{
work();
}
catch (Exception exception)
{
failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception);
}
finally
{
done.Release();
}
},
() =>
{
failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(
new OperationCanceledException("The command stream stopped before the video-out state change could run."));
done.Release();
}))
{
work();
return;
}
done.Wait();
failure?.Throw();
}
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
MetalVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);

View File

@ -26,8 +26,104 @@ public sealed record BoundedFill(
FillWord[]? Pattern = null,
FillWord? PatternLength = null);
public sealed record BoundedCopy(
uint GroupScalarRegister,
FillWord Count,
FillWord Modulus,
FillWord[] Source,
FillWord[] Destination);
public static class BoundedFillDetector
{
private static readonly string[] ModuloCopyOpcodes =
[
"VLshlAddU32", "SBufferLoadDword", "VCmpxGtU32", "SCbranchExecz", "SBufferLoadDword",
"VCvtF32U32", "SCmpLgU32", "SCselectB64", "VRcpIflagF32", "VMulF32", "VCvtU32F32", "VMadU64U32", "VCmpNeU32", "VSubI32",
"VCndmaskB32", "VMulHiU32", "VSubI32", "VAddI32", "VCndmaskB32", "VMulHiU32", "VMulLoU32", "VSubI32", "VCmpGeU32",
"VCmpLeU32", "SAndB64", "VAddcU32", "VAddCoCiU32", "VCndmaskB32", "VMulLoU32", "VSubI32",
"BufferLoadFormatX", "BufferStoreFormatX", "SEndpgm",
];
public static BoundedCopy? DetectCopy(Gen5ShaderProgram program)
{
var instructions = program.Instructions
.Where(instruction => instruction.Opcode is not ("SWaitcnt" or "STtraceData" or "SInstPrefetch") &&
!(instruction.Opcode == "SMovB32" && instruction.Destinations is [{ Kind: Gen5OperandKind.ScalarRegister, Value: MarkerRegister }]))
.ToArray();
if (!instructions.Select(instruction => instruction.Opcode).SequenceEqual(ModuloCopyOpcodes))
{
return null;
}
var countLoad = instructions[1];
var modulusLoad = instructions[4];
if (instructions[0].Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } index] ||
instructions[0].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } group, { Kind: Gen5OperandKind.EncodedConstant, Value: Shift64 },
{ Kind: Gen5OperandKind.VectorRegister, Value: ThreadIndexRegister }] ||
countLoad.Destinations is not [{ Kind: Gen5OperandKind.ScalarRegister } count] ||
modulusLoad.Destinations is not [{ Kind: Gen5OperandKind.ScalarRegister } modulus] ||
countLoad.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } constants, _] ||
modulusLoad.Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } modulusConstants, _] ||
modulusConstants.Value != constants.Value ||
instructions[2].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } limit, { Kind: Gen5OperandKind.VectorRegister } compared] ||
limit.Value != count.Value || compared.Value != index.Value ||
instructions[5].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } converted] || converted.Value != modulus.Value ||
instructions[6].Sources is not [{ Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }, { Kind: Gen5OperandKind.ScalarRegister } tested] ||
tested.Value != modulus.Value)
{
return null;
}
foreach (var at in new[] { 11, 20, 23, 28 })
{
if (instructions[at].Sources is not [{ Kind: Gen5OperandKind.ScalarRegister } multiplier, ..] || multiplier.Value != modulus.Value)
{
return null;
}
}
var load = instructions[30];
var store = instructions[31];
if (instructions[29].Destinations is not [{ Kind: Gen5OperandKind.VectorRegister } remainder] ||
instructions[29].Sources is not [{ Kind: Gen5OperandKind.VectorRegister } dividend, { Kind: Gen5OperandKind.VectorRegister }] ||
dividend.Value != index.Value ||
load.Control is not Gen5BufferMemoryControl { DwordCount: 1, OffsetBytes: 0, IndexEnabled: true, OffsetEnabled: false, Typed: false } loadControl ||
store.Control is not Gen5BufferMemoryControl { DwordCount: 1, OffsetBytes: 0, IndexEnabled: true, OffsetEnabled: false, Typed: false } storeControl ||
load.Sources is not [_, _, { Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }] ||
store.Sources is not [_, _, { Kind: Gen5OperandKind.EncodedConstant, Value: ZeroConstant }] ||
loadControl.VectorAddress != remainder.Value || storeControl.VectorAddress != index.Value ||
storeControl.VectorData != loadControl.VectorData)
{
return null;
}
var branch = instructions[3];
if (branch.Pc + sizeof(uint) + (uint)((short)(branch.Words[0] & 0xFFFF) * sizeof(uint)) != instructions[^1].Pc)
{
return null;
}
var resourceRegisters = Enumerable.Range(0, 4)
.SelectMany(word => new[] { loadControl.ScalarResource + (uint)word, storeControl.ScalarResource + (uint)word });
var constantRegisters = Enumerable.Range(0, 4).Select(word => constants.Value + (uint)word);
if (resourceRegisters.Any(WrittenScalars(instructions, instructions.Length).Contains) ||
constantRegisters.Any(WrittenScalars(instructions, 4).Contains) ||
WrittenScalars(instructions[5..29], 24).Contains(modulus.Value) ||
countLoad.Control is not Gen5ScalarMemoryControl { DynamicOffsetRegister: null, ImmediateOffsetBytes: >= 0 } countControl ||
modulusLoad.Control is not Gen5ScalarMemoryControl { DynamicOffsetRegister: null, ImmediateOffsetBytes: >= 0 } modulusControl)
{
return null;
}
FillWord User(uint register) => new(FillWordSource.UserData, register, 0, 0);
return new BoundedCopy(
group.Value,
new FillWord(FillWordSource.BufferResource, constants.Value, 0, countControl.ImmediateOffsetBytes),
new FillWord(FillWordSource.BufferResource, constants.Value, 0, modulusControl.ImmediateOffsetBytes),
[.. Enumerable.Range(0, 4).Select(word => User(loadControl.ScalarResource + (uint)word))],
[.. Enumerable.Range(0, 4).Select(word => User(storeControl.ScalarResource + (uint)word))]);
}
private const uint Shift64 = 134;
private const uint ZeroConstant = 128;
private const uint NullScalar = 125;

View File

@ -41,9 +41,11 @@ internal interface IShaderPipelineHost
// The device supports shaderSharedInt64Atomics, so LDS 64-bit atomics can be
// emitted as real 64-bit atomics instead of a non-atomic 32-bit pair.
bool SharedInt64AtomicsEnabled { get; }
bool ShaderSignedZeroInfNanPreserveFloat32Supported => false;
bool ExecGuardElisionEnabled => true;
ShaderPrewarmList? ShaderPrewarm => null;
bool PerVertexPixelInputsSupported => true;
bool ClipDistanceEnabled => false;
RenderHostLimits Limits { get; }

View File

@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Globalization;
using System.IO.Hashing;
using System.Text;
using SharpEmu.HLE;
@ -141,6 +142,7 @@ internal sealed class ShaderPrewarmList : IDisposable
{
public const string FileName = "shader-prewarm.bin";
public const string StampFileName = "shader-prewarm.stamp";
public const string ProgressFileName = "shader-prewarm.progress";
private const uint Magic = 0x57504553;
private const uint FormatVersion = 1;
@ -151,16 +153,19 @@ internal sealed class ShaderPrewarmList : IDisposable
private readonly object _gate = new();
private readonly FileStream _file;
private readonly string _stampPath;
private readonly string _progressPath;
private readonly object _progressGate = new();
private readonly HashSet<(ulong Hash, uint CodeSize, ulong Address)> _codes = [];
private readonly HashSet<ulong> _computes = [];
private readonly Dictionary<(ulong Hash, uint CodeSize, ulong Address), ShaderCodeCapture> _loadedCodes = new();
private readonly List<ComputePrewarmRecord> _loadedComputes = [];
private bool _failed;
private ShaderPrewarmList(FileStream file, string stampPath)
private ShaderPrewarmList(FileStream file, string stampPath, string progressPath)
{
_file = file;
_stampPath = stampPath;
_progressPath = progressPath;
}
public int LoadedComputeCount => _loadedComputes.Count;
@ -172,7 +177,7 @@ internal sealed class ShaderPrewarmList : IDisposable
Directory.CreateDirectory(directory);
var path = Path.Combine(directory, FileName);
var file = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
var list = new ShaderPrewarmList(file, Path.Combine(directory, StampFileName));
var list = new ShaderPrewarmList(file, Path.Combine(directory, StampFileName), Path.Combine(directory, ProgressFileName));
try
{
list.Load();
@ -230,6 +235,93 @@ internal sealed class ShaderPrewarmList : IDisposable
}
}
public static ulong Identity(ComputePrewarmRecord record) => XxHash3.HashToUInt64(SerializeCompute(record));
public HashSet<ulong> ReadProgress(string stamp)
{
var completed = new HashSet<ulong>();
lock (_progressGate)
{
try
{
if (!File.Exists(_progressPath))
{
return completed;
}
var lines = File.ReadAllLines(_progressPath);
if (lines.Length == 0 || lines[0] != ProgressKey(stamp))
{
File.Delete(_progressPath);
return completed;
}
for (var index = 1; index < lines.Length; index++)
{
if (ulong.TryParse(lines[index], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var identity))
{
completed.Add(identity);
}
}
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
completed.Clear();
}
}
return completed;
}
public void AppendProgress(string stamp, IReadOnlyCollection<ulong> identities)
{
if (identities.Count == 0)
{
return;
}
lock (_progressGate)
{
try
{
var lines = new List<string>(identities.Count + 1);
if (!File.Exists(_progressPath))
{
lines.Add(ProgressKey(stamp));
}
foreach (var identity in identities)
{
lines.Add(identity.ToString("X16", CultureInfo.InvariantCulture));
}
File.AppendAllLines(_progressPath, lines);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm progress write failed: {exception.Message}");
}
}
}
public void ClearProgress()
{
lock (_progressGate)
{
try
{
File.Delete(_progressPath);
}
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException)
{
Console.Error.WriteLine($"[LOADER][WARN] Shader prewarm progress clear failed: {exception.Message}");
}
}
}
private static string ProgressKey(string stamp) =>
XxHash3.HashToUInt64(Encoding.UTF8.GetBytes(stamp)).ToString("X16", CultureInfo.InvariantCulture);
public void RecordCompute(ShaderCodeCapture code, ComputePrewarmRecord record)
{
var payload = SerializeCompute(record);

View File

@ -87,6 +87,7 @@ internal sealed class ProgramSourceEntry
public required bool HasBitwiseExclusiveOr { get; init; }
public ConstantFill? ConstantFill { get; init; }
public BoundedFill? BoundedFill { get; init; }
public BoundedCopy? BoundedCopy { get; init; }
public EmbeddedVertexFetchPlan? EmbeddedFetch { get; init; }
public ShaderVertexInput[] VertexInputs { get; init; } = [];
public List<ProgramPermutation> Permutations { get; } = new(8);
@ -383,6 +384,7 @@ internal sealed class ShaderProgramCache
HasBitwiseExclusiveOr = exclusiveOr,
ConstantFill = source.Stage == ShaderStage.Compute ? ConstantFillDetector.Detect(program) : null,
BoundedFill = source.Stage == ShaderStage.Compute ? BoundedFillDetector.Detect(program) : null,
BoundedCopy = source.Stage == ShaderStage.Compute ? BoundedFillDetector.DetectCopy(program) : null,
EmbeddedFetch = fetch,
VertexInputs = vertexInputs,
};
@ -591,6 +593,7 @@ internal sealed class ShaderProgramCache
{
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
var signedZeroInfNanPreserve = _host.ShaderSignedZeroInfNanPreserveFloat32Supported;
switch (source.Stage)
{
case ShaderStage.Vertex:
@ -603,9 +606,11 @@ internal sealed class ShaderProgramCache
ScratchDwords = info.ScratchDwords,
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
SupportsSharedInt64Atomics = sharedInt64Atomics,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
VertexInputs = entry.VertexInputs,
PositionExportControl = info.PositionExportControl,
SupportsClipDistance = _host.ClipDistanceEnabled,
ClipSpace = new ShaderClipSpaceTransform(
info.ClipSpace.Enabled,
info.ClipSpace.ScaleX,
@ -629,6 +634,7 @@ internal sealed class ShaderProgramCache
ScratchDwords = info.ScratchDwords,
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
SupportsSharedInt64Atomics = sharedInt64Atomics,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
PixelOutputs = options.PixelOutputs,
PixelInputEnable = options.PixelInputEnable,
PixelCustomInterpolationMask = info.CustomInterpolationMask,
@ -640,7 +646,7 @@ internal sealed class ShaderProgramCache
default:
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
sharedInt64Atomics, _host.ExecGuardElisionEnabled);
sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve);
}
}
@ -656,7 +662,8 @@ internal sealed class ShaderProgramCache
usesDispatchThreadLimits: usesDispatchThreadLimits);
private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout,
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision) =>
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision,
bool signedZeroInfNanPreserve) =>
new(plan, resources, layout)
{
WaveSize = info.WaveSize,
@ -664,6 +671,7 @@ internal sealed class ShaderProgramCache
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
ScratchDwords = info.ScratchDwords,
SupportsSharedInt64Atomics = sharedInt64Atomics,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
ComputeSystemRegisters = systemRegisters,
LocalDataShareDwords = info.LocalDataShareDwords,
LocalSizeX = Math.Max(info.ThreadsX, 1),
@ -672,7 +680,8 @@ internal sealed class ShaderProgramCache
};
internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler,
bool sharedInt64Atomics, bool execGuardElision, out IGuestCompiledShader? compiled, out BindingLayout? layout, out string error)
bool sharedInt64Atomics, bool execGuardElision, bool signedZeroInfNanPreserve, out IGuestCompiledShader? compiled,
out BindingLayout? layout, out string error)
{
compiled = null;
layout = null;
@ -689,7 +698,7 @@ internal sealed class ShaderProgramCache
layout = AllocateLayout(program, plan, resources, record.UserDataBase, record.UserDataCount, record.PushDataCursor,
record.Info.DispatchThreadDimensions);
var request = BuildComputeRequest(plan, resources, layout, record.Info, record.SystemRegisters,
sharedInt64Atomics, execGuardElision);
sharedInt64Atomics, execGuardElision, signedZeroInfNanPreserve);
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
}
catch (Exception exception)
@ -837,6 +846,7 @@ internal sealed class ShaderProgramCache
HasBitwiseExclusiveOr = entry.HasBitwiseExclusiveOr,
ConstantFill = entry.ConstantFill,
BoundedFill = entry.BoundedFill,
BoundedCopy = entry.BoundedCopy,
Buffers = buffers,
Images = images,
SamplerCount = info.Samplers.Count,

View File

@ -71,6 +71,7 @@ public static class VertexAttributeFormats
56 => (Format.R8G8B8A8Unorm, 4u),
50 => (Format.A2B10G10R10UnormPack32, 4u),
51 => (Format.A2B10G10R10SNormPack32, 4u),
36 => (Format.B10G11R11UfloatPack32, 3u),
29 => (Format.R16G16Sfloat, 2u),
28 => (Format.R16G16Sint, 2u),
27 => (Format.R16G16Uint, 2u),

View File

@ -188,5 +188,7 @@ public interface IRenderHost
bool TryFillDccMetadata(ulong address, ulong size, uint fillValue);
bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words);
Exception Fatal(string message);
}

View File

@ -101,6 +101,12 @@ public sealed partial class RenderExecutor
return;
}
if (indirectArgumentsAddress == 0 && TryConsumeBoundedCopy(input, groupsX, groupsY, groupsZ, dispatchInitiator))
{
_host.ResetBindings();
return;
}
if (indirectArgumentsAddress == 0 && TryConsumeImageClear(input, groupsX, groupsY, groupsZ, dispatchInitiator))
{
_host.ResetBindings();
@ -151,7 +157,7 @@ public sealed partial class RenderExecutor
Thread.Sleep(1);
}
var bindings = _host.PrepareBindings(input.Stage);
var bindings = PrepareBindings(input.Stage);
if (program.UsesDeviceAddresses)
{
_host.PrepareDeviceAddresses();
@ -552,6 +558,71 @@ public sealed partial class RenderExecutor
return consumed;
}
private bool TryConsumeBoundedCopy(ComputeInputInfo input, uint groupsX, uint groupsY, uint groupsZ, uint dispatchInitiator)
{
var program = input.Stage.Program!;
if (program.BoundedCopy is not { } copy || !BoundedCopyEnabled)
{
return false;
}
if (program.UserDataBase != 0 || copy.GroupScalarRegister != (uint)input.WorkgroupRegister || !input.GroupIdX || input.GroupIdY || input.GroupIdZ ||
input.ThreadIdCount < 1 || input.ThreadsX != ImageClearWaveSize || input.ThreadsY != 1 || input.ThreadsZ != 1 ||
groupsX == 0 || groupsY != 1 || groupsZ != 1)
{
return RefuseBoundedFill(program, $"copy shape dispatch={groupsX}x{groupsY}x{groupsZ} local={input.ThreadsX}x{input.ThreadsY}x{input.ThreadsZ}");
}
var userData = input.Stage.Resources.UserData;
Span<uint> sourceWords = stackalloc uint[4];
Span<uint> destinationWords = stackalloc uint[4];
for (var word = 0; word < 4; word++)
{
if (!TryResolveFillWord(copy.Source[word], userData, out sourceWords[word]) ||
!TryResolveFillWord(copy.Destination[word], userData, out destinationWords[word]))
{
return RefuseBoundedFill(program, $"copy descriptor word {word} unreadable");
}
}
if (!TryResolveFillWord(copy.Count, userData, out var count) || !TryResolveFillWord(copy.Modulus, userData, out var modulus))
{
return RefuseBoundedFill(program, "copy range unreadable");
}
var source = BufferDescriptorWords.From(sourceWords);
var destination = BufferDescriptorWords.From(destinationWords);
if (!IsPlainWordBuffer(source) || !IsPlainWordBuffer(destination) || source.Format != destination.Format ||
modulus == 0 || modulus > source.RecordCount)
{
return RefuseBoundedFill(program,
$"copy src=0x{source.Address:X}/{source.Format}/{source.Stride}/{source.OutOfBounds} dst=0x{destination.Address:X}/{destination.Format}/{destination.Stride}/{destination.OutOfBounds} " +
$"modulus={modulus} records={source.RecordCount}");
}
var threads = (dispatchInitiator & DispatchInitiatorUseThreadDimensions) != 0 ? groupsX : (ulong)groupsX * input.ThreadsX;
var words = Math.Min(Math.Min((ulong)count, threads), destination.RecordCount);
if (words == 0)
{
return false;
}
var consumed = _host.TryCopyWordsOnHost(destination.Address, source.Address, modulus, words);
if (RenderTrace.Enabled)
{
RenderTrace.Write(
$"Bounded copy: shader=0x{program.Hash:X16} src=0x{source.Address:X16} dst=0x{destination.Address:X16} words={words} modulus={modulus} consumed={consumed}");
}
return consumed || RefuseBoundedFill(program, $"copy refused by the host dst=0x{destination.Address:X} words={words} modulus={modulus}");
}
private static readonly bool BoundedCopyEnabled = Environment.GetEnvironmentVariable("SHARPEMU_HOST_BOUNDED_COPY") != "0";
private static bool IsPlainWordBuffer(BufferDescriptorWords descriptor) =>
descriptor.Stride == sizeof(uint) && !descriptor.SwizzleEnabled && !descriptor.AddThreadId && descriptor.OutOfBounds == 0 &&
descriptor.Type == 0 && (descriptor.Address & 3) == 0 && descriptor.Format is Format32UInt or Format32SInt or Format32Float;
private static bool RefuseBoundedFill(ShaderProgramInfo program, string reason)
{
Diagnostics.DccWriterTrace.Refuse(program.Hash, reason);

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.GpuCommands.Registers;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.Libs.VideoOut;
using Silk.NET.Vulkan;
@ -170,6 +171,30 @@ public sealed partial class RenderExecutor
private void SetDrawDebugPhase(ulong submitId, in DrawCall draw, uint phase) =>
_host.SetDebugInformation(draw.Operation, submitId, phase, draw.Count, 0, draw.InstanceCount, draw.FirstInstance);
private IPreparedBindings PrepareBindings(in ShaderStageResources stage)
{
var bindings = _host.PrepareBindings(stage);
NoteSampledTextures(stage);
return bindings;
}
private static void NoteSampledTextures(in ShaderStageResources stage)
{
if (stage.Program is not { } program)
{
return;
}
var descriptors = stage.Resources.Images;
for (var index = 0; index < program.Images.Length && index < descriptors.Length; index++)
{
if (program.Images[index].Class == ImageResourceClass.Sampled)
{
MipStatistics.Shared.NoteSampled(new TextureDescriptorWords(descriptors[index]));
}
}
}
// Binds everything the draw needs inside one preparation scope, then records it.
private void RecordDraw(
ulong submitId,
@ -192,8 +217,8 @@ public sealed partial class RenderExecutor
IPreparedBindings? pixelBindings;
try
{
vertexBindings = _host.PrepareBindings(vertexInput.Stage);
pixelBindings = state.PixelActive ? _host.PrepareBindings(pixelInput.Stage) : null;
vertexBindings = PrepareBindings(vertexInput.Stage);
pixelBindings = state.PixelActive ? PrepareBindings(pixelInput.Stage) : null;
}
catch (DrawImageTypeMismatchException rejection)
{

View File

@ -58,6 +58,7 @@ public class ShaderProgramInfo
public bool HasBitwiseExclusiveOr { get; init; }
public Pipelines.ConstantFill? ConstantFill { get; init; }
public Pipelines.BoundedFill? BoundedFill { get; init; }
public Pipelines.BoundedCopy? BoundedCopy { get; init; }
public BufferResourceInfo[] Buffers { get; init; } = [];
public ImageResourceInfo[] Images { get; init; } = [];
public int SamplerCount { get; init; }

View File

@ -14,16 +14,60 @@ public sealed class GpuWorkerRelay : IGpuQueueRelay
private readonly object _gate = new();
private readonly Queue<Action> _commands = new();
private readonly Action _wake;
private readonly Func<Action, Action, bool>? _tryEnqueueAfterPendingWork;
private int _pendingCount;
private bool _accepting = true;
public GpuWorkerRelay(Action wake)
public GpuWorkerRelay(Action wake, Func<Action, Action, bool>? tryEnqueueAfterPendingWork = null)
{
_wake = wake;
_tryEnqueueAfterPendingWork = tryEnqueueAfterPendingWork;
}
public bool TryRunAfterPendingWork(Action work) => TryRunOnGpuQueue(work);
public bool TryRunAfterAcceptedCommandStreams(Action work)
{
ArgumentNullException.ThrowIfNull(work);
if (IsGpuQueueThread || _tryEnqueueAfterPendingWork is null)
{
return TryRunOnGpuQueue(work);
}
using var done = new SemaphoreSlim(0);
System.Runtime.ExceptionServices.ExceptionDispatchInfo? failure = null;
if (!_tryEnqueueAfterPendingWork(
() =>
{
try
{
work();
}
catch (Exception exception)
{
failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(exception);
}
finally
{
done.Release();
}
},
() =>
{
failure = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(
new OperationCanceledException(
"The command stream stopped before the ordered GPU worker action could run."));
done.Release();
}))
{
return false;
}
done.Wait();
failure?.Throw();
return true;
}
public bool IsGpuQueueThread => _boundWorker == this;
public bool HasPendingCommands => Volatile.Read(ref _pendingCount) != 0;

View File

@ -66,6 +66,9 @@ internal sealed class VulkanGuestGpuBackend : IGuestGpuBackend
public IdleOutcome SubmitDone(ICpuMemory memory) =>
VulkanVideoPresenter.SubmitDone(memory);
public void RunAfterPendingCommandStreams(Action work) =>
VulkanVideoPresenter.RunAfterPendingCommandStreams(work);
public void RegisterKnownDisplayBuffer(ulong address, uint guestFormat) =>
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);

View File

@ -141,6 +141,25 @@ public static class JsonExports
InitializerSetGlobalNullAccessCallback(ctx);
#pragma warning restore SHEM004
[SysAbiExport(
Nid = "i1393UBWu1U",
ExportName = "_ZN3sce4Json11Initializer28setGlobalSpecialFloatHandlerEPFKNS0_5ValueENS0_12FunctionTypeEdPS3_PvPbES6_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitializerSetGlobalSpecialFloatHandler(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
JsonObjectHeap.GlobalSpecialFloatHandler = ctx[CpuRegister.Rsi];
JsonObjectHeap.GlobalSpecialFloatHandlerContext = ctx[CpuRegister.Rdx];
TraceJson("Initializer.setGlobalSpecialFloatHandler", thisAddress, ctx[CpuRegister.Rsi]);
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "WSOuge5IsCg",
ExportName = "_ZN3sce4Json14InitParameter2C1Ev",
@ -168,6 +187,14 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GvGvswb0v34",
ExportName = "_ZN3sce4Json14InitParameter2C2Ev",
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2BaseConstructor(CpuContext ctx) =>
InitParameter2Constructor(ctx);
[SysAbiExport(
Nid = "I2QC8PYhJWY",
ExportName = "_ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv",
@ -193,6 +220,14 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "W72B9ylU2JA",
ExportName = "_ZN3sce4Json18InitParameterRtti216setAllocatorRttiEPNS0_14AllocParamRttiEPv",
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameterRtti2SetAllocatorRtti(CpuContext ctx) =>
InitParameter2SetAllocator(ctx);
[SysAbiExport(
Nid = "Eu95jmqn5Rw",
ExportName = "_ZN3sce4Json14InitParameter217setFileBufferSizeEm",
@ -210,6 +245,30 @@ public static class JsonExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WVZBP4IyM+E",
ExportName = "_ZN3sce4Json14InitParameter225setSpecialFloatFormatTypeENS0_22SpecialFloatFormatTypeE",
Target = Generation.Gen5,
LibraryName = "libSceJson")]
public static int InitParameter2SetSpecialFloatFormatType(CpuContext ctx)
{
var thisAddress = ctx[CpuRegister.Rdi];
if (thisAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> field = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(field, unchecked((uint)ctx[CpuRegister.Rsi]));
if (!ctx.Memory.TryWrite(thisAddress + 0x18, field))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
ctx[CpuRegister.Rax] = thisAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "IXW-z8pggfg",
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E",

View File

@ -87,6 +87,10 @@ internal static class JsonObjectHeap
public static ulong GlobalNullAccessCallbackContext;
public static ulong GlobalSpecialFloatHandler;
public static ulong GlobalSpecialFloatHandlerContext;
public static void SetValue(ulong address, JsonValueState state) => Values[address] = state;
public static void RemoveValue(ulong address) => Values.TryRemove(address, out _);
@ -106,5 +110,7 @@ internal static class JsonObjectHeap
Strings.Clear();
GlobalNullAccessCallback = 0;
GlobalNullAccessCallbackContext = 0;
GlobalSpecialFloatHandler = 0;
GlobalSpecialFloatHandlerContext = 0;
}
}

View File

@ -161,20 +161,43 @@ public static class KernelEventQueueCompatExports
}
public bool Remove(ulong ident, short filter)
=> RemoveAll(ident, filter) != 0;
public int RemoveAll(ulong ident, short filter)
{
var index = FindIndex(ident, filter);
if (index < 0)
var originalCount = Count;
var writeIndex = 0;
for (var readIndex = 0; readIndex < originalCount; readIndex++)
{
return false;
var candidate = this[readIndex];
if (candidate.Ident == ident && candidate.Filter == filter)
{
continue;
}
this[writeIndex++] = candidate;
}
for (var i = index; i + 1 < Count; i++)
Count = writeIndex;
return originalCount - writeIndex;
}
public int UpdateUserData(ulong ident, short filter, ulong userData)
{
var updatedCount = 0;
for (var index = 0; index < Count; index++)
{
this[i] = this[i + 1];
var candidate = this[index];
if (candidate.Ident != ident || candidate.Filter != filter)
{
continue;
}
this[index] = candidate with { UserData = userData };
updatedCount++;
}
Count--;
return true;
return updatedCount;
}
}
@ -824,52 +847,55 @@ public static class KernelEventQueueCompatExports
}
var waiterId = Interlocked.Increment(ref _nextEventQueueWaiterId);
if (timeoutAddress == 0)
{
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEqueue",
state.WakeKey,
new EqueueWaiter
{
Ctx = ctx,
State = state,
EventsAddress = eventsAddress,
EventCapacity = eventCapacity,
OutCountAddress = outCountAddress,
WaiterId = waiterId,
});
if (requestedBlock)
var blockDeadlineTimestamp = timeoutAddress == 0
? 0
: GuestThreadExecution.ComputeDeadlineTimestamp(
TimeSpan.FromMicroseconds(timeoutUsec));
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEqueue",
state.WakeKey,
new EqueueWaiter
{
var wakeAfterRegistration = false;
lock (_eventQueueGate)
{
wakeAfterRegistration =
!IsLiveEventQueueLocked(state) ||
HasPendingEventsLocked(state.Handle);
}
if (wakeAfterRegistration)
{
WakeEventQueue(
state,
_logEqueue
? "source=post-registration-state-check"
: null);
}
if (_logEqueue)
{
TraceEventQueue(
ctx,
"wait-block",
handle,
$"generation={state.Generation} waiter={waiterId} " +
$"capacity={eventCapacity} timeout=infinite " +
$"events=0x{eventsAddress:X16} out_count=0x{outCountAddress:X16}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
Ctx = ctx,
State = state,
EventsAddress = eventsAddress,
EventCapacity = eventCapacity,
OutCountAddress = outCountAddress,
WaiterId = waiterId,
},
blockDeadlineTimestamp);
if (requestedBlock)
{
var wakeAfterRegistration = false;
lock (_eventQueueGate)
{
wakeAfterRegistration =
!IsLiveEventQueueLocked(state) ||
HasPendingEventsLocked(state.Handle);
}
if (wakeAfterRegistration)
{
WakeEventQueue(
state,
_logEqueue
? "source=post-registration-state-check"
: null);
}
if (_logEqueue)
{
TraceEventQueue(
ctx,
"wait-block",
handle,
$"generation={state.Generation} waiter={waiterId} " +
$"capacity={eventCapacity} timeout=" +
(timeoutAddress == 0 ? "infinite " : $"{timeoutUsec}_usec ") +
$"events=0x{eventsAddress:X16} out_count=0x{outCountAddress:X16}");
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (timeoutAddress != 0)
@ -1141,9 +1167,10 @@ public static class KernelEventQueueCompatExports
}
var key = (ident, filter);
var isExistingRegistration = events.ContainsKey(key);
var isNewReadRegistration =
filter == KernelEventFilterRead &&
!events.ContainsKey(key);
!isExistingRegistration;
events[(ident, filter)] = new KernelEventRegistration(
ident,
filter,
@ -1152,6 +1179,11 @@ public static class KernelEventQueueCompatExports
lowWater,
unchecked((ulong)Interlocked.Increment(
ref _nextEventRegistrationGeneration)));
if (isExistingRegistration &&
_pendingEvents.TryGetValue(handle, out var pending))
{
_ = pending.UpdateUserData(ident, filter, userData);
}
if (isNewReadRegistration)
{
_readEventRegistrationCount++;
@ -1296,7 +1328,7 @@ public static class KernelEventQueueCompatExports
if (_pendingEvents.TryGetValue(handle, out var pending))
{
_ = pending.Remove(ident, filter);
_ = pending.RemoveAll(ident, filter);
}
return true;
@ -1387,22 +1419,13 @@ public static class KernelEventQueueCompatExports
_pendingEvents[handle] = queue;
}
// Keep each interrupt as a separate event. Limit growth when the queue is not read.
var queuedEvent = new KernelQueuedEvent(
queue.AddLast(new KernelQueuedEvent(
registration.Ident,
registration.Filter,
registration.Flags,
1,
data,
registration.UserData);
if (CountPendingEvents(queue, registration.Ident, registration.Filter) < 256)
{
queue.AddLast(queuedEvent);
}
else
{
QueueOrUpdateEvent(queue, queuedEvent);
}
registration.UserData));
(wakeQueues ??= []).Add(state);
triggeredCount++;
@ -1630,24 +1653,6 @@ public static class KernelEventQueueCompatExports
}
}
private static int CountPendingEvents(
KernelEventDeque queue,
ulong ident,
short filter)
{
var count = 0;
for (var i = 0; i < queue.Count; i++)
{
var pending = queue[i];
if (pending.Ident == ident && pending.Filter == filter)
{
count++;
}
}
return count;
}
private static void PollReadEvents(object? state)
{
_ = state;

View File

@ -399,7 +399,7 @@ public static class KernelExports
ExportName = "sceKernelOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenCore(ctx);
[SysAbiExport(
Nid = "mqQMh1zPPT8",

View File

@ -274,7 +274,7 @@ public static partial class KernelMemoryCompatExports
try
{
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, FileShare.ReadWrite);
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Write, GuestFileShare);
stream.SetLength(length);
}
catch (FileNotFoundException)

View File

@ -14,12 +14,14 @@ using System.Threading;
using System.Runtime.InteropServices;
using System.Linq;
using System.Globalization;
using System.Security.Cryptography;
namespace SharpEmu.Libs.Kernel;
public static partial class KernelMemoryCompatExports
{
private const int MaxGuestStringLength = 4096;
internal const FileShare GuestFileShare = FileShare.ReadWrite | FileShare.Delete;
private const int WideCharSize = sizeof(ushort);
private const int MemsetChunkSize = 16 * 1024;
private static readonly byte[] _zeroChunk = new byte[MemsetChunkSize];
@ -96,6 +98,7 @@ public static partial class KernelMemoryCompatExports
private static readonly object _fdGate = new();
private static readonly Dictionary<int, FileStream> _openFiles = new();
private static readonly HashSet<int> _randomDeviceDescriptors = new();
private static readonly Dictionary<int, HostMovieBridge.BinkGuestCompletionShim>
_binkGuestCompletionShims = new();
private static readonly Dictionary<int, string> _observedBinkGuestFiles = new();
@ -1439,7 +1442,9 @@ public static partial class KernelMemoryCompatExports
ExportName = "_open",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelOpenUnderscore(CpuContext ctx)
public static int KernelOpenUnderscore(CpuContext ctx) => PosixOpen(ctx);
internal static int KernelOpenCore(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
@ -1450,6 +1455,19 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (IsRandomDevicePath(guestPath) && (flags & O_DIRECTORY) == 0)
{
var randomFd = (int)Interlocked.Increment(ref _nextFileDescriptor);
lock (_fdGate)
{
_randomDeviceDescriptors.Add(randomFd);
}
LogOpenTrace($"_open random-device path='{guestPath}' flags=0x{flags:X8} fd={randomFd}");
ctx[CpuRegister.Rax] = unchecked((ulong)randomFd);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var hostPath = ResolveGuestPath(guestPath);
var access = ResolveOpenAccess(flags);
var mode = ResolveOpenMode(flags, access);
@ -1519,7 +1537,7 @@ public static partial class KernelMemoryCompatExports
}
EnsureOpenParentDirectoryExists(guestPath, hostPath, flags);
var stream = new FileStream(hostPath, mode, access, FileShare.ReadWrite);
var stream = new FileStream(hostPath, mode, access, GuestFileShare);
if ((flags & O_APPEND) != 0)
{
stream.Seek(0, SeekOrigin.End);
@ -1677,11 +1695,11 @@ public static partial class KernelMemoryCompatExports
}
// POSIX open(2): translates a failed raw open into -1/errno. On success
// KernelOpenUnderscore already writes the fd into RAX (the import bridge
// KernelOpenCore already writes the fd into RAX (the import bridge
// prefers a written RAX over the return value), so returning 0 is correct.
public static int PosixOpen(CpuContext ctx)
{
var result = KernelOpenUnderscore(ctx);
var result = KernelOpenCore(ctx);
return result == (int)OrbisGen2Result.ORBIS_GEN2_OK
? 0
: PosixFailure(ctx, result);
@ -1734,7 +1752,8 @@ public static partial class KernelMemoryCompatExports
{
// Stop at the first miss and report its index.
// The caller can then use its normal file-open fallback.
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
Console.Error.WriteLine(
$"[LOADER][WARN] sceKernelAprResolveFilepathsToIdsAndFileSizes: '{guestPath}' -> '{hostPath}' not found (index {i} of {count})");
if (sizesAddress != 0 &&
!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), 0))
{
@ -2254,7 +2273,12 @@ public static partial class KernelMemoryCompatExports
string? observedBinkPath = null;
lock (_fdGate)
{
if (_openFiles.Remove(fd, out stream))
if (_randomDeviceDescriptors.Remove(fd))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
else if (_openFiles.Remove(fd, out stream))
{
_binkGuestCompletionShims.Remove(fd);
if (_observedBinkGuestFiles.Remove(fd, out observedBinkPath))
@ -2307,6 +2331,25 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
bool isRandomDevice;
lock (_fdGate)
{
isRandomDevice = _randomDeviceDescriptors.Contains(fd);
}
if (isRandomDevice)
{
var randomBytes = GC.AllocateUninitializedArray<byte>(requested);
RandomNumberGenerator.Fill(randomBytes);
if (!ctx.Memory.TryWrite(bufferAddress, randomBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = unchecked((ulong)requested);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (KernelSocketCompatExports.TryReadSocketFd(
ctx,
fd,
@ -2842,15 +2885,23 @@ public static partial class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var searchStart = searchStartRaw < 0 ? 0UL : (ulong)searchStartRaw;
var searchEnd = searchEndRaw <= 0
? GuestMemoryLayout.DirectBytes
: Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
if (searchStart >= searchEnd)
if (!ctx.TryWriteUInt64(outAddress, 0) || !ctx.TryWriteUInt64(outSize, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (searchStartRaw < 0 || searchEndRaw < 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var searchStart = (ulong)searchStartRaw;
var searchEnd = Math.Min((ulong)searchEndRaw, GuestMemoryLayout.DirectBytes);
if (searchStart >= searchEnd)
{
return MemoryNoSpace;
}
bool foundSpan;
ulong candidate;
ulong rangeAvailable;
@ -2866,7 +2917,7 @@ public static partial class KernelMemoryCompatExports
if (!foundSpan)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return MemoryNoSpace;
}
if (!ctx.TryWriteUInt64(outAddress, candidate) || !ctx.TryWriteUInt64(outSize, rangeAvailable))
@ -6901,9 +6952,14 @@ public static partial class KernelMemoryCompatExports
string? hostPath = null;
bool isDirectory = false;
bool isRandomDevice = false;
lock (_fdGate)
{
if (_openDirectories.TryGetValue(fd, out var directory))
if (_randomDeviceDescriptors.Contains(fd))
{
isRandomDevice = true;
}
else if (_openDirectories.TryGetValue(fd, out var directory))
{
hostPath = directory.Path;
isDirectory = true;
@ -6914,6 +6970,14 @@ public static partial class KernelMemoryCompatExports
}
}
if (isRandomDevice)
{
var now = DateTime.UtcNow;
LogIoTrace("fstat", "/dev/urandom", $"fd={fd} size=0 device=random");
return TryWriteKernelStat(ctx, statAddress, isDirectory: false, size: 0,
now, now, now, "/dev/urandom");
}
if (!string.IsNullOrWhiteSpace(hostPath))
{
long size = 0;
@ -6935,6 +6999,10 @@ public static partial class KernelMemoryCompatExports
return !string.IsNullOrWhiteSpace(hostPath) && TryWriteHostPathStat(ctx, statAddress, hostPath!, isDirectory);
}
private static bool IsRandomDevicePath(string path) =>
string.Equals(path, "/dev/random", StringComparison.Ordinal) ||
string.Equals(path, "/dev/urandom", StringComparison.Ordinal);
private static bool TryWriteHostPathStat(CpuContext ctx, ulong statAddress, string hostPath)
{
var isDirectory = Directory.Exists(hostPath);

View File

@ -548,23 +548,38 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong stackAddress = 0;
ulong stackSize = 0;
var hasStackBounds = false;
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is not null &&
scheduler.TryGetGuestThreadStackBounds(thread, out stackAddress, out stackSize) &&
IsValidStackBounds(stackAddress, stackSize))
{
hasStackBounds = true;
}
else
{
stackAddress = 0;
stackSize = 0;
if (thread == KernelPthreadState.GetCurrentThreadHandle() &&
TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out stackAddress))
{
stackSize = NativeGuestStackSize;
hasStackBounds = true;
}
}
lock (_stateGate)
{
var threadState = GetOrCreateThreadStateLocked(thread);
// The native executor maps guest pthread stacks itself, after the
// kernel-facing thread object has been created. Report that live
// mapping when a thread asks for its own attributes. IL2CPP's
// conservative collector uses these two fields to register the stack;
// returning the default null address lets it recycle objects that are
// still reachable only from guest registers/stack frames.
if (thread == KernelPthreadState.GetCurrentThreadHandle() &&
TryInferNativeGuestStack(ctx[CpuRegister.Rsp], out var stackAddress))
if (hasStackBounds)
{
threadState.Attributes = threadState.Attributes with
{
StackAddress = stackAddress,
StackSize = NativeGuestStackSize,
StackSize = stackSize,
};
}
_attrStates[outAttrAddress] = threadState.Attributes;
@ -574,6 +589,11 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool IsValidStackBounds(ulong stackAddress, ulong stackSize) =>
stackAddress != 0 &&
stackSize != 0 &&
stackAddress <= ulong.MaxValue - stackSize;
private static bool TryInferNativeGuestStack(ulong stackPointer, out ulong stackAddress)
{
stackAddress = 0;

View File

@ -80,6 +80,13 @@ public static class KernelRuntimeCompatExports
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate ulong RdtscDelegate();
[SysAbiExport(
Nid = "uvT2iYBBnkY",
ExportName = "sceKernelSync",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelSync(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "1jfXLRVzisc",
ExportName = "sceKernelUsleep",

View File

@ -73,7 +73,7 @@ public static class KernelSemaphoreCompatExports
Count = initialCount,
};
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
if (!ctx.TryWriteUInt64(semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@ -113,6 +113,27 @@ public static class KernelSemaphoreCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return WaitSemaphoreCore(
ctx,
semaphore,
handle,
needCount,
hasTimeout: timeoutAddress != 0,
timeoutAddress,
timeoutUsec,
"sceKernelWaitSema");
}
private static int WaitSemaphoreCore(
CpuContext ctx,
KernelSemaphoreState semaphore,
uint handle,
int needCount,
bool hasTimeout,
ulong timeoutAddress,
uint timeoutUsec,
string operationName)
{
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
@ -125,7 +146,7 @@ public static class KernelSemaphoreCompatExports
if (_traceSema)
{
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)}");
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@ -137,7 +158,7 @@ public static class KernelSemaphoreCompatExports
// tokens (so a wake commits the acquisition), while the resume
// handler distinguishes a real acquisition from a deadline expiry.
var acquired = false;
var deadline = timeoutAddress != 0
var deadline = hasTimeout
? GuestThreadExecution.ComputeDeadlineTimestamp(TimeSpan.FromMicroseconds(timeoutUsec))
: 0;
@ -187,7 +208,7 @@ public static class KernelSemaphoreCompatExports
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitSema",
operationName,
GetSemaphoreWakeKey(handle),
ResumeWait,
WakePredicate,
@ -216,14 +237,21 @@ public static class KernelSemaphoreCompatExports
if (_traceSema)
{
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")} waiters={semaphore.WaitingThreads} {FormatCallSite(ctx)}");
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
// Not a guest thread (or no scheduler): fall back to a host-thread
// wait so the semantics still hold on non-cooperative callers.
return WaitSemaphoreOnHostThread(ctx, semaphore, handle, needCount, timeoutAddress, timeoutUsec);
return WaitSemaphoreOnHostThread(
ctx,
semaphore,
handle,
needCount,
hasTimeout,
timeoutAddress,
timeoutUsec);
}
private static int WaitSemaphoreOnHostThread(
@ -231,46 +259,62 @@ public static class KernelSemaphoreCompatExports
KernelSemaphoreState semaphore,
uint handle,
int needCount,
bool hasTimeout,
ulong timeoutAddress,
uint timeoutUsec)
{
var deadlineMs = timeoutAddress != 0
var deadlineMs = hasTimeout
? Environment.TickCount64 + Math.Max(1L, timeoutUsec / 1000L)
: long.MaxValue;
lock (semaphore.Gate)
if (_traceSema)
{
if (_traceSema)
lock (semaphore.Gate)
{
TraceSemaphore(
$"wait-host-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} " +
$"count={semaphore.Count} timeout={(timeoutAddress == 0 ? "infinite" : timeoutUsec)} {FormatCallSite(ctx)}");
$"count={semaphore.Count} timeout={(hasTimeout ? timeoutUsec : "infinite")} {FormatCallSite(ctx)}");
}
while (semaphore.Count < needCount)
}
while (true)
{
lock (semaphore.Gate)
{
if (semaphore.Count >= needCount)
{
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore(
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
}
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
var remaining = deadlineMs - Environment.TickCount64;
if (timeoutAddress != 0 && remaining <= 0)
if (hasTimeout && remaining <= 0)
{
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
Monitor.Wait(semaphore.Gate, (int)Math.Min(remaining, 100));
var waitMilliseconds = hasTimeout
? (int)Math.Max(1L, Math.Min(remaining, 10L))
: 10;
Monitor.Wait(semaphore.Gate, waitMilliseconds);
}
semaphore.Count -= needCount;
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
if (_traceSema)
{
TraceSemaphore(
$"wait-host-wake handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} {FormatCallSite(ctx)}");
}
if (timeoutAddress != 0)
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
}
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
GuestThreadExecution.Scheduler?.DeliverPendingGuestExceptionIfReady(ctx);
}
}
@ -328,7 +372,7 @@ public static class KernelSemaphoreCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (signalCount <= 0)
if (signalCount < 0)
{
signalTrace.Record(SemaphoreSignalProfile.Stage.Rejected,
result: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
@ -532,6 +576,35 @@ public static class KernelSemaphoreCompatExports
: result;
}
[SysAbiExport(
Nid = "fjN6NQHhK8k",
ExportName = "scePthreadSemTimedwait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadSemTimedWait(CpuContext ctx)
{
var timeoutUsec = unchecked((uint)ctx[CpuRegister.Rsi]);
if (!TryGetPosixSemaphoreHandle(ctx, ctx[CpuRegister.Rdi], out var handle))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
return WaitSemaphoreCore(
ctx,
semaphore,
handle,
needCount: 1,
hasTimeout: true,
timeoutAddress: 0,
timeoutUsec,
"scePthreadSemTimedwait");
}
[SysAbiExport(
Nid = "w5IHyvahg-o",
ExportName = "sem_timedwait",

View File

@ -10,23 +10,44 @@ namespace SharpEmu.Libs.Kernel;
// Wake selects a fixed number of registered waiters at the same address.
public static class KernelSyncOnAddressCompatExports
{
private const int UmtxOperationWait = 2;
private const int UmtxOperationWake = 3;
private const int PosixEperm = 1;
private const int PosixEacces = 13;
private const int PosixEfault = 14;
private const int PosixEinval = 22;
private const int PosixEagain = 35;
private const int PosixEtimedout = 60;
private const int PosixEcanceled = 85;
private const int PosixEother = 1062;
private sealed class SyncWaiter : IGuestThreadBlockWaiter
{
private readonly CpuContext _context;
private readonly ulong _expected;
private readonly bool _is64Bit;
private readonly bool _usePosixResult;
private readonly long _deadlineTimestamp;
private readonly object _hostGate = new();
private readonly long _registeredTicks;
private readonly int _registeredManagedThread;
private int _wakeRequested;
private int _resumeRecorded;
public SyncWaiter(CpuContext context, ulong address, ulong expected, bool is64Bit)
public SyncWaiter(
CpuContext context,
ulong address,
ulong expected,
bool is64Bit,
bool usePosixResult,
long deadlineTimestamp)
{
_context = context;
Address = address;
_expected = expected;
_is64Bit = is64Bit;
_usePosixResult = usePosixResult;
_deadlineTimestamp = deadlineTimestamp;
_registeredTicks = KernelSyncOnAddressProfile.Enabled
? Stopwatch.GetTimestamp()
: 0L;
@ -50,13 +71,13 @@ public static class KernelSyncOnAddressCompatExports
if (Volatile.Read(ref _wakeRequested) != 0)
{
RecordResume(explicitWake: true, valueChanged: false, timedOut: false, faulted: false);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
return Complete((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
if (!TryReadValue(_context, Address, _is64Bit, out var value))
{
RecordResume(explicitWake: false, valueChanged: false, timedOut: false, faulted: true);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return Complete((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var valueChanged = value != _expected;
@ -65,15 +86,21 @@ public static class KernelSyncOnAddressCompatExports
valueChanged,
timedOut: !valueChanged,
faulted: false);
return valueChanged
var result = valueChanged
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
return Complete(result);
}
public bool TryWake() => Volatile.Read(ref _wakeRequested) != 0;
public bool RequestWake()
{
if (_deadlineTimestamp != 0 && Stopwatch.GetTimestamp() >= _deadlineTimestamp)
{
return false;
}
if (Interlocked.Exchange(ref _wakeRequested, 1) != 0)
{
return false;
@ -87,42 +114,50 @@ public static class KernelSyncOnAddressCompatExports
return true;
}
public int WaitOnHost(TimeSpan? timeout)
public int WaitOnHost()
{
if (Volatile.Read(ref _wakeRequested) == 0)
while (Volatile.Read(ref _wakeRequested) == 0)
{
lock (_hostGate)
if (!TryReadValue(_context, Address, _is64Bit, out var current) ||
current != _expected)
{
if (timeout is null)
{
while (Volatile.Read(ref _wakeRequested) == 0)
{
Monitor.Wait(_hostGate);
}
}
else
{
var deadline = Stopwatch.GetTimestamp() +
(long)Math.Ceiling(timeout.Value.TotalSeconds * Stopwatch.Frequency);
while (Volatile.Read(ref _wakeRequested) == 0)
{
var remainingTicks = deadline - Stopwatch.GetTimestamp();
if (remainingTicks <= 0)
{
break;
}
break;
}
var remaining = TimeSpan.FromSeconds(
remainingTicks / (double)Stopwatch.Frequency);
Monitor.Wait(_hostGate, remaining);
}
var waitSlice = TimeSpan.FromMilliseconds(10);
if (_deadlineTimestamp != 0)
{
var remainingTicks = _deadlineTimestamp - Stopwatch.GetTimestamp();
if (remainingTicks <= 0)
{
break;
}
var remainingSeconds = remainingTicks / (double)Stopwatch.Frequency;
if (remainingSeconds < waitSlice.TotalSeconds)
{
waitSlice = TimeSpan.FromSeconds(remainingSeconds);
}
}
lock (_hostGate)
{
if (Volatile.Read(ref _wakeRequested) == 0)
{
Monitor.Wait(_hostGate, waitSlice);
}
}
GuestThreadExecution.Scheduler?.DeliverPendingGuestExceptionIfReady(_context);
}
return Resume();
}
private int Complete(int result) => _usePosixResult
? SetPosixReturn(_context, result)
: result;
private void RecordResume(
bool explicitWake,
bool valueChanged,
@ -171,6 +206,80 @@ public static class KernelSyncOnAddressCompatExports
LibraryName = "libKernel")]
public static int SyncOnAddressWait64(CpuContext ctx) => Wait(ctx, is64Bit: true);
[SysAbiExport(
Nid = "04AjkP0jO9U",
ExportName = "_umtx_op",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int UmtxOp(CpuContext ctx)
{
var address = ctx[CpuRegister.Rdi];
var operation = unchecked((int)ctx[CpuRegister.Rsi]);
var value = ctx[CpuRegister.Rdx];
var secondaryAddress = ctx[CpuRegister.Rcx];
var timeoutAddress = ctx[CpuRegister.R8];
if (secondaryAddress != 0)
{
return SetPosixReturn(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
switch (operation)
{
case UmtxOperationWait:
{
TimeSpan? timeout = null;
long deadline = 0;
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt64(timeoutAddress, out var secondsBits) ||
!ctx.TryReadUInt64(timeoutAddress + sizeof(long), out var nanosecondsBits))
{
return SetPosixReturn(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var seconds = unchecked((long)secondsBits);
var nanoseconds = unchecked((long)nanosecondsBits);
if (seconds < 0 || nanoseconds < 0 || nanoseconds >= 1_000_000_000L)
{
return SetPosixReturn(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
timeout = CreateTimespecDuration(seconds, nanoseconds);
deadline = GuestThreadExecution.ComputeDeadlineTimestamp(timeout.Value);
}
return WaitCore(
ctx,
is64Bit: true,
address,
value,
timeout,
deadline,
usePosixResult: true,
reason: "_umtx_op.wait");
}
case UmtxOperationWake:
return Wake(
ctx,
address,
unchecked((int)value),
usePosixResult: true);
default:
return SetPosixReturn(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
}
[SysAbiExport(
Nid = "q2y-wDIVWZA",
ExportName = "sceKernelSyncOnAddressWake",
@ -180,14 +289,29 @@ public static class KernelSyncOnAddressCompatExports
{
var address = ctx[CpuRegister.Rdi];
var requested = unchecked((int)ctx[CpuRegister.Rsi]);
return Wake(ctx, address, requested, usePosixResult: false);
}
private static int Wake(
CpuContext ctx,
ulong address,
int requested,
bool usePosixResult)
{
if (address == 0 || (address & 3) != 0 || requested < 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
usePosixResult);
}
if (requested == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_OK,
usePosixResult);
}
List<SyncWaiter>? selected = null;
@ -259,7 +383,10 @@ public static class KernelSyncOnAddressCompatExports
GuestThreadExecution.CurrentGuestThreadHandle,
Environment.CurrentManagedThreadId);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_OK,
usePosixResult);
}
private static void CaptureCallStack(
@ -304,15 +431,15 @@ public static class KernelSyncOnAddressCompatExports
private static int Wait(CpuContext ctx, bool is64Bit)
{
var address = ctx[CpuRegister.Rdi];
var expected = is64Bit
? ctx[CpuRegister.Rsi]
: unchecked((uint)ctx[CpuRegister.Rsi]);
var alignmentMask = is64Bit ? 7UL : 3UL;
if (address == 0 || (address & alignmentMask) != 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var expected = is64Bit
? ctx[CpuRegister.Rsi]
: unchecked((uint)ctx[CpuRegister.Rsi]);
if (!TryReadValue(ctx, address, is64Bit, out var current))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@ -334,28 +461,92 @@ public static class KernelSyncOnAddressCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (timeoutMicroseconds == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
timeout = TimeSpan.FromTicks((long)timeoutMicroseconds * 10L);
deadline = GuestThreadExecution.ComputeDeadlineTimestamp(timeout.Value);
}
var waiter = new SyncWaiter(ctx, address, expected, is64Bit);
return WaitCore(
ctx,
is64Bit,
address,
expected,
timeout,
deadline,
usePosixResult: false,
reason: is64Bit
? "sceKernelSyncOnAddressWait64"
: "sceKernelSyncOnAddressWait32");
}
private static int WaitCore(
CpuContext ctx,
bool is64Bit,
ulong address,
ulong expected,
TimeSpan? timeout,
long deadline,
bool usePosixResult,
string reason)
{
var alignmentMask = is64Bit ? 7UL : 3UL;
if (address == 0 || (address & alignmentMask) != 0)
{
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
usePosixResult);
}
if (!TryReadValue(ctx, address, is64Bit, out var current))
{
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
usePosixResult);
}
if (current != expected)
{
KernelSyncOnAddressProfile.RecordImmediate(address);
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_OK,
usePosixResult);
}
if (timeout == TimeSpan.Zero)
{
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT,
usePosixResult);
}
var waiter = new SyncWaiter(
ctx,
address,
expected,
is64Bit,
usePosixResult,
deadline);
Register(address, waiter);
if (!TryReadValue(ctx, address, is64Bit, out current))
{
Unregister(waiter);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT,
usePosixResult);
}
if (current != expected || waiter.TryWake())
{
Unregister(waiter);
KernelSyncOnAddressProfile.RecordImmediate(address);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
return CompleteResult(
ctx,
(int)OrbisGen2Result.ORBIS_GEN2_OK,
usePosixResult);
}
var returnRip = 0UL;
@ -383,7 +574,7 @@ public static class KernelSyncOnAddressCompatExports
if (GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
is64Bit ? "sceKernelSyncOnAddressWait64" : "sceKernelSyncOnAddressWait32",
reason,
waiter.WakeKey,
waiter,
deadline))
@ -393,7 +584,24 @@ public static class KernelSyncOnAddressCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
return SetReturn(ctx, waiter.WaitOnHost(timeout));
return SetReturn(ctx, waiter.WaitOnHost());
}
private static TimeSpan CreateTimespecDuration(long seconds, long nanoseconds)
{
var subsecondTicks = (nanoseconds + 99L) / 100L;
if (seconds > TimeSpan.MaxValue.Ticks / TimeSpan.TicksPerSecond)
{
return TimeSpan.MaxValue;
}
var secondTicks = seconds * TimeSpan.TicksPerSecond;
if (secondTicks > TimeSpan.MaxValue.Ticks - subsecondTicks)
{
return TimeSpan.MaxValue;
}
return TimeSpan.FromTicks(secondTicks + subsecondTicks);
}
private static void Register(ulong address, SyncWaiter waiter)
@ -458,4 +666,31 @@ public static class KernelSyncOnAddressCompatExports
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
return result;
}
private static int CompleteResult(CpuContext ctx, int result, bool usePosixResult) =>
usePosixResult
? SetPosixReturn(ctx, result)
: SetReturn(ctx, result);
private static int SetPosixReturn(CpuContext ctx, int result)
{
if (result == (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return SetReturn(ctx, 0);
}
var errno = result switch
{
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED => PosixEperm,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED => PosixEacces,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT => PosixEfault,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT => PosixEinval,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN => PosixEagain,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT => PosixEtimedout,
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED => PosixEcanceled,
_ => PosixEother,
};
_ = KernelRuntimeCompatExports.TrySetErrno(ctx, errno);
return SetReturn(ctx, -1);
}
}

View File

@ -0,0 +1,31 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
/// <summary>
/// Process-argument accessors exported by libKernel for the platform libc.
/// </summary>
public static class LibcProcessArgumentExports
{
[SysAbiExport(
Nid = "iKJMWrAumPE",
ExportName = "getargc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int GetArgc(CpuContext ctx) =>
ctx.SetReturn(HleDataSymbols.ProcessArgumentCount);
[SysAbiExport(
Nid = "FJmglmTMdr4",
ExportName = "getargv",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int GetArgv(CpuContext ctx)
{
ctx[CpuRegister.Rax] = HleDataSymbols.ProcessArgumentVectorAddress;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}

View File

@ -69,6 +69,20 @@ public static class LibcStdioExports
private static readonly object _ctypeTableGate = new();
private static nint _ctypeTableBase;
[SysAbiExport(
Nid = "vZkmJmvqueY",
ExportName = "_Lockfilelock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int LockFileLock(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "0x7rx8TKy2Y",
ExportName = "_Unlockfilelock",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libc")]
public static int UnlockFileLock(CpuContext ctx) => ctx.SetReturn(0);
[SysAbiExport(
Nid = "xeYO4u7uyJ0",
ExportName = "fopen",
@ -133,7 +147,7 @@ public static class LibcStdioExports
}
}
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
var stream = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
{
stream.Seek(0, SeekOrigin.End);
@ -1049,7 +1063,7 @@ public static class LibcStdioExports
}
}
var replacement = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
var replacement = new FileStream(hostPath, fileMode, fileAccess, KernelMemoryCompatExports.GuestFileShare);
lock (file.Gate)
{
if (file.IsClosed)

View File

@ -12,29 +12,82 @@ public static class Http2Exports
private const int Http2ErrorInvalidVersion = unchecked((int)0x817B106A);
private const int Http2ErrorInvalidId = unchecked((int)0x817B1100);
private const int Http2ErrorInvalidValue = unchecked((int)0x817B11FE);
private const int Http2ErrorNullPointer = unchecked((int)0x817B1225);
private const int Http2ErrorProhibited = unchecked((int)0x817B5224);
private static readonly ConcurrentDictionary<int, Http2Context> Contexts = new();
private static readonly ConcurrentDictionary<int, Http2Template> Templates = new();
private static readonly ConcurrentDictionary<int, Http2Request> Requests = new();
private static readonly ConcurrentDictionary<int, Http2CookieBox> CookieBoxes = new();
private static int _nextContextId;
private static int _nextTemplateId = 0x1000;
private static int _nextRequestId = 0x2000;
private static int _nextCookieBoxId = 0x3000;
private sealed record Http2Context(int NetId, int SslId, ulong PoolSize, int MaxRequests);
private sealed record Http2Template(int ContextId, string UserAgent, int HttpVersion, bool AutoProxyConfig);
private sealed record Http2CookieBox;
private sealed record Http2Request(int TemplateId, string Method, string Url, ulong ContentLength);
private sealed record Http2Options(
uint MinimumSslVersion,
uint SslOptions,
ulong RedirectCallback,
ulong RedirectUserArgument,
bool AutoRedirect,
int CookieBoxId,
uint ConnectTimeoutMicroseconds,
uint ReceiveTimeoutMicroseconds,
uint SendTimeoutMicroseconds,
bool AuthEnabled,
ulong SslCallback,
ulong SslUserArgument)
{
public static readonly Http2Options Default = new(0, 0, 0, 0, true, 0, 0, 0, 0, false, 0, 0);
}
private sealed record Http2Header(string Name, string Value, uint Mode);
private sealed record Http2Template(
int ContextId,
string UserAgent,
int HttpVersion,
bool AutoProxyConfig,
Http2Options Options);
private sealed record Http2Request(
int TemplateId,
string Method,
string Url,
ulong ContentLength,
Http2Options Options,
Http2Header[] Headers);
internal readonly record struct Http2OptionsSnapshot(
uint MinimumSslVersion,
uint SslOptions,
ulong RedirectCallback,
ulong RedirectUserArgument,
bool AutoRedirect,
int CookieBoxId,
uint ConnectTimeoutMicroseconds,
uint ReceiveTimeoutMicroseconds,
uint SendTimeoutMicroseconds,
bool AuthEnabled,
ulong SslCallback,
ulong SslUserArgument);
internal readonly record struct Http2HeaderSnapshot(string Name, string Value, uint Mode);
public static void ResetRuntimeState()
{
Contexts.Clear();
Templates.Clear();
Requests.Clear();
CookieBoxes.Clear();
_nextContextId = 0;
_nextTemplateId = 0x1000;
_nextRequestId = 0x2000;
_nextCookieBoxId = 0x3000;
}
[SysAbiExport(
@ -119,7 +172,12 @@ public static class Http2Exports
}
var id = Interlocked.Increment(ref _nextTemplateId);
Templates[id] = new Http2Template(contextId, userAgent, httpVersion, autoProxyConfig != 0);
Templates[id] = new Http2Template(
contextId,
userAgent,
httpVersion,
autoProxyConfig != 0,
Http2Options.Default);
TraceHttp2("create_template", id, unchecked((ulong)contextId), ctx[CpuRegister.Rsi], unchecked((ulong)httpVersion), unchecked((ulong)autoProxyConfig));
return ctx.SetReturn(id);
}
@ -140,6 +198,175 @@ public static class Http2Exports
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "09tk+kIA1Ns",
ExportName = "sceHttp2SetMinSslVersion",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetMinSslVersion(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var minimumSslVersion = unchecked((uint)ctx[CpuRegister.Rsi]);
return TryUpdateOptions(id, options => options with { MinimumSslVersion = minimumSslVersion })
? TraceAndReturn(ctx, "set_min_ssl_version", id, minimumSslVersion, 0, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
Nid = "B37SruheQ5Y",
ExportName = "sceHttp2SslDisableOption",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SslDisableOption(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var flags = unchecked((uint)ctx[CpuRegister.Rsi]);
return TryUpdateOptions(id, options => options with { SslOptions = options.SslOptions & ~flags })
? TraceAndReturn(ctx, "ssl_disable_option", id, flags, 0, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
Nid = "EWcwMpbr5F8",
ExportName = "sceHttp2SslEnableOption",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SslEnableOption(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var flags = unchecked((uint)ctx[CpuRegister.Rsi]);
return TryUpdateOptions(id, options => options with { SslOptions = options.SslOptions | flags })
? TraceAndReturn(ctx, "ssl_enable_option", id, flags, 0, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
Nid = "BJgi0CH7al4",
ExportName = "sceHttp2SetRedirectCallback",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetRedirectCallback(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var callback = ctx[CpuRegister.Rsi];
var userArgument = ctx[CpuRegister.Rdx];
return TryUpdateOptions(
id,
options => options with
{
RedirectCallback = callback,
RedirectUserArgument = userArgument,
})
? TraceAndReturn(ctx, "set_redirect_callback", id, callback, userArgument, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
Nid = "b9AvoIaOuHI",
ExportName = "sceHttp2SetAutoRedirect",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetAutoRedirect(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var enabled = unchecked((int)ctx[CpuRegister.Rsi]) != 0;
return TryUpdateOptions(id, options => options with { AutoRedirect = enabled })
? TraceAndReturn(ctx, "set_auto_redirect", id, enabled ? 1UL : 0UL, 0, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
Nid = "N4UfjvWJsMw",
ExportName = "sceHttp2CreateCookieBox",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2CreateCookieBox(CpuContext ctx)
{
var id = Interlocked.Increment(ref _nextCookieBoxId);
CookieBoxes[id] = new Http2CookieBox();
TraceHttp2("create_cookie_box", id, 0, 0, 0, 0);
return ctx.SetReturn(id);
}
[SysAbiExport(
Nid = "jrVHsKCXA0g",
ExportName = "sceHttp2SetCookieBox",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetCookieBox(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var cookieBoxId = unchecked((int)ctx[CpuRegister.Rsi]);
if (cookieBoxId != 0 && !CookieBoxes.ContainsKey(cookieBoxId))
{
return ctx.SetReturn(Http2ErrorInvalidId);
}
return TryUpdateOptions(id, options => options with { CookieBoxId = cookieBoxId })
? TraceAndReturn(ctx, "set_cookie_box", id, unchecked((ulong)cookieBoxId), 0, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
Nid = "-HIO4VT87v8",
ExportName = "sceHttp2SetConnectTimeOut",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetConnectTimeOut(CpuContext ctx) =>
SetTimeout(ctx, static (options, value) => options with { ConnectTimeoutMicroseconds = value }, "connect");
[SysAbiExport(
Nid = "izvHhqgDt44",
ExportName = "sceHttp2SetRecvTimeOut",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetRecvTimeOut(CpuContext ctx) =>
SetTimeout(ctx, static (options, value) => options with { ReceiveTimeoutMicroseconds = value }, "recv");
[SysAbiExport(
Nid = "XPtW45xiLHk",
ExportName = "sceHttp2SetSendTimeOut",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetSendTimeOut(CpuContext ctx) =>
SetTimeout(ctx, static (options, value) => options with { SendTimeoutMicroseconds = value }, "send");
[SysAbiExport(
Nid = "jjFahkBPCYs",
ExportName = "sceHttp2SetAuthEnabled",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetAuthEnabled(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var enabled = unchecked((int)ctx[CpuRegister.Rsi]) != 0;
return TryUpdateOptions(id, options => options with { AuthEnabled = enabled })
? TraceAndReturn(ctx, "set_auth_enabled", id, enabled ? 1UL : 0UL, 0, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
Nid = "YrWX+DhPHQY",
ExportName = "sceHttp2SetSslCallback",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetSslCallback(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var callback = ctx[CpuRegister.Rsi];
var userArgument = ctx[CpuRegister.Rdx];
return TryUpdateOptions(
id,
options => options with
{
SslCallback = callback,
SslUserArgument = userArgument,
})
? TraceAndReturn(ctx, "set_ssl_callback", id, callback, userArgument, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
ExportName = "sceHttp2CreateRequestWithURL",
Target = Generation.Gen5,
@ -147,7 +374,7 @@ public static class Http2Exports
public static int Http2CreateRequestWithUrl(CpuContext ctx)
{
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Templates.ContainsKey(templateId))
if (!Templates.TryGetValue(templateId, out var template))
{
return ctx.SetReturn(Http2ErrorInvalidId);
}
@ -159,11 +386,73 @@ public static class Http2Exports
}
var id = Interlocked.Increment(ref _nextRequestId);
Requests[id] = new Http2Request(templateId, method, url, ctx[CpuRegister.Rcx]);
Requests[id] = new Http2Request(
templateId,
method,
url,
ctx[CpuRegister.Rcx],
template.Options,
[]);
TraceHttp2("create_request", id, unchecked((ulong)templateId), ctx[CpuRegister.Rsi], ctx[CpuRegister.Rdx], ctx[CpuRegister.Rcx]);
return ctx.SetReturn(id);
}
[SysAbiExport(
Nid = "nrPfOE8TQu0",
ExportName = "sceHttp2AddRequestHeader",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2AddRequestHeader(CpuContext ctx)
{
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
var nameAddress = ctx[CpuRegister.Rsi];
var valueAddress = ctx[CpuRegister.Rdx];
var mode = unchecked((uint)ctx[CpuRegister.Rcx]);
if (nameAddress == 0 || valueAddress == 0)
{
return ctx.SetReturn(Http2ErrorNullPointer);
}
if (!TryReadUtf8Z(ctx, nameAddress, 4096, out var name) ||
!TryReadUtf8Z(ctx, valueAddress, 16384, out var value))
{
return ctx.SetReturn(Http2ErrorInvalidValue);
}
while (Requests.TryGetValue(requestId, out var request))
{
var headers = new Http2Header[request.Headers.Length + 1];
Array.Copy(request.Headers, headers, request.Headers.Length);
headers[^1] = new Http2Header(name, value, mode);
if (Requests.TryUpdate(requestId, request with { Headers = headers }, request))
{
return TraceAndReturn(ctx, "add_request_header", requestId, nameAddress, valueAddress, mode, 0);
}
}
return ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
Nid = "FSAFOzi0FpM",
ExportName = "sceHttp2SetRequestContentLength",
Target = Generation.Gen5,
LibraryName = "libSceHttp2")]
public static int Http2SetRequestContentLength(CpuContext ctx)
{
var requestId = unchecked((int)ctx[CpuRegister.Rdi]);
var contentLength = ctx[CpuRegister.Rsi];
while (Requests.TryGetValue(requestId, out var request))
{
if (Requests.TryUpdate(requestId, request with { ContentLength = contentLength }, request))
{
return TraceAndReturn(ctx, "set_request_content_length", requestId, contentLength, 0, 0, 0);
}
}
return ctx.SetReturn(Http2ErrorInvalidId);
}
[SysAbiExport(
ExportName = "sceHttp2DeleteRequest",
Target = Generation.Gen5,
@ -202,6 +491,100 @@ public static class Http2Exports
}
}
private static int SetTimeout(
CpuContext ctx,
Func<Http2Options, uint, Http2Options> update,
string timeoutName)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var microseconds = unchecked((uint)ctx[CpuRegister.Rsi]);
return TryUpdateOptions(id, options => update(options, microseconds))
? TraceAndReturn(ctx, $"set_{timeoutName}_timeout", id, microseconds, 0, 0, 0)
: ctx.SetReturn(Http2ErrorInvalidId);
}
private static bool TryUpdateOptions(int id, Func<Http2Options, Http2Options> update)
{
while (Templates.TryGetValue(id, out var template))
{
if (Templates.TryUpdate(id, template with { Options = update(template.Options) }, template))
{
return true;
}
}
while (Requests.TryGetValue(id, out var request))
{
if (Requests.TryUpdate(id, request with { Options = update(request.Options) }, request))
{
return true;
}
}
return false;
}
internal static bool TryGetOptionsForTests(int id, out Http2OptionsSnapshot snapshot)
{
var options = Templates.TryGetValue(id, out var template)
? template.Options
: Requests.TryGetValue(id, out var request)
? request.Options
: null;
if (options is null)
{
snapshot = default;
return false;
}
snapshot = new Http2OptionsSnapshot(
options.MinimumSslVersion,
options.SslOptions,
options.RedirectCallback,
options.RedirectUserArgument,
options.AutoRedirect,
options.CookieBoxId,
options.ConnectTimeoutMicroseconds,
options.ReceiveTimeoutMicroseconds,
options.SendTimeoutMicroseconds,
options.AuthEnabled,
options.SslCallback,
options.SslUserArgument);
return true;
}
internal static Http2HeaderSnapshot[] GetRequestHeadersForTests(int requestId) =>
Requests.TryGetValue(requestId, out var request)
? request.Headers
.Select(static header => new Http2HeaderSnapshot(header.Name, header.Value, header.Mode))
.ToArray()
: [];
internal static bool TryGetRequestContentLengthForTests(int requestId, out ulong contentLength)
{
if (Requests.TryGetValue(requestId, out var request))
{
contentLength = request.ContentLength;
return true;
}
contentLength = 0;
return false;
}
private static int TraceAndReturn(
CpuContext ctx,
string operation,
int id,
ulong arg0,
ulong arg1,
ulong arg2,
ulong arg3)
{
TraceHttp2(operation, id, arg0, arg1, arg2, arg3);
return ctx.SetReturn(0);
}
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;

View File

@ -14,28 +14,42 @@ namespace SharpEmu.Libs.Network;
public static class NetExports
{
private const int NetErrorNoEntry = unchecked((int)0x80410102);
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
private const int NetErrorFault = unchecked((int)0x8041010E);
private const int NetErrorAlreadyExists = unchecked((int)0x80410111);
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
private const int NetErrorTooManyFiles = unchecked((int)0x80410118);
private const int NetErrorWouldBlock = unchecked((int)0x80410123);
private const int NetErrorAddressInUse = unchecked((int)0x80410130);
private const int NetErrorTooManyOpenFiles = unchecked((int)0x80410118);
private const int NetErrorResolverNoDns = unchecked((int)0x804101E1);
private const int NetErrorNotInitialized = unchecked((int)0x804101C8);
private const int NetErrnoNoEntry = 2;
private const int NetErrnoBadFileDescriptor = 9;
private const int NetErrnoFault = 14;
private const int NetErrnoAlreadyExists = 17;
private const int NetErrnoInvalidArgument = 22;
private const int NetErrnoTooManyFiles = 24;
private const int NetErrnoWouldBlock = 35;
private const int NetErrnoAddressInUse = 48;
private const int NetErrnoTooManyOpenFiles = 24;
private const int NetErrnoNotInitialized = 200;
private const int NetErrnoResolverNoDns = 225;
private const int MaxNameLength = 256;
private const int EpollIdBase = 1024;
private const int EpollCapacity = 32;
private const int NetEpollEventSize = 24;
private const uint NetEpollIn = 0x00000001;
private const uint NetEpollOut = 0x00000002;
private const uint NetEpollError = 0x00000008;
private static ReadOnlySpan<byte> OfflineMacAddress => [0x02, 0x53, 0x48, 0x41, 0x52, 0x50];
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
private static readonly ConcurrentDictionary<int, Socket> _sockets = new();
private static readonly object EpollGate = new();
private static readonly Dictionary<int, NetEpollContext> _epolls = new();
private static int _nextPoolId;
private static int _nextResolverId = 0x2000;
private static int _nextSocketId = 255;
@ -52,6 +66,26 @@ public static class NetExports
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
private sealed class NetEpollContext(string name)
{
public string Name { get; } = name;
public List<NetEpollRegistration> Registrations { get; } = [];
}
private readonly record struct NetEpollRegistration(int SocketId, NetEpollEvent Event);
private readonly record struct NetEpollEvent(uint Events, ulong Data);
internal static void ResetEpollsForTests()
{
lock (EpollGate)
{
_epolls.Clear();
Monitor.PulseAll(EpollGate);
}
}
internal static bool TryGetReadEventState(
int socketId,
ulong lowWater,
@ -116,6 +150,11 @@ public static class NetExports
_initialized = false;
_pools.Clear();
_resolvers.Clear();
lock (EpollGate)
{
_epolls.Clear();
Monitor.PulseAll(EpollGate);
}
foreach (var socket in _sockets.Values)
{
socket.Dispose();
@ -195,6 +234,12 @@ public static class NetExports
public static int NetSocketClose(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (KernelSocketCompatExports.TryCloseSocketFd(id))
{
TraceNet("socket.close.posix", id, 0, 0, 0);
return ctx.SetReturn(0);
}
if (!_sockets.TryRemove(id, out var socket))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
@ -715,6 +760,291 @@ public static class NetExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "SF47kB2MNTo",
ExportName = "sceNetEpollCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEpollCreate(CpuContext ctx)
{
if (!_initialized)
{
return SetNetError(ctx, NetErrorNotInitialized, NetErrnoNotInitialized);
}
var nameAddress = ctx[CpuRegister.Rdi];
var flags = unchecked((int)ctx[CpuRegister.Rsi]);
if (nameAddress == 0 || flags != 0 ||
!TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var name))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
lock (EpollGate)
{
for (var index = 0; index < EpollCapacity; index++)
{
var id = EpollIdBase + index;
if (_epolls.ContainsKey(id))
{
continue;
}
_epolls.Add(id, new NetEpollContext(name));
TraceNet("epoll.create", id, nameAddress, 0, 0);
return ctx.SetReturn(id);
}
}
return SetNetError(ctx, NetErrorTooManyOpenFiles, NetErrnoTooManyOpenFiles);
}
[SysAbiExport(
Nid = "ZVw46bsasAk",
ExportName = "sceNetEpollControl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEpollControl(CpuContext ctx)
{
const int EpollControlAdd = 1;
const int EpollControlModify = 2;
const int EpollControlDelete = 3;
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
var operation = unchecked((int)ctx[CpuRegister.Rsi]);
var socketId = unchecked((int)ctx[CpuRegister.Rdx]);
var eventAddress = ctx[CpuRegister.Rcx];
if (operation is < EpollControlAdd or > EpollControlDelete ||
(operation is EpollControlAdd or EpollControlModify && eventAddress == 0) ||
(operation == EpollControlDelete && eventAddress != 0))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
if (!_sockets.ContainsKey(socketId))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
NetEpollEvent requestedEvent = default;
if (eventAddress != 0 && !TryReadNetEpollEvent(ctx, eventAddress, out requestedEvent))
{
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
}
lock (EpollGate)
{
if (!_epolls.TryGetValue(epollId, out var epoll))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
var registrationIndex = epoll.Registrations.FindIndex(value => value.SocketId == socketId);
switch (operation)
{
case EpollControlAdd when registrationIndex >= 0:
return SetNetError(ctx, NetErrorAlreadyExists, NetErrnoAlreadyExists);
case EpollControlAdd:
epoll.Registrations.Add(new NetEpollRegistration(socketId, requestedEvent));
break;
case EpollControlModify when registrationIndex < 0:
return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry);
case EpollControlModify:
epoll.Registrations[registrationIndex] =
new NetEpollRegistration(socketId, requestedEvent);
break;
case EpollControlDelete when registrationIndex < 0:
return SetNetError(ctx, NetErrorNoEntry, NetErrnoNoEntry);
case EpollControlDelete:
epoll.Registrations.RemoveAt(registrationIndex);
break;
}
Monitor.PulseAll(EpollGate);
}
TraceNet(
"epoll.control",
epollId,
unchecked((uint)operation),
unchecked((uint)socketId),
requestedEvent.Events);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "drjIbDbA7UQ",
ExportName = "sceNetEpollWait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEpollWait(CpuContext ctx)
{
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
var eventsAddress = ctx[CpuRegister.Rsi];
var maxEvents = unchecked((int)ctx[CpuRegister.Rdx]);
var timeoutMicroseconds = unchecked((int)ctx[CpuRegister.Rcx]);
if (eventsAddress == 0)
{
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
}
if (maxEvents <= 0 || timeoutMicroseconds < -1)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
NetEpollRegistration[] registrations;
lock (EpollGate)
{
if (!_epolls.TryGetValue(epollId, out var epoll))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
if (epoll.Registrations.Count == 0 && timeoutMicroseconds != 0)
{
if (timeoutMicroseconds < 0)
{
while (_epolls.TryGetValue(epollId, out epoll) && epoll.Registrations.Count == 0)
{
Monitor.Wait(EpollGate);
}
}
else
{
Monitor.Wait(
EpollGate,
TimeSpan.FromTicks((long)timeoutMicroseconds * TimeSpan.TicksPerMicrosecond));
}
if (!_epolls.TryGetValue(epollId, out epoll))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
}
registrations = epoll.Registrations.ToArray();
}
if (registrations.Length == 0)
{
TraceNet("epoll.wait", epollId, unchecked((uint)maxEvents), unchecked((uint)timeoutMicroseconds), 0);
return ctx.SetReturn(0);
}
var readSockets = new List<Socket>();
var writeSockets = new List<Socket>();
var errorSockets = new List<Socket>();
var hostRegistrations = new Dictionary<Socket, NetEpollRegistration>();
foreach (var registration in registrations)
{
if (!_sockets.TryGetValue(registration.SocketId, out var socket))
{
continue;
}
hostRegistrations[socket] = registration;
if ((registration.Event.Events & NetEpollIn) != 0)
{
readSockets.Add(socket);
}
if ((registration.Event.Events & NetEpollOut) != 0)
{
writeSockets.Add(socket);
}
errorSockets.Add(socket);
}
if (hostRegistrations.Count == 0)
{
return ctx.SetReturn(0);
}
try
{
Socket.Select(readSockets, writeSockets, errorSockets, timeoutMicroseconds);
}
catch (ObjectDisposedException)
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
catch (SocketException)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
var readyCount = 0;
foreach (var (socket, registration) in hostRegistrations)
{
uint readyEvents = 0;
if (readSockets.Contains(socket))
{
readyEvents |= NetEpollIn;
}
if (writeSockets.Contains(socket))
{
readyEvents |= NetEpollOut;
}
if (errorSockets.Contains(socket))
{
readyEvents |= NetEpollError;
}
if (readyEvents == 0)
{
continue;
}
var outputAddress = eventsAddress + (ulong)(readyCount * NetEpollEventSize);
if (!TryWriteNetEpollEvent(
ctx,
outputAddress,
readyEvents,
registration.SocketId,
registration.Event.Data))
{
return SetNetError(ctx, NetErrorFault, NetErrnoFault);
}
readyCount++;
if (readyCount == maxEvents)
{
break;
}
}
TraceNet(
"epoll.wait",
epollId,
unchecked((uint)maxEvents),
unchecked((uint)timeoutMicroseconds),
unchecked((uint)readyCount));
return ctx.SetReturn(readyCount);
}
[SysAbiExport(
Nid = "Inp1lfL+Jdw",
ExportName = "sceNetEpollDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEpollDestroy(CpuContext ctx)
{
var epollId = unchecked((int)ctx[CpuRegister.Rdi]);
lock (EpollGate)
{
if (!_epolls.Remove(epollId))
{
return SetNetError(ctx, NetErrorBadFileDescriptor, NetErrnoBadFileDescriptor);
}
Monitor.PulseAll(EpollGate);
}
TraceNet("epoll.destroy", epollId, 0, 0, 0);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "dgJBaeJnGpo",
ExportName = "sceNetPoolCreate",
@ -816,6 +1146,53 @@ public static class NetExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "v6M4txecCuo",
ExportName = "sceNetEtherNtostr",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetEtherNtostr(CpuContext ctx)
{
const int EtherStringLength = 18;
const string HexDigits = "0123456789abcdef";
var address = ctx[CpuRegister.Rdi];
var destination = ctx[CpuRegister.Rsi];
var destinationLength = ctx[CpuRegister.Rdx];
if (address == 0 || destination == 0 || destinationLength < EtherStringLength)
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
Span<byte> macAddress = stackalloc byte[6];
if (!ctx.Memory.TryRead(address, macAddress))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
Span<byte> text = stackalloc byte[EtherStringLength];
for (var index = 0; index < macAddress.Length; index++)
{
var value = macAddress[index];
var offset = index * 3;
text[offset] = unchecked((byte)HexDigits[value >> 4]);
text[offset + 1] = unchecked((byte)HexDigits[value & 0xF]);
if (index != macAddress.Length - 1)
{
text[offset + 2] = (byte)':';
}
}
text[^1] = 0;
if (!ctx.Memory.TryWrite(destination, text))
{
return SetNetError(ctx, NetErrorInvalidArgument, NetErrnoInvalidArgument);
}
TraceNet("ether_ntostr", 0, address, destination, destinationLength);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "C4UgDHHPvdw",
ExportName = "sceNetResolverCreate",
@ -1025,6 +1402,39 @@ public static class NetExports
return true;
}
private static bool TryReadNetEpollEvent(
CpuContext ctx,
ulong address,
out NetEpollEvent epollEvent)
{
Span<byte> bytes = stackalloc byte[NetEpollEventSize];
if (!ctx.Memory.TryRead(address, bytes))
{
epollEvent = default;
return false;
}
epollEvent = new NetEpollEvent(
BinaryPrimitives.ReadUInt32LittleEndian(bytes),
BinaryPrimitives.ReadUInt64LittleEndian(bytes[16..]));
return true;
}
private static bool TryWriteNetEpollEvent(
CpuContext ctx,
ulong address,
uint events,
int socketId,
ulong data)
{
Span<byte> bytes = stackalloc byte[NetEpollEventSize];
bytes.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(bytes, events);
BinaryPrimitives.WriteUInt64LittleEndian(bytes[8..], unchecked((uint)socketId));
BinaryPrimitives.WriteUInt64LittleEndian(bytes[16..], data);
return ctx.Memory.TryWrite(address, bytes);
}
[SysAbiExport(
Nid = "8Kcp5d-q1Uo",
ExportName = "sceNetInetPton",

View File

@ -0,0 +1,121 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Network;
/// <summary>
/// Minimal reliable-UDP poll object support. SharpEmu does not expose a host
/// RUDP transport yet, but callers still require a distinct non-zero poll ID
/// for their offline/event-processing path.
/// </summary>
public static class RudpExports
{
private const int MaxLivePolls = 4096;
private const ulong PollEventSize = 8;
private const int RudpErrorInvalidArgument = unchecked((int)0x80770004);
private const int RudpErrorInvalidPollId = unchecked((int)0x8077001F);
private const int RudpErrorOutOfMemory = unchecked((int)0x80770003);
private static readonly object PollGate = new();
private static readonly Dictionary<int, uint> PollCapacities = [];
private static int _nextPollId;
[SysAbiExport(
Nid = "MVbmLASjn5M",
ExportName = "sceRudpPollCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRudp")]
public static int RudpPollCreate(CpuContext ctx)
{
var requestedEvents = ctx[CpuRegister.Rdi];
if (requestedEvents is 0 or > uint.MaxValue)
{
return ctx.SetReturn(RudpErrorInvalidArgument);
}
lock (PollGate)
{
if (PollCapacities.Count >= MaxLivePolls)
{
return ctx.SetReturn(RudpErrorOutOfMemory);
}
int pollId;
do
{
_nextPollId = _nextPollId == int.MaxValue ? 1 : _nextPollId + 1;
pollId = _nextPollId;
}
while (PollCapacities.ContainsKey(pollId));
PollCapacities.Add(pollId, unchecked((uint)requestedEvents));
return ctx.SetReturn(pollId);
}
}
[SysAbiExport(
Nid = "M6ggviwXpLs",
ExportName = "sceRudpPollWait",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRudp")]
public static int RudpPollWait(CpuContext ctx)
{
var pollId = unchecked((int)ctx[CpuRegister.Rdi]);
var events = ctx[CpuRegister.Rsi];
var maxEvents = ctx[CpuRegister.Rdx];
var timeoutMicroseconds = ctx[CpuRegister.Rcx];
if (!TryGetPollCapacity(pollId, out _))
{
return ctx.SetReturn(RudpErrorInvalidPollId);
}
if (events == 0 ||
maxEvents == 0 ||
maxEvents > uint.MaxValue ||
maxEvents > ulong.MaxValue / PollEventSize ||
!ctx.Memory.CanRead(events, maxEvents * PollEventSize))
{
return ctx.SetReturn(RudpErrorInvalidArgument);
}
ctx[CpuRegister.Rax] = 0;
if (timeoutMicroseconds == 0 ||
!GuestThreadExecution.IsGuestThread ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out _))
{
return 0;
}
var timeout = timeoutMicroseconds > (ulong)(TimeSpan.MaxValue.Ticks / 10)
? TimeSpan.MaxValue
: TimeSpan.FromTicks(checked((long)timeoutMicroseconds * 10));
_ = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceRudpPollWait",
$"rudp-poll:{pollId}",
waiter: null,
blockDeadlineTimestamp: GuestThreadExecution.ComputeDeadlineTimestamp(timeout));
return 0;
}
public static void ResetRuntimeState()
{
lock (PollGate)
{
PollCapacities.Clear();
_nextPollId = 0;
}
}
internal static bool TryGetPollCapacity(int pollId, out uint capacity)
{
lock (PollGate)
{
return PollCapacities.TryGetValue(pollId, out capacity);
}
}
internal static void ResetForTests() => ResetRuntimeState();
}

View File

@ -13,15 +13,22 @@ public static class NpAuthExports
private const int NpAuthErrorAborted = unchecked((int)0x80550304);
private const int NpAuthErrorRequestMaximum = unchecked((int)0x80550305);
private const int NpAuthErrorRequestNotFound = unchecked((int)0x80550306);
private const int NpAuthErrorServiceDown = unchecked((int)0x80550401);
private const int NpAuthErrorInvalidArgument = unchecked((int)0x80550301);
private const int NpErrorSignedOut = unchecked((int)0x80550006);
private const int AuthorizationCodeSize = 136;
private const int IdTokenSize = 4104;
private const ulong AsyncParameterSize = 24;
private static readonly ConcurrentDictionary<int, AuthRequest> Requests = new();
private static readonly object RequestGate = new();
private static int _nextRequestId;
private sealed class AuthRequest
private sealed class AuthRequest(bool async)
{
public bool Async { get; } = async;
public bool Completed { get; set; }
public bool Aborted { get; set; }
public int Result { get; set; }
@ -47,7 +54,7 @@ public static class NpAuthExports
ExportName = "sceNpAuthCreateRequest",
Target = Generation.Gen5,
LibraryName = "libSceNpAuth")]
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx);
public static int NpAuthCreateRequest(CpuContext ctx) => CreateRequest(ctx, async: false);
[SysAbiExport(
ExportName = "sceNpAuthCreateAsyncRequest",
@ -63,7 +70,7 @@ public static class NpAuthExports
return ctx.SetReturn(NpAuthErrorInvalidSize);
}
return CreateRequest(ctx);
return CreateRequest(ctx, async: true);
}
[SysAbiExport(
@ -134,21 +141,23 @@ public static class NpAuthExports
ExportName = "sceNpAuthGetAuthorizationCodeV3",
Target = Generation.Gen5,
LibraryName = "libSceNpAuth")]
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) => RejectOnlineOperation(ctx);
public static int NpAuthGetAuthorizationCodeV3(CpuContext ctx) =>
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], AuthorizationCodeSize, ctx[CpuRegister.Rcx]);
[SysAbiExport(
ExportName = "sceNpAuthGetIdTokenV3",
Target = Generation.Gen5,
LibraryName = "libSceNpAuth")]
public static int NpAuthGetIdTokenV3(CpuContext ctx) => RejectOnlineOperation(ctx);
public static int NpAuthGetIdTokenV3(CpuContext ctx) =>
CompleteSignedOut(ctx, ctx[CpuRegister.Rdx], IdTokenSize, 0);
[SysAbiExport(
ExportName = "sceNpAuthGetAuthorizedAppCode",
Target = Generation.Gen5,
LibraryName = "libSceNpAuth")]
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => RejectOnlineOperation(ctx);
public static int NpAuthGetAuthorizedAppCode(CpuContext ctx) => CompleteSignedOut(ctx, 0, 0, 0);
private static int CreateRequest(CpuContext ctx)
private static int CreateRequest(CpuContext ctx, bool async)
{
lock (RequestGate)
{
@ -160,7 +169,7 @@ public static class NpAuthExports
while (true)
{
var id = Interlocked.Increment(ref _nextRequestId);
if (Requests.TryAdd(id, new AuthRequest()))
if (Requests.TryAdd(id, new AuthRequest(async)))
{
return ctx.SetReturn(id);
}
@ -168,8 +177,20 @@ public static class NpAuthExports
}
}
private static int RejectOnlineOperation(CpuContext ctx)
private static int CompleteSignedOut(CpuContext ctx, ulong outputAddress, int outputSize, ulong issuerAddress)
{
if (outputAddress != 0 && outputSize != 0)
{
Span<byte> cleared = stackalloc byte[outputSize];
cleared.Clear();
ctx.Memory.TryWrite(outputAddress, cleared);
}
if (issuerAddress != 0)
{
ctx.TryWriteUInt32(issuerAddress, 0);
}
if (!Requests.TryGetValue(unchecked((int)ctx[CpuRegister.Rdi]), out var request))
{
return ctx.SetReturn(NpAuthErrorRequestNotFound);
@ -177,8 +198,21 @@ public static class NpAuthExports
lock (request)
{
request.Result = request.Aborted ? NpAuthErrorAborted : NpAuthErrorServiceDown;
return ctx.SetReturn(request.Result);
if (request.Aborted)
{
request.Result = NpAuthErrorAborted;
return ctx.SetReturn(NpAuthErrorAborted);
}
if (request.Completed)
{
request.Result = NpAuthErrorInvalidArgument;
return ctx.SetReturn(NpAuthErrorInvalidArgument);
}
request.Completed = true;
request.Result = NpErrorSignedOut;
return ctx.SetReturn(request.Async ? 0 : NpErrorSignedOut);
}
}

View File

@ -95,6 +95,26 @@ public static class NpEntitlementAccessExports
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "y7JD1gfwyfA",
ExportName = "sceNpEntitlementAccessGetGameTrialsFlag",
Target = Generation.Gen5,
LibraryName = "libSceNpEntitlementAccess")]
public static int NpEntitlementAccessGetGameTrialsFlag(CpuContext ctx)
{
var flagAddress = ctx[CpuRegister.Rdi];
if (flagAddress == 0)
{
return ctx.SetReturn(NpEntitlementAccessErrorParameter);
}
Span<byte> flagBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(flagBytes, 0);
return ctx.Memory.TryWrite(flagAddress, flagBytes)
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "TFyU+KFBv54",
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfoList",

View File

@ -11,6 +11,8 @@ public static class NpUniversalDataSystemExports
private const int ErrorInvalidArgument = unchecked((int)0x80553102);
private const int MaximumGuestStringBytes = 64 * 1024;
public static void ResetRuntimeState() => NpUniversalDataSystemState.ResetRuntimeState();
[SysAbiExport(
Nid = "sjaobBgqeB4",
ExportName = "sceNpUniversalDataSystemInitialize",

View File

@ -64,9 +64,14 @@ internal static class NpUniversalDataSystemState
{
lock (Gate)
{
ResetLocked();
_initialized = true;
if (!_initialized)
{
ResetLocked();
_initialized = true;
}
_poolSize = poolSize;
UpdateMaximumInUseLocked();
return true;
}
}
@ -85,8 +90,7 @@ internal static class NpUniversalDataSystemState
{
if (!_initialized)
{
context = 0;
return false;
_initialized = true;
}
context = NextPositiveId(ref _nextContext);
@ -480,7 +484,7 @@ internal static class NpUniversalDataSystemState
}
}
internal static void ResetForTests()
internal static void ResetRuntimeState()
{
lock (Gate)
{
@ -488,6 +492,8 @@ internal static class NpUniversalDataSystemState
}
}
internal static void ResetForTests() => ResetRuntimeState();
private static ulong CreateObjectLocked(UdsObjectNode node)
{
var handle = ObjectHandleBase | ++_nextObject;

View File

@ -12,9 +12,27 @@ public static class NpWebApi2Exports
private static int _initialized;
private static int _nextLibraryContextHandle;
private static int _nextPushEventHandle;
private static int _nextPushEventCallbackHandle;
private static int _nextUserContextHandle = 1000;
private static readonly object _contextGate = new();
private static readonly HashSet<int> _libraryContexts = [];
private static readonly HashSet<int> _userContexts = [];
private static readonly HashSet<int> _pushEventFilters = [];
public static void ResetRuntimeState()
{
Interlocked.Exchange(ref _initialized, 0);
Interlocked.Exchange(ref _nextLibraryContextHandle, 0);
Interlocked.Exchange(ref _nextPushEventHandle, 0);
Interlocked.Exchange(ref _nextPushEventCallbackHandle, 0);
Interlocked.Exchange(ref _nextUserContextHandle, 1000);
lock (_contextGate)
{
_libraryContexts.Clear();
_userContexts.Clear();
_pushEventFilters.Clear();
}
}
[SysAbiExport(
Nid = "+o9816YQhqQ",
@ -51,6 +69,11 @@ public static class NpWebApi2Exports
}
var filterHandle = Interlocked.Increment(ref _nextPushEventHandle);
lock (_contextGate)
{
_pushEventFilters.Add(filterHandle);
}
TraceNpWebApi2("push-event-create-filter", libraryContextId, (ulong)filterHandle);
return ctx.SetReturn(filterHandle);
}
@ -74,6 +97,20 @@ public static class NpWebApi2Exports
return ctx.SetReturn(handle);
}
[SysAbiExport(
Nid = "fIATVMo4Y1w",
ExportName = "sceNpWebApi2PushEventDeleteHandle",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpWebApi2")]
public static int NpWebApi2PushEventDeleteHandle(CpuContext ctx)
{
TraceNpWebApi2(
"push-event-delete-handle",
unchecked((int)ctx[CpuRegister.Rdi]),
unchecked((uint)ctx[CpuRegister.Rsi]));
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "sk54bi6FtYM",
ExportName = "sceNpWebApi2CreateUserContext",
@ -97,9 +134,34 @@ public static class NpWebApi2Exports
}
var userContextId = Interlocked.Increment(ref _nextUserContextHandle);
lock (_contextGate)
{
_userContexts.Add(userContextId);
}
return ctx.SetReturn(userContextId);
}
[SysAbiExport(
Nid = "fY3QqeNkF8k",
ExportName = "sceNpWebApi2PushEventRegisterCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpWebApi2")]
public static int NpWebApi2PushEventRegisterCallback(CpuContext ctx)
{
var userContextId = unchecked((int)ctx[CpuRegister.Rdi]);
var filterId = unchecked((int)ctx[CpuRegister.Rsi]);
var callback = ctx[CpuRegister.Rdx];
if (callback == 0 || !IsRegisteredPushEventPair(userContextId, filterId))
{
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
}
var callbackId = Interlocked.Increment(ref _nextPushEventCallbackHandle);
TraceNpWebApi2("push-event-register-callback", userContextId, (ulong)callbackId);
return ctx.SetReturn(callbackId);
}
[SysAbiExport(
Nid = "bEvXpcEk200",
ExportName = "sceNpWebApi2Terminate",
@ -171,10 +233,25 @@ public static class NpWebApi2Exports
if (_libraryContexts.Count == 0)
{
Interlocked.Exchange(ref _initialized, 0);
_userContexts.Clear();
_pushEventFilters.Clear();
}
}
}
private static bool IsRegisteredPushEventPair(int userContextId, int filterId)
{
if (Volatile.Read(ref _initialized) == 0 || userContextId <= 0 || filterId <= 0)
{
return false;
}
lock (_contextGate)
{
return _userContexts.Contains(userContextId) && _pushEventFilters.Contains(filterId);
}
}
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP_WEB_API2"), "1", StringComparison.Ordinal))

View File

@ -11,6 +11,8 @@ namespace SharpEmu.Libs.Pad;
public static class PadExports
{
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
private const int OrbisPadErrorInvalidArgument = unchecked((int)0x80920001);
private const int OrbisPadErrorAlreadyOpened = unchecked((int)0x80920004);
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
@ -19,8 +21,10 @@ public static class PadExports
// valid 0x10000000 user to scePadOpen and receive DEVICE_NOT_CONNECTED,
// leaving every later keyboard/gamepad read on an invalid handle.
private const int PrimaryUserId = 0x10000000;
private const int SystemUserId = 0xFF;
private const int StandardPortType = 0;
private const int PrimaryPadHandle = 1;
private const int SpecialPortType = 2;
private const int RemotePortType = 16;
private const int ControllerInformationSize = 0x1C;
private const int PadDataSize = 0x78;
private const float StandardGravity = 9.80665f;
@ -31,11 +35,32 @@ public static class PadExports
// user slots and treats every valid handle as a connected controller, so accepting
// 0 there reports phantom pads that repeat the primary pad's buttons.
private static int _padOpened;
private static readonly object PadHandleGate = new();
private static readonly Dictionary<PadOpenKey, int> PadHandleByKey = new();
private static readonly Dictionary<int, PadOpenKey> PadKeyByHandle = new();
private static int _nextPadHandle = 1;
private static bool IsPrimaryPadHandle(int handle) =>
handle == PrimaryPadHandle || (handle == 0 && Volatile.Read(ref _padOpened) == 0);
private readonly record struct PadOpenKey(int UserId, int Type, int Index);
internal static void ResetOpenedPadForTests() => Volatile.Write(ref _padOpened, 0);
private static bool IsOpenPadHandle(int handle)
{
lock (PadHandleGate)
{
return PadKeyByHandle.ContainsKey(handle) || (handle == 0 && _padOpened == 0);
}
}
internal static void ResetOpenedPadForTests()
{
lock (PadHandleGate)
{
PadHandleByKey.Clear();
PadKeyByHandle.Clear();
_nextPadHandle = 1;
_padOpened = 0;
_initialized = false;
}
}
private static readonly long InputSampleIntervalTicks = Math.Max(1, Stopwatch.Frequency / 1000);
[ThreadStatic]
@ -45,6 +70,9 @@ public static class PadExports
private static PadState _cachedInputState;
private static bool _initialized;
private static readonly bool LogPadState =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PAD_STATE"), "1", StringComparison.Ordinal);
private static long _loggedPadSignature = -1;
// Motion data is reported until a title turns it off: Astro Bot reads it for
// shake/tilt without ever importing scePadSetMotionSensorState.
private static int _motionSensorEnabled = 1;
@ -62,6 +90,18 @@ public static class PadExports
return ctx.SetReturn(0);
}
#pragma warning disable SHEM006
[SysAbiExport(
Nid = "n3kSX62fgNo",
ExportName = "scePadUnknownN3kSX62fgNo",
Target = Generation.Gen5,
LibraryName = "libScePad")]
public static int PadUnknownN3kSX62fgNo(CpuContext ctx)
{
return ctx.SetReturn(0);
}
#pragma warning restore SHEM006
[SysAbiExport(
Nid = "xk0AcarP3V4",
ExportName = "scePadOpen",
@ -76,12 +116,6 @@ public static class PadExports
LibraryName = "libScePad")]
public static int PadOpenExt(CpuContext ctx) => PadOpenCore(ctx, extended: true);
// scePadGetHandle(userId, type, index): returns the handle of an already-open
// pad without opening a new one. Dead Cells calls it every frame to poll
// input; leaving it unresolved returned a garbage handle so the input path
// (and the game loop that drives it) misbehaved. Same validation as
// scePadOpen — the one primary pad — returning its handle or a not-connected
// error, never opening or logging.
[SysAbiExport(
Nid = "u1GRHp+oWoY",
ExportName = "scePadGetHandle",
@ -97,22 +131,24 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorNotInitialized);
}
if (userId != PrimaryUserId || type is not (0 or 1 or 2) || index != 0)
if (userId == -1)
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
return ctx.SetReturn(PrimaryPadHandle);
lock (PadHandleGate)
{
return PadHandleByKey.TryGetValue(new PadOpenKey(userId, type, index), out var handle)
? ctx.SetReturn(handle)
: ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
}
// scePadOpen rejects a non-null 4th arg and non-standard ports; scePadOpenExt accepts a
// ScePadOpenExtParam* plus ports 1/2 (racing titles retry scePadOpenExt(type=2) forever if rejected).
private static int PadOpenCore(CpuContext ctx, bool extended)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
var parameterAddress = ctx[CpuRegister.Rcx];
if (!_initialized)
{
return ctx.SetReturn(OrbisPadErrorNotInitialized);
@ -123,8 +159,7 @@ public static class PadExports
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
}
var typeAccepted = extended ? type is 0 or 1 or 2 : type == StandardPortType;
if (userId != PrimaryUserId || !typeAccepted || index != 0 || (!extended && parameterAddress != 0))
if (!IsSupportedOpenTuple(userId, type, index, extended))
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
}
@ -138,8 +173,42 @@ public static class PadExports
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
}
Volatile.Write(ref _padOpened, 1);
return ctx.SetReturn(PrimaryPadHandle);
var key = new PadOpenKey(userId, type, index);
lock (PadHandleGate)
{
if (PadHandleByKey.ContainsKey(key))
{
return ctx.SetReturn(OrbisPadErrorAlreadyOpened);
}
var handle = _nextPadHandle++;
PadHandleByKey.Add(key, handle);
PadKeyByHandle.Add(handle, key);
_padOpened = 1;
return ctx.SetReturn(handle);
}
}
private static bool IsSupportedOpenTuple(int userId, int type, int index, bool extended)
{
if (index != 0)
{
return false;
}
if (userId == SystemUserId && type == RemotePortType)
{
return true;
}
if (userId != PrimaryUserId)
{
return false;
}
return extended
? type is 0 or 1 or 2
: type is StandardPortType or SpecialPortType;
}
[SysAbiExport(
@ -150,9 +219,16 @@ public static class PadExports
public static int PadClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
lock (PadHandleGate)
{
if (!PadKeyByHandle.Remove(handle, out var key))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
PadHandleByKey.Remove(key);
return ctx.SetReturn(0);
}
}
[SysAbiExport(
@ -163,7 +239,7 @@ public static class PadExports
public static int PadSetMotionSensorState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -180,7 +256,7 @@ public static class PadExports
public static int PadSetTiltCorrectionState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -193,7 +269,7 @@ public static class PadExports
public static int PadSetAngularVelocityDeadbandState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -207,7 +283,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -258,7 +334,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var informationAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -272,7 +348,7 @@ public static class PadExports
// (DualSense). We emulate no special peripheral (guitar/drums/wheel), so
// the class-data union stays zeroed — the guest treats it as a plain
// controller with no extended capabilities.
Span<byte> information = stackalloc byte[0x20];
Span<byte> information = stackalloc byte[0x14];
information.Clear();
BinaryPrimitives.WriteInt32LittleEndian(information[0x00..], 0);
@ -281,6 +357,57 @@ public static class PadExports
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "IHPqcbc0zCA",
ExportName = "scePadDeviceClassParseData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadDeviceClassParseData(CpuContext ctx)
{
const int deviceClassDataSize = 24;
const int deviceUniqueDataLengthOffset = 0x6B;
const int deviceUniqueDataOffset = 0x6C;
const int maximumDeviceUniqueDataLength = 12;
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var padDataAddress = ctx[CpuRegister.Rsi];
var classDataAddress = ctx[CpuRegister.Rdx];
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (padDataAddress == 0 || classDataAddress == 0)
{
return ctx.SetReturn(OrbisPadErrorInvalidArgument);
}
Span<byte> padData = stackalloc byte[PadDataSize];
if (!ctx.Memory.TryRead(padDataAddress, padData))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Span<byte> classData = stackalloc byte[deviceClassDataSize];
classData.Clear();
classData[0x04] = padData[0x4C] != 0 ? (byte)1 : (byte)0;
var uniqueDataLength = Math.Min(
(int)padData[deviceUniqueDataLengthOffset],
maximumDeviceUniqueDataLength);
if (uniqueDataLength > 0)
{
BinaryPrimitives.WriteInt32LittleEndian(classData[0x00..], -1);
classData[0x08] = (byte)uniqueDataLength;
padData.Slice(deviceUniqueDataOffset, uniqueDataLength)
.CopyTo(classData.Slice(0x0C, uniqueDataLength));
}
return ctx.Memory.TryWrite(classDataAddress, classData)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "YndgXqQVV7c",
ExportName = "scePadReadState",
@ -290,7 +417,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -315,7 +442,7 @@ public static class PadExports
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -349,7 +476,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -387,7 +514,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var stateAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -575,7 +702,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -605,7 +732,7 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -634,7 +761,7 @@ public static class PadExports
public static int PadResetLightBar(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!IsPrimaryPadHandle(handle))
if (!IsOpenPadHandle(handle))
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -651,7 +778,7 @@ public static class PadExports
public static int PadResetOrientation(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -664,7 +791,7 @@ public static class PadExports
public static int PadIsRemoteController(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return IsPrimaryPadHandle(handle)
return IsOpenPadHandle(handle)
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
@ -765,6 +892,17 @@ public static class PadExports
buttons |= 0x4000;
}
if (LogPadState)
{
var signature = ((ulong)buttons << 8) | ((ulong)(acceptsKeyboardInput ? 1 : 0) << 4) | (uint)gamepadCount;
if (Interlocked.Exchange(ref _loggedPadSignature, (long)signature) != (long)signature)
{
Console.Error.WriteLine(
$"[LOADER][INFO] pad state: focused={acceptsKeyboardInput} gamepads={gamepadCount} buttons=0x{buttons:X8} " +
$"left={leftX},{leftY} thread={Environment.CurrentManagedThreadId}");
}
}
_cachedInputState = new PadState(
Connected: true,
Buttons: buttons,

View File

@ -19,6 +19,7 @@ public static class PlayGoExports
private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C);
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010);
private const int OrbisPlayGoErrorBadOptionalType = unchecked((int)0x80B20024);
private const ulong PlayGoInitBufAddrOffset = 0;
private const ulong PlayGoInitBufSizeOffset = 8;
private const uint PlayGoMinimumInitBufferSize = 0x200000;
@ -29,6 +30,10 @@ public static class PlayGoExports
private const int PlayGoInstallSpeedSuspended = 0;
private const int PlayGoInstallSpeedTrickle = 1;
private const int PlayGoInstallSpeedFull = 2;
private const int PlayGoOptionalTypeLanguage = 0;
private const int PlayGoOptionalTypeScenario = 1;
private const ulong PlayGoLanguageMaskAll = ulong.MaxValue;
private const ulong PlayGoScenarioMaskAll = 0x1f;
private const uint MaxPlayGoQueryEntries = 0x4000;
private const uint PlayGoAllEntriesSentinel = uint.MaxValue;
@ -231,7 +236,7 @@ public static class PlayGoExports
chunkIds = _metadata.ChunkIds;
}
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
var availableEntries = (uint)chunkIds.Length;
if (outChunkIdList == 0)
{
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
@ -249,7 +254,7 @@ public static class PlayGoExports
for (uint i = 0; i < entriesToWrite; i++)
{
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
var chunkId = chunkIds[i];
if (!ctx.TryWriteUInt16(outChunkIdList + (i * sizeof(ushort)), chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@ -262,6 +267,91 @@ public static class PlayGoExports
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "8-e7E989rCU",
ExportName = "scePlayGoGetInstallChunkId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetInstallChunkId(CpuContext ctx)
{
return PlayGoGetChunkId(ctx);
}
[SysAbiExport(
Nid = "g4AZyxpSAlA",
ExportName = "scePlayGoGetOptionalChunk",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetOptionalChunk(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var outOption = ctx[CpuRegister.Rdx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outOption == 0)
{
return OrbisPlayGoErrorBadPointer;
}
var option = type switch
{
PlayGoOptionalTypeLanguage => PlayGoLanguageMaskAll,
PlayGoOptionalTypeScenario => PlayGoScenarioMaskAll,
_ => 0UL,
};
if (type is not PlayGoOptionalTypeLanguage and not PlayGoOptionalTypeScenario)
{
return OrbisPlayGoErrorBadOptionalType;
}
return ctx.TryWriteUInt64(outOption, option)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "IfiN+-oeVWI",
ExportName = "scePlayGoGetSupportedOptionalChunk",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetSupportedOptionalChunk(CpuContext ctx)
{
return PlayGoGetOptionalChunk(ctx);
}
[SysAbiExport(
Nid = "HVAa744ecdw",
ExportName = "scePlayGoPrefetchOptionalChunk",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoPrefetchOptionalChunk(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var type = unchecked((int)ctx[CpuRegister.Rsi]);
var option = ctx[CpuRegister.Rdx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (option == 0)
{
return OrbisPlayGoErrorBadPointer;
}
return type is PlayGoOptionalTypeLanguage or PlayGoOptionalTypeScenario
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisPlayGoErrorBadOptionalType;
}
[SysAbiExport(
Nid = "v6EZ-YWRdMs",
ExportName = "scePlayGoGetEta",
@ -685,10 +775,7 @@ public static class PlayGoExports
{
// No app0 override to probe for sidecar files: same fully-installed
// single-chunk fallback as below, or scePlayGoOpen fails fatally.
return new PlayGoMetadata(
true,
[(ushort)0],
PlayGoChunkIdKnowledge.Authoritative);
return CreateBaseChunkMetadata(PlayGoChunkIdKnowledge.Authoritative);
}
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
@ -718,14 +805,27 @@ public static class PlayGoExports
}
var chunkIds = LoadChunkIds(chunkDefsXml);
if (chunkIds.Length == 0)
{
TracePlayGo("metadata_has_no_chunk_definitions; installed chunk ids unknown");
return new PlayGoMetadata(
true,
Array.Empty<ushort>(),
PlayGoChunkIdKnowledge.Unknown);
}
return new PlayGoMetadata(
true,
chunkIds,
chunkIds.Length == 0
? PlayGoChunkIdKnowledge.Unknown
: PlayGoChunkIdKnowledge.Authoritative);
PlayGoChunkIdKnowledge.Authoritative);
}
private static PlayGoMetadata CreateBaseChunkMetadata(PlayGoChunkIdKnowledge knowledge) =>
new(
true,
[(ushort)0],
knowledge);
// Chunk ids for a title that ships no PlayGo sidecar, taken from the
// pakchunk<N>-<platform>.pak files on disk. Chunk 0 is always included: it
// is the base chunk and must resolve even for a title with no pak files at
@ -831,7 +931,7 @@ public static class PlayGoExports
}
}
internal static void ResetForTests()
public static void ResetRuntimeState()
{
lock (_stateGate)
{
@ -846,6 +946,8 @@ public static class PlayGoExports
Interlocked.Exchange(ref _locusTraceDiagnostics, 0);
}
internal static void ResetForTests() => ResetRuntimeState();
private enum PlayGoChunkIdKnowledge
{
Unknown,

View File

@ -74,9 +74,11 @@ public static class SaveDataExports
private const int OrbisSaveDataErrorNoEvent = unchecked((int)0x809F0008); // NOT_FOUND: no pending event
private const int OrbisSaveDataErrorBadMounted = unchecked((int)0x809F0013);
// SceSaveDataEventType
private const uint EventTypeBackupEnd = 2;
private const uint EventTypeSaveDataMemorySyncEnd = 3;
private const int SaveDataEventSize = 0x60;
private const int MountInfoSize = 0x40;
private const int SaveDataTitleIdStructSize = 0x10;
private const int SaveDataEventSize = 0x68;
private const int MountInfoSize = 0x30;
private const uint SaveDataBlockSize = 65536;
private const ulong SaveDataBlocksMax = 16384;
@ -86,16 +88,79 @@ public static class SaveDataExports
// mountPoint -> live mount, for umount/IsMounted/GetMountInfo.
private static readonly Dictionary<string, MountEntry> _mounts = new(StringComparer.Ordinal);
private readonly record struct SaveDataEvent(uint Type, int ErrorCode, int UserId, string DirName);
private readonly record struct SaveDataEvent(
uint Type,
int ErrorCode,
int UserId,
string TitleId,
string DirName);
private sealed record MountEntry(string SlotDir, string DirName, int UserId);
private static void EnqueueEvent(uint type, int userId, string dirName, int errorCode = 0)
private static void EnqueueEvent(
uint type,
int userId,
string titleId,
string dirName,
int errorCode = 0)
{
lock (_eventGate)
{
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, dirName));
_events.Enqueue(new SaveDataEvent(type, errorCode, userId, titleId, dirName));
}
TraceSaveData($"event.enqueue type={type} user={userId} dir='{dirName}' err=0x{errorCode:X}");
TraceSaveData($"event.enqueue type={type} user={userId} title='{titleId}' dir='{dirName}' err=0x{errorCode:X}");
}
[SysAbiExport(
Nid = "z1JA8-iJt3k",
ExportName = "sceSaveDataBackup",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataBackup(CpuContext ctx)
{
var backupAddress = ctx[CpuRegister.Rdi];
if (backupAddress == 0)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!TryReadInt32(ctx, backupAddress, out var userId) ||
!ctx.TryReadUInt64(backupAddress + 0x08, out var titleIdAddress) ||
!ctx.TryReadUInt64(backupAddress + 0x10, out var dirNameAddress))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (userId < 0 || dirNameAddress == 0)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (string.IsNullOrWhiteSpace(dirName))
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
string titleId;
if (titleIdAddress == 0)
{
titleId = ResolveConfiguredTitleId();
}
else if (!TryReadFixedAscii(ctx, titleIdAddress, SaveDataTitleIdSize, out titleId))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
else if (string.IsNullOrWhiteSpace(titleId))
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
EnqueueEvent(EventTypeBackupEnd, userId, titleId, dirName);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@ -131,7 +196,14 @@ public static class SaveDataExports
BinaryPrimitives.WriteUInt32LittleEndian(ev[0x00..], pending.Type);
BinaryPrimitives.WriteInt32LittleEndian(ev[0x04..], pending.ErrorCode);
BinaryPrimitives.WriteInt32LittleEndian(ev[0x08..], pending.UserId);
WriteAscii(ev.Slice(0x10, SaveDataDirNameSize), pending.DirName);
WriteFixedAscii(
ev.Slice(0x10, SaveDataTitleIdStructSize),
pending.TitleId,
SaveDataTitleIdSize);
WriteFixedAscii(
ev.Slice(0x20, SaveDataDirNameSize),
pending.DirName,
SaveDataDirNameSize);
if (!ctx.Memory.TryWrite(eventAddress, ev))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@ -617,6 +689,11 @@ public static class SaveDataExports
ExportName = "sceSaveDataDirNameSearchPs4",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
[SysAbiExport(
Nid = "PHnuI4LhuRk",
ExportName = "sceSaveDataDirNameSearch2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataDirNameSearch(CpuContext ctx)
{
var condAddress = ctx[CpuRegister.Rdi];
@ -1241,6 +1318,19 @@ public static class SaveDataExports
}
}
private static void WriteFixedAscii(
Span<byte> destination,
string value,
int maxContentLength)
{
var count = Math.Min(value.Length, Math.Min(destination.Length, maxContentLength));
for (var i = 0; i < count; i++)
{
var ch = value[i];
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
}
}
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
@ -1486,7 +1576,11 @@ public static class SaveDataExports
// sync as asynchronous and blocks a worker on sceSaveDataGetEventResult
// until the SAVE_DATA_MEMORY_SYNC_END event arrives. Post it so that
// poll completes (this is what wedged Dead Cells at FLIP 0 in-level).
EnqueueEvent(EventTypeSaveDataMemorySyncEnd, userId, string.Empty);
EnqueueEvent(
EventTypeSaveDataMemorySyncEnd,
userId,
ResolveConfiguredTitleId(),
"sce_sdmemory");
return ctx.SetReturn(0);
}

View File

@ -0,0 +1,34 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Share;
public static class SharePlayExports
{
private static int _controllerProhibited;
[SysAbiExport(
Nid = "FzQS6DREDfk",
ExportName = "sceSharePlayProhibitController",
Target = Generation.Gen5,
LibraryName = "libSceSharePlay")]
public static int SharePlayProhibitController(CpuContext ctx)
{
var prohibited = ctx[CpuRegister.Rdi];
if (prohibited > 1)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Volatile.Write(ref _controllerProhibited, unchecked((int)prohibited));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
public static void ResetRuntimeState() => Volatile.Write(ref _controllerProhibited, 0);
internal static bool IsControllerProhibited => Volatile.Read(ref _controllerProhibited) != 0;
internal static void ResetForTests() => ResetRuntimeState();
}

View File

@ -64,14 +64,6 @@ public static class GameServiceStubs
Target = Generation.Gen5, LibraryName = "libSceNpGameIntent")]
public static int NpGameIntentTerminate(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "jqb7HntFQFc", ExportName = "sceWebBrowserDialogInitialize",
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogInitialize(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "ocHtyBwHfys", ExportName = "sceWebBrowserDialogTerminate",
Target = Generation.Gen5, LibraryName = "libSceWebBrowserDialog")]
public static int WebBrowserDialogTerminate(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "mlYGfmqE3fQ", ExportName = "sceSigninDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceSigninDialog")]
public static int SigninDialogInitialize(CpuContext ctx) => Ok(ctx);
@ -114,6 +106,14 @@ public static class GameServiceStubs
public static int TextToSpeech2Open(CpuContext ctx) =>
ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
[SysAbiExport(Nid = "2jiIxUmcsGo", ExportName = "sceTextToSpeech2Cancel",
Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")]
public static int TextToSpeech2Cancel(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "08JSg9p6bgQ", ExportName = "sceTextToSpeech2GetSpeechStatus",
Target = Generation.Gen5, LibraryName = "libSceTextToSpeech2")]
public static int TextToSpeech2GetSpeechStatus(CpuContext ctx) => Ok(ctx);
[SysAbiExport(Nid = "kvYEw2lBndk", ExportName = "sceGameLiveStreamingInitialize",
Target = Generation.Gen5, LibraryName = "libSceGameLiveStreaming")]
public static int GameLiveStreamingInitialize(CpuContext ctx) => Ok(ctx);

View File

@ -34,6 +34,17 @@ public static class UserServiceExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bwFjS+bX9mA",
ExportName = "sceUserServiceTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceTerminate(CpuContext ctx)
{
Interlocked.Exchange(ref _loginEventDelivered, 0);
return SetReturnWithTrace(ctx, 0, "terminate");
}
[SysAbiExport(
Nid = "CdWp0oHWGr0",
ExportName = "sceUserServiceGetInitialUser",
@ -144,6 +155,30 @@ public static class UserServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "qbwy0Ub8b3M",
ExportName = "sceUserServiceGetUserNumber",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetUserNumber(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var numberAddress = ctx[CpuRegister.Rsi];
if (numberAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
if (userId != PrimaryUserId && userId != 1)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
return TryWriteInt32(ctx, numberAddress, 1)
? SetReturnWithTrace(ctx, 0, $"get_user_number user={userId} number=1 out=0x{numberAddress:X16}")
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
// Name not yet in ps5_names.txt and the NID was captured from titles; revisit when the symbol is catalogued.
#pragma warning disable SHEM006
[SysAbiExport(

View File

@ -67,8 +67,16 @@ public sealed record HostVideoOptions
public PerformanceOverlayCorner OverlayCorner { get; init; } = PerformanceOverlayCorner.TopRight;
public PerformanceOverlayMode OverlayMode { get; init; } = PerformanceOverlayMode.TitleBar;
// On macOS, Auto keeps guests on SDR output: reporting HDR moves titles onto render paths
// MoltenVK cannot build yet (Silent Hill's HDR switch fails a pipeline compile and then
// crashes inside MoltenVK on every run). --hdr=on still opts in.
internal static bool AutoHdrAllowed => !OperatingSystem.IsMacOS();
internal bool CanUseHdr(bool displayHdrEnabled, bool surfaceSupportsHdr) =>
surfaceSupportsHdr && (HdrMode == HostHdrMode.On || HdrMode == HostHdrMode.Auto && displayHdrEnabled);
CanUseHdr(displayHdrEnabled, surfaceSupportsHdr, AutoHdrAllowed);
internal bool CanUseHdr(bool displayHdrEnabled, bool surfaceSupportsHdr, bool autoAllowed) =>
surfaceSupportsHdr && (HdrMode == HostHdrMode.On || HdrMode == HostHdrMode.Auto && autoAllowed && displayHdrEnabled);
public HostVideoOptions Normalize() => this with
{

View File

@ -398,15 +398,18 @@ public static partial class VideoOutExports
public static int VideoOutClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
GuestGpu.Current.RunAfterPendingCommandStreams(() =>
{
foreach (var request in _flipRequests.Values.Where(request => request.Handle == handle).ToArray())
lock (_stateGate)
{
CancelFlipLocked(request);
foreach (var request in _flipRequests.Values.Where(request => request.Handle == handle).ToArray())
{
CancelFlipLocked(request);
}
_ports.Remove(handle);
Monitor.PulseAll(_stateGate);
}
_ports.Remove(handle);
Monitor.PulseAll(_stateGate);
}
});
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@ -1246,36 +1249,42 @@ public static partial class VideoOutExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var attributeIndex = unchecked((int)ctx[CpuRegister.Rsi]);
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (attributeIndex < 0)
{
return OrbisVideoOutErrorInvalidValue;
}
lock (_stateGate)
var result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
GuestGpu.Current.RunAfterPendingCommandStreams(() =>
{
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
lock (_stateGate)
{
return OrbisVideoOutErrorInvalidValue;
}
port.Groups[attributeIndex] = null;
foreach (var slot in port.BufferSlots)
{
if (slot.GroupIndex == attributeIndex)
if (!_ports.TryGetValue(handle, out var port))
{
slot.GroupIndex = -1;
slot.AddressLeft = 0;
slot.AddressRight = 0;
result = OrbisVideoOutErrorInvalidHandle;
return;
}
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
{
result = OrbisVideoOutErrorInvalidValue;
return;
}
port.Groups[attributeIndex] = null;
foreach (var slot in port.BufferSlots)
{
if (slot.GroupIndex == attributeIndex)
{
slot.GroupIndex = -1;
slot.AddressLeft = 0;
slot.AddressRight = 0;
}
}
}
});
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
return result;
}
[SysAbiExport(

View File

@ -276,7 +276,7 @@ internal static unsafe partial class VulkanVideoPresenter
private bool ResolveDccAttachmentClear(ColorAttachment target, out ClearColorValue clearValue)
{
clearValue = default;
if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc)
if (target.Request.Description.Metadata.Kind != MetadataKind.Dcc || target.Request.Description.Metadata.Range.Size != 0)
{
return false;
}

View File

@ -40,6 +40,7 @@ internal static unsafe partial class VulkanVideoPresenter
}
EnsureStarted(1280, 720);
WaitForShaderPrewarm("first GPU submission");
// Queue publication precedes device initialization; only the render thread consumes it.
lock (_gate)
{
@ -71,6 +72,26 @@ internal static unsafe partial class VulkanVideoPresenter
? testStream.Done()
: IdleOutcome.Completed;
public static void RunAfterPendingCommandStreams(Action work)
{
ArgumentNullException.ThrowIfNull(work);
if (TryGetActivePresenter(out var presenter))
{
if (presenter.Relay.TryRunAfterAcceptedCommandStreams(work))
{
return;
}
if (!HostSessionControl.IsShutdownRequested && !Volatile.Read(ref _closed) &&
!Volatile.Read(ref _presenterCloseRequested))
{
throw SubmissionScheduler.Fatal("The GPU worker rejected an ordered video-out state change.");
}
}
work();
}
// The blocked heads of the stream this memory submits to; null when no stream exists for it.
internal static BlockedSnapshot? SnapshotBlockedCommandStream(ICpuMemory? memory)
{
@ -117,6 +138,7 @@ internal static unsafe partial class VulkanVideoPresenter
_ = width;
_ = height;
_ = pitchInPixel;
WaitForShaderPrewarm("first flip");
if (!IsKnownDisplayBuffer(address) || !TryGetActivePresenter(out var presenter))
{
return false;

View File

@ -110,6 +110,8 @@ internal static unsafe partial class VulkanVideoPresenter
DepthCompare: image.DepthCompare,
Atomic: image.Atomic);
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, byte> _tracedTextureBindings = new();
// Render-state discovery for one shader image; the view is acquired later with the draw.
private TextureResource ResolveImageBinding(ImageResource image, uint[] words, ShaderProgramInfo program, int index)
{
@ -127,11 +129,12 @@ internal static unsafe partial class VulkanVideoPresenter
imageIdentifier = ImageRequestBuilders.ValidateTextureOwner(_imageCache, imageIdentifier, resolution);
BindImage(imageIdentifier, storage);
var descriptor = new TextureDescriptorWords(words);
if (ShouldTraceTextureBindings())
if (ShouldTraceTextureBindings() &&
(!_traceVolumeTextureBindingsOnly || image.Dimension == ImageDimension.Dim3D))
{
var cached = _imageCache.GetImage(imageIdentifier);
var description = cached.Description;
Console.Error.WriteLine(
var line =
$"TextureBinding stage={program.Stage} hash=0x{program.Hash:X16} index={index} " +
$"address=0x{new TextureDescriptorWords(words).BaseAddress:X16} " +
$"descriptor={descriptor.BaseAddress:X16} size={descriptor.Width + 1}x{descriptor.Height + 1} " +
@ -139,7 +142,14 @@ internal static unsafe partial class VulkanVideoPresenter
$"image=0x{description.Data.Address:X16} size=0x{description.Data.Size:X} " +
$"extent={description.Extent.Width}x{description.Extent.Height} pitch={description.Pitch} " +
$"guestFormat={(uint)description.GuestFormat} imageTile={(uint)description.TileMode} " +
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format}");
$"backing={cached.Backing.Extent.Width}x{cached.Backing.Extent.Height} format={cached.Backing.Format} " +
$"depth={descriptor.Depth + 1} type={descriptor.Type} dcc={descriptor.MetadataCompress} " +
$"metadata=0x{descriptor.MetadataAddress << 8:X} metaKind={description.Metadata.Kind} " +
$"words={string.Join(',', words.Select(static word => word.ToString("X8")))}";
if (!_traceVolumeTextureBindingsOnly || _tracedTextureBindings.TryAdd(line, 0))
{
Console.Error.WriteLine(line);
}
}
return new TextureResource
{
@ -402,8 +412,7 @@ internal static unsafe partial class VulkanVideoPresenter
}
using var profileScope = BufferUploadProfile.BeginSweep(vertexProgramHash, pixelProgramHash, computeProgramHash);
var memory = GuestGpuMemoryHook.Current ?? throw SubmissionScheduler.Fatal("A device-address program needs the guest GPU memory registry.");
var spans = new List<GuestSpan>();
memory.ForEachSpan((address, size) => spans.Add(new GuestSpan(address, size)));
var spans = DeviceAddressSpans(memory);
var traceAddress = GuestGpuMemoryHook.TraceAddress;
if (traceAddress != 0)
{
@ -420,7 +429,29 @@ internal static unsafe partial class VulkanVideoPresenter
if (traceAddress != 0 && !memory.Covers(traceAddress, 1))
GuestGpuMemoryHook.Trace(traceAddress, 1,
$"device-address-mapping-check readable={_guestMemory.CanRead(traceAddress, 1)} backed={_guestBacking.IsBackedView(traceAddress)}");
_bufferCache.PrepareBda(spans);
_bufferCache.PrepareBda(spans, _bdaSpanMapping);
}
private List<GuestSpan>? _bdaSpans;
private GuestGpuMemory? _bdaSpanMemory;
private long _bdaSpanVersion = -1;
private ulong _bdaSpanMapping;
private List<GuestSpan> DeviceAddressSpans(GuestGpuMemory memory)
{
var version = memory.SpanVersion;
if (_bdaSpans is { } cached && ReferenceEquals(_bdaSpanMemory, memory) && _bdaSpanVersion == version)
{
return cached;
}
var spans = new List<GuestSpan>();
memory.ForEachSpan((address, size) => spans.Add(new GuestSpan(address, size)));
_bdaSpans = spans;
_bdaSpanMemory = memory;
_bdaSpanVersion = version;
_bdaSpanMapping = GuestBufferCache.MappingKey(spans);
return spans;
}
public void BindResources(IPreparedBindings prepared)

View File

@ -45,6 +45,7 @@ internal static unsafe partial class VulkanVideoPresenter
private ulong _minStorageBufferOffsetAlignment = 1;
private bool _supportsIndependentBlend;
private bool _supportsDepthBiasClamp;
private bool _supportsShaderSignedZeroInfNanPreserveFloat32;
private uint _maxColorAttachments;
private Device _device;
private PipelineCache _pipelineCache;
@ -583,10 +584,15 @@ internal static unsafe partial class VulkanVideoPresenter
{
SType = StructureType.PhysicalDevicePushDescriptorPropertiesKhr,
};
var floatControls = new PhysicalDeviceFloatControlsProperties
{
SType = StructureType.PhysicalDeviceFloatControlsProperties,
PNext = &pushDescriptorProperties,
};
var subgroupSizeControl = new PhysicalDeviceSubgroupSizeControlProperties
{
SType = StructureType.PhysicalDeviceSubgroupSizeControlProperties,
PNext = &pushDescriptorProperties,
PNext = &floatControls,
};
var subgroup = new PhysicalDeviceSubgroupProperties
{
@ -599,6 +605,8 @@ internal static unsafe partial class VulkanVideoPresenter
PNext = &subgroup,
};
_vk.GetPhysicalDeviceProperties2(_physicalDevice, &properties2);
_supportsShaderSignedZeroInfNanPreserveFloat32 =
floatControls.ShaderSignedZeroInfNanPreserveFloat32;
SetNativeSubgroupCapabilities(subgroup.SubgroupSize, subgroup.SupportedStages);
_canRequireComputeSubgroup32 =
subgroup.SubgroupSize != RdnaSubgroupSize &&
@ -657,6 +665,9 @@ internal static unsafe partial class VulkanVideoPresenter
_ when GraphicsSubgroupOperationsEnabled => "auto-native-wave32",
_ => "auto-fallback",
};
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan shaderSignedZeroInfNanPreserveFloat32 " +
$"enabled={_supportsShaderSignedZeroInfNanPreserveFloat32}");
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan graphics subgroup operations " +
$"enabled={GraphicsSubgroupOperationsEnabled} " +
@ -742,6 +753,7 @@ internal static unsafe partial class VulkanVideoPresenter
_supportsIndependentBlend = supportedFeatures.IndependentBlend;
_supportsDepthBiasClamp = supportedFeatures.DepthBiasClamp;
_supportsDepthBounds = supportedFeatures.DepthBounds;
_supportsShaderClipDistance = supportedFeatures.ShaderClipDistance;
_supportsFillRectangle = IsDeviceExtensionAvailable(FillRectangleExtensionName);
var enabledFeatures = new PhysicalDeviceFeatures
{
@ -751,6 +763,8 @@ internal static unsafe partial class VulkanVideoPresenter
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
ShaderInt64 = supportedFeatures.ShaderInt64,
ShaderFloat64 = supportedFeatures.ShaderFloat64,
ShaderClipDistance = supportedFeatures.ShaderClipDistance,
ShaderCullDistance = supportedFeatures.ShaderCullDistance,
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,

View File

@ -91,10 +91,9 @@ internal static unsafe partial class VulkanVideoPresenter
"1",
StringComparison.Ordinal);
private static readonly bool _traceTextureBindingsEnabled =
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS"),
"1",
StringComparison.Ordinal);
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS") is "1" or "volume";
private static readonly bool _traceVolumeTextureBindingsOnly =
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_TEXTURE_BINDINGS") == "volume";
private static readonly uint _traceGuestImageWidth =
uint.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_WIDTH"),

View File

@ -87,9 +87,12 @@ internal static unsafe partial class VulkanVideoPresenter
bool IShaderPipelineHost.GraphicsSubgroupOperationsEnabled => GraphicsSubgroupOperationsEnabled;
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => SharedInt64AtomicsEnabled;
bool IShaderPipelineHost.ShaderSignedZeroInfNanPreserveFloat32Supported =>
_supportsShaderSignedZeroInfNanPreserveFloat32;
// NVIDIA's compiler rejects the elided-EXEC wave64 compute module with NVVM error 3.
bool IShaderPipelineHost.ExecGuardElisionEnabled => _physicalDeviceVendorId != NvidiaVendorId;
bool IShaderPipelineHost.PerVertexPixelInputsSupported => _supportsPerVertexPixelInputs;
bool IShaderPipelineHost.ClipDistanceEnabled => _supportsShaderClipDistance;
RenderHostLimits IShaderPipelineHost.Limits => _renderHostLimits;
@ -100,6 +103,7 @@ internal static unsafe partial class VulkanVideoPresenter
{
using var profile = ResourceMaterializationProfile.Measure(ResourceMaterializationProfile.Phase.GuestRead);
word = 0;
var synchronized = false;
if (IsCleanReadPage(address, sizeof(uint)))
{
if (TryGetAliasPointer(address, sizeof(uint), out var alias))
@ -132,6 +136,8 @@ internal static unsafe partial class VulkanVideoPresenter
{
return false;
}
synchronized = true;
}
else
{
@ -146,6 +152,11 @@ internal static unsafe partial class VulkanVideoPresenter
}
word = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(bytes);
if (Diagnostics.GpuReadTrace.Enabled && synchronized)
{
Diagnostics.GpuReadTrace.RecordValue(address, word);
}
return true;
}

View File

@ -157,8 +157,73 @@ internal static unsafe partial class VulkanVideoPresenter
_guestFlipVersionSequence = 0;
}
private static readonly bool _shaderPrewarmWaitDisabled =
Environment.GetEnvironmentVariable("SHARPEMU_SHADER_PREWARM_WAIT") == "0";
private static bool _shaderPrewarmDecided;
private static bool _shaderPrewarmRunning;
private static bool _shaderPrewarmReleased;
private static int _shaderPrewarmHoldReported;
private static int _shaderPrewarmProgress;
private static int _shaderPrewarmTotal;
private static void SetShaderPrewarmState(bool running)
{
lock (_gate)
{
_shaderPrewarmDecided = true;
_shaderPrewarmRunning = running;
Volatile.Write(ref _shaderPrewarmReleased, !running);
System.Threading.Monitor.PulseAll(_gate);
}
}
internal static void WaitForShaderPrewarm(string holder)
{
if (_shaderPrewarmWaitDisabled || Volatile.Read(ref _shaderPrewarmReleased))
{
return;
}
var started = System.Diagnostics.Stopwatch.GetTimestamp();
var reported = started;
var reporter = false;
lock (_gate)
{
while (_thread is not null && (!_shaderPrewarmDecided || _shaderPrewarmRunning) &&
!_closed && _presenterStartupFailure is null &&
!HostSessionControl.IsShutdownRequested && !Volatile.Read(ref _presenterCloseRequested))
{
if (!reporter && _shaderPrewarmRunning &&
Interlocked.Exchange(ref _shaderPrewarmHoldReported, 1) == 0)
{
reporter = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Shader prewarm: holding the game at its {holder} until it finishes.");
}
System.Threading.Monitor.Wait(_gate, 1000);
if (reporter && System.Diagnostics.Stopwatch.GetElapsedTime(reported) >= TimeSpan.FromSeconds(5))
{
reported = System.Diagnostics.Stopwatch.GetTimestamp();
var total = Volatile.Read(ref _shaderPrewarmTotal);
var done = Volatile.Read(ref _shaderPrewarmProgress);
Console.Error.WriteLine(
$"[LOADER][INFO] Shader prewarm: {done}/{total} ({(total == 0 ? 0 : done * 100 / total)}%)");
}
}
}
if (reporter)
{
Console.Error.WriteLine(string.Create(
System.Globalization.CultureInfo.InvariantCulture,
$"[LOADER][INFO] Shader prewarm: game released after {System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalSeconds:F1}s."));
}
}
public static void HideSplashScreen()
{
WaitForShaderPrewarm("splash screen");
lock (_gate)
{
_splashHidden = true;

View File

@ -126,6 +126,7 @@ internal static unsafe partial class VulkanVideoPresenter
private bool _supportsDepthClipControl;
private bool _supportsDepthClipEnable;
private bool _supportsDepthBounds;
private bool _supportsShaderClipDistance;
private bool _supportsFillRectangle;
private RenderHostLimits _renderHostLimits;
private IGuestBackedSpace _guestBacking = null!;
@ -573,7 +574,8 @@ internal static unsafe partial class VulkanVideoPresenter
const byte DccClearToZero = 0x00;
foreach (var binding in bindings)
{
if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc)
if (binding.IsHostMovie || binding.CachedImage is not { } image || image.Description.Metadata.Kind != MetadataKind.Dcc ||
image.Description.Metadata.Range.Size != 0)
{
continue;
}
@ -602,7 +604,7 @@ internal static unsafe partial class VulkanVideoPresenter
_vk.CmdClearColorImage(command, image.Backing.Handle, ImageLayout.TransferDstOptimal, &clearValue, 1, &vkRange);
if (!tracked)
{
_bufferCache.FillBuffer(guestSlice, sliceSize, uint.MaxValue, false);
_bufferCache.FillDccMetadata(guestSlice, sliceSize, uint.MaxValue);
if (RenderTrace.Enabled && RenderTrace.MetadataClear())
{
RenderTrace.Write(
@ -1200,5 +1202,8 @@ internal static unsafe partial class VulkanVideoPresenter
_bufferCache.FillDccMetadata(address, size, fillValue);
return true;
}
public bool TryCopyWordsOnHost(ulong destination, ulong source, ulong sourceWords, ulong words) =>
_bufferCache.TryCopyWordsOnHost(destination, source, sourceWords, words);
}
}

View File

@ -22,7 +22,7 @@ internal static unsafe partial class VulkanVideoPresenter
// This partial owns the submission scheduler and the GPU worker relay.
private readonly object _queueGate = new();
private readonly GpuWorkerRelay _relay = new(WakeRenderThread);
private readonly GpuWorkerRelay _relay;
internal GpuWorkerRelay Relay => _relay;
private readonly SubmissionContext _submissionContext = new();

View File

@ -17,6 +17,7 @@ internal static unsafe partial class VulkanVideoPresenter
private sealed partial class Presenter
{
private const int MaxLoggedPrewarmFailures = 8;
private const int PrewarmProgressBatch = 256;
private ShaderPrewarmList? _shaderPrewarmList;
private ConcurrentDictionary<string, byte>? _prewarmedShaderIdentities;
@ -27,16 +28,25 @@ internal static unsafe partial class VulkanVideoPresenter
private int _shaderPrewarmCompiled;
private int _shaderPrewarmFailed;
private int _shaderPrewarmRuntimeHits;
private readonly ConcurrentQueue<ulong> _shaderPrewarmCompleted = new();
ShaderPrewarmList? IShaderPipelineHost.ShaderPrewarm => Volatile.Read(ref _shaderPrewarmList);
private void StartShaderPrewarm()
{
if (!StartShaderPrewarmCore())
{
SetShaderPrewarmState(false);
}
}
private bool StartShaderPrewarmCore()
{
if (_shaderPrewarmList is not null ||
_pipelineCacheShardDirectory is not null ||
Environment.GetEnvironmentVariable("SHARPEMU_SHADER_PREWARM") == "0")
{
return;
return false;
}
var directory = Path.GetDirectoryName(VulkanPipelineCacheStorage.ResolvePath(
@ -44,7 +54,7 @@ internal static unsafe partial class VulkanVideoPresenter
Environment.GetEnvironmentVariable("SHARPEMU_VK_PIPELINE_CACHE_PATH")));
if (string.IsNullOrEmpty(directory) || ShaderPrewarmList.Open(directory) is not { } list)
{
return;
return false;
}
_prewarmedShaderIdentities = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal);
@ -57,7 +67,7 @@ internal static unsafe partial class VulkanVideoPresenter
{
list.WriteStamp(stamp);
_shaderPrewarmFinished = true;
return;
return false;
}
if (list.IsStampCurrent(stamp))
@ -65,18 +75,31 @@ internal static unsafe partial class VulkanVideoPresenter
_shaderPrewarmFinished = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Shader prewarm skipped: compute={list.LoadedComputeCount} reason=same-build-and-driver");
return;
return false;
}
var work = new ConcurrentQueue<(ComputePrewarmRecord Record, ShaderCodeCapture Code)>(list.LoadedComputes());
var completed = list.ReadProgress(stamp);
var pending = list.LoadedComputes()
.Where(item => !completed.Contains(ShaderPrewarmList.Identity(item.Record)))
.ToList();
var started = Stopwatch.GetTimestamp();
if (pending.Count == 0)
{
FinishShaderPrewarm(list, stamp, started);
return false;
}
var work = new ConcurrentQueue<(ComputePrewarmRecord Record, ShaderCodeCapture Code)>(pending);
var workerCount = Math.Clamp(Environment.ProcessorCount / 4, 1, 4);
var remaining = workerCount;
var started = Stopwatch.GetTimestamp();
var host = (IShaderPipelineHost)this;
var sharedInt64Atomics = host.SharedInt64AtomicsEnabled;
var execGuardElision = host.ExecGuardElisionEnabled;
Volatile.Write(ref _shaderPrewarmTotal, pending.Count);
Volatile.Write(ref _shaderPrewarmProgress, 0);
Console.Error.WriteLine(
$"[LOADER][INFO] Shader prewarm started: compute={work.Count} workers={workerCount}");
$"[LOADER][INFO] Shader prewarm started: compute={pending.Count} resumed={completed.Count} workers={workerCount}");
SetShaderPrewarmState(true);
_shaderPrewarmThreads = new Thread[workerCount];
for (var index = 0; index < workerCount; index++)
{
@ -85,8 +108,14 @@ internal static unsafe partial class VulkanVideoPresenter
while (!_shaderPrewarmStopping && work.TryDequeue(out var item))
{
PrewarmComputePipeline(item.Record, item.Code, compiler, sharedInt64Atomics, execGuardElision);
_shaderPrewarmCompleted.Enqueue(ShaderPrewarmList.Identity(item.Record));
if (Interlocked.Increment(ref _shaderPrewarmProgress) % PrewarmProgressBatch == 0)
{
FlushShaderPrewarmProgress(list, stamp);
}
}
FlushShaderPrewarmProgress(list, stamp);
if (Interlocked.Decrement(ref remaining) == 0)
{
FinishShaderPrewarm(list, stamp, started);
@ -99,6 +128,19 @@ internal static unsafe partial class VulkanVideoPresenter
};
_shaderPrewarmThreads[index].Start();
}
return true;
}
private void FlushShaderPrewarmProgress(ShaderPrewarmList list, string stamp)
{
var batch = new List<ulong>();
while (_shaderPrewarmCompleted.TryDequeue(out var identity))
{
batch.Add(identity);
}
list.AppendProgress(stamp, batch);
}
private static string ShaderPrewarmStampPart(Type type) =>
@ -109,8 +151,7 @@ internal static unsafe partial class VulkanVideoPresenter
'|',
ShaderPrewarmStampPart(typeof(Gen5ShaderTranslator)),
ShaderPrewarmStampPart(typeof(Gen5SpirvTranslator)),
ShaderPrewarmStampPart(compiler.GetType()),
ShaderPrewarmStampPart(typeof(ShaderProgramCache)),
compiler.GetType().FullName,
DriverCacheSignature().TrimEnd('\n'));
private void PrewarmComputePipeline(
@ -121,7 +162,8 @@ internal static unsafe partial class VulkanVideoPresenter
bool execGuardElision)
{
if (!ShaderProgramCache.TryCompilePrewarm(
record, code, compiler, sharedInt64Atomics, execGuardElision, out var compiled, out var layout, out var error))
record, code, compiler, sharedInt64Atomics, execGuardElision,
_supportsShaderSignedZeroInfNanPreserveFloat32, out var compiled, out var layout, out var error))
{
NoteShaderPrewarmFailure(record, error);
return;
@ -182,11 +224,14 @@ internal static unsafe partial class VulkanVideoPresenter
var matched = runtime.Keys.Count(prewarmed.ContainsKey);
if (_shaderPrewarmStopping)
{
SetShaderPrewarmState(false);
return;
}
list.WriteStamp(stamp);
list.ClearProgress();
_shaderPrewarmFinished = true;
SetShaderPrewarmState(false);
_pipelineCacheDirty = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Shader prewarm done: compiled={Volatile.Read(ref _shaderPrewarmCompiled)} " +
@ -218,6 +263,7 @@ internal static unsafe partial class VulkanVideoPresenter
private void StopShaderPrewarm()
{
_shaderPrewarmStopping = true;
SetShaderPrewarmState(false);
foreach (var thread in _shaderPrewarmThreads)
{
thread.Join(TimeSpan.FromSeconds(10));

View File

@ -9,6 +9,7 @@ using SharpEmu.Libs.AvPlayer;
using SharpEmu.Libs.Media;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Gpu.GpuCommands;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.Libs.Gpu.Images;
using SharpEmu.ShaderCompiler;
using SharpEmu.ShaderCompiler.Vulkan;
@ -255,6 +256,7 @@ internal static unsafe partial class VulkanVideoPresenter
public Presenter(uint width, uint height)
{
_commandStream = new CommandStreamQueue(this);
_relay = new GpuWorkerRelay(WakeRenderThread, _commandStream.TryEnqueueControlBarrier);
_hostBufferPool = new VulkanHostBufferPool(
MaximumCachedHostBufferBytes,
DestroyHostBufferAllocation);

View File

@ -142,15 +142,15 @@ public static partial class Gen5MslTranslator
}
case "VCndmaskB32":
{
// dst = mask-bit(lane) ? src1 : src0. Sources are raw (no
// float modifiers), matching the SPIR-V translator; the mask
// is VCC for VOP2 and an explicit SGPR operand for VOP3.
// dst = mask-bit(lane) ? src1 : src0. VOP3 abs/neg act on the
// sign bit, matching the SPIR-V translator; the mask is VCC
// for VOP2 and an explicit SGPR operand for VOP3.
var mask = instruction.Sources.Count > 2
? MaskBitExpression(instruction.Sources[2])
: "vcc";
StoreVector(
DestinationVector(instruction),
$"({mask}) ? ({RawSource(instruction, 1)}) : ({RawSource(instruction, 0)})");
$"({mask}) ? ({SignModifiedSource(instruction, 1)}) : ({SignModifiedSource(instruction, 0)})");
return true;
}
}
@ -1776,6 +1776,24 @@ public static partial class Gen5MslTranslator
return value;
}
private string SignModifiedSource(Gen5ShaderInstruction instruction, int sourceIndex)
{
var value = RawSource(instruction, sourceIndex);
if (instruction.Control is not Gen5Vop3Control control)
{
return value;
}
if ((control.AbsoluteMask & (1u << sourceIndex)) != 0)
{
value = $"(({value}) & 0x7FFFFFFFu)";
}
return (control.NegateMask & (1u << sourceIndex)) != 0
? $"(({value}) ^ 0x80000000u)"
: value;
}
/// <summary>64-bit source: SGPR/VGPR pair, sign-extended inline, or zero-extended 32-bit.</summary>
private string RawSource64(Gen5ShaderInstruction instruction, int sourceIndex)
{

View File

@ -117,8 +117,15 @@ public static partial class Gen5SpirvTranslator
{
var oldValue = LoadV(destination);
var sourceValue = GetRawSource(instruction, 0);
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1));
if (_laneSpillSlots.Count != 0)
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(LaneSelectMask));
if (TryGetConstantLane(instruction, out var constantLane))
{
if (_laneSpillSlots.TryGetValue((destination, constantLane), out var slot))
{
Store(slot, sourceValue);
}
}
else if (_laneSpillSlots.Count != 0)
{
foreach (var ((slotRegister, slotLane), variable) in _laneSpillSlots)
{
@ -157,8 +164,8 @@ public static partial class Gen5SpirvTranslator
SpirvOp.Select,
_uintType,
condition,
GetRawSource(instruction, 1),
GetRawSource(instruction, 0));
GetSignModifiedSource(instruction, 1),
GetSignModifiedSource(instruction, 0));
break;
}
case "VCvtU32F32":
@ -190,7 +197,7 @@ public static partial class Gen5SpirvTranslator
var source = GetFloatSource(instruction, 0);
if (instruction.Opcode == "VCvtRpiI32F32")
{
source = Ext(9, _floatType, source);
source = Ext(8, _floatType, _module.AddInstruction(SpirvOp.FAdd, _floatType, source, Float(0.5f)));
}
else if (instruction.Opcode == "VCvtFlrI32F32")
{
@ -421,7 +428,7 @@ public static partial class Gen5SpirvTranslator
var converted = _module.AddInstruction(
SpirvOp.ConvertFToU,
_uintType,
GetFloatSource(instruction, 0));
Ext(81, _floatType, GetFloatSource(instruction, 0), Float(0), Float(255)));
var offset = ShiftLeftLogical(
BitwiseAnd(GetRawSource(instruction, 1), UInt(3)),
UInt(3));
@ -435,6 +442,15 @@ public static partial class Gen5SpirvTranslator
break;
}
case "VRcpF32":
result = EmitFloatResult(
instruction,
_module.AddInstruction(
SpirvOp.FDiv,
_floatType,
Float(1),
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VRcpIflagF32":
result = EmitFloatResult(
instruction,
@ -447,7 +463,11 @@ public static partial class Gen5SpirvTranslator
case "VLogF32":
result = EmitFloatResult(
instruction,
Ext(30, _floatType, GetFloatSource(instruction, 0)));
Ext(
30,
_floatType,
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VLdexpF32":
result = EmitFloatResult(
@ -461,12 +481,20 @@ public static partial class Gen5SpirvTranslator
case "VExpF32":
result = EmitFloatResult(
instruction,
Ext(29, _floatType, GetFloatSource(instruction, 0)));
Ext(
29,
_floatType,
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VRsqF32":
result = EmitFloatResult(
instruction,
Ext(32, _floatType, GetFloatSource(instruction, 0)));
Ext(
32,
_floatType,
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VRcpF16":
result = EmitFloat16Result(instruction, destination, _module.AddInstruction(SpirvOp.FDiv, _floatType, Float(1), GetFloat16Source(instruction, 0)));
@ -535,7 +563,11 @@ public static partial class Gen5SpirvTranslator
case "VSqrtF32":
result = EmitFloatResult(
instruction,
Ext(31, _floatType, GetFloatSource(instruction, 0)));
Ext(
31,
_floatType,
EmitFlushF32DenormToSignedZero(
GetFloatSource(instruction, 0))));
break;
case "VSinF32":
result = EmitFloatResult(
@ -546,7 +578,9 @@ public static partial class Gen5SpirvTranslator
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
GetFloatSource(instruction, 0),
EmitTrigCycleF32(
GetFloatSource(instruction, 0),
preserveSignedZero: true),
Float(MathF.Tau))));
break;
case "VCosF32":
@ -558,7 +592,9 @@ public static partial class Gen5SpirvTranslator
_module.AddInstruction(
SpirvOp.FMul,
_floatType,
GetFloatSource(instruction, 0),
EmitTrigCycleF32(
GetFloatSource(instruction, 0),
preserveSignedZero: false),
Float(MathF.Tau))));
break;
case "VAddF32":
@ -678,9 +714,9 @@ public static partial class Gen5SpirvTranslator
var c = GetFloat16Source(instruction, 2);
var value = instruction.Opcode switch
{
"VMin3F16" => Ext(37, _floatType, Ext(37, _floatType, a, b), c),
"VMax3F16" => Ext(40, _floatType, Ext(40, _floatType, a, b), c),
_ => Ext(40, _floatType, Ext(37, _floatType, a, b), Ext(37, _floatType, Ext(40, _floatType, a, b), c)),
"VMin3F16" => NanIgnoringMinMax(37, NanIgnoringMinMax(37, a, b), c),
"VMax3F16" => NanIgnoringMinMax(40, NanIgnoringMinMax(40, a, b), c),
_ => EmitFloatMedian(a, b, c),
};
result = EmitFloat16Result(instruction, destination, value);
break;
@ -1219,18 +1255,9 @@ public static partial class Gen5SpirvTranslator
}
case "VMed3F32":
{
var left = GetFloatSource(instruction, 0);
var middle = GetFloatSource(instruction, 1);
var right = GetFloatSource(instruction, 2);
var low = Ext(37, _floatType, left, middle);
var high = Ext(40, _floatType, left, middle);
result = EmitFloatResult(
instruction,
Ext(
40,
_floatType,
low,
Ext(37, _floatType, high, right)));
EmitFloatMedian(GetFloatSource(instruction, 0), GetFloatSource(instruction, 1), GetFloatSource(instruction, 2)));
break;
}
case "VCubeidF32":
@ -3879,6 +3906,24 @@ public static partial class Gen5SpirvTranslator
return value;
}
private uint GetSignModifiedSource(Gen5ShaderInstruction instruction, int sourceIndex)
{
var value = GetRawSource(instruction, sourceIndex);
if (instruction.Control is not Gen5Vop3Control control)
{
return value;
}
if ((control.AbsoluteMask & (1u << sourceIndex)) != 0)
{
value = BitwiseAnd(value, UInt(0x7FFFFFFFu));
}
return (control.NegateMask & (1u << sourceIndex)) != 0
? BitwiseXor(value, UInt(0x80000000u))
: value;
}
private uint ApplyDpp8Source(Gen5Dpp8Control control, uint value)
{
var lane = GuestWaveLane();
@ -4451,7 +4496,7 @@ public static partial class Gen5SpirvTranslator
};
if (clamp)
{
doubleValue = Ext(43, type, doubleValue, Double(0.0), Double(1.0));
doubleValue = Ext(81, type, doubleValue, Double(0.0), Double(1.0));
}
var bits = Bitcast(_ulongType, doubleValue);
@ -4485,9 +4530,8 @@ public static partial class Gen5SpirvTranslator
EmitFloat16Result(
instruction,
destination,
Ext(
NanIgnoringMinMax(
operation,
_floatType,
GetFloat16Source(instruction, 0),
GetFloat16Source(instruction, 1)));
@ -4515,6 +4559,19 @@ public static partial class Gen5SpirvTranslator
GetFloatSource(instruction, 0),
GetFloatSource(instruction, 1)));
private uint EmitFloatMedian(uint a, uint b, uint c)
{
var low = NanIgnoringMinMax(37, a, b);
var high = NanIgnoringMinMax(40, a, b);
var median = NanIgnoringMinMax(40, low, NanIgnoringMinMax(37, high, c));
var anyNan = _module.AddInstruction(
SpirvOp.LogicalOr,
_boolType,
IsNanBits(a),
_module.AddInstruction(SpirvOp.LogicalOr, _boolType, IsNanBits(b), IsNanBits(c)));
return _module.AddInstruction(SpirvOp.Select, _floatType, anyNan, NanIgnoringMinMax(37, low, c), median);
}
private uint EmitFloatTernaryExt(
Gen5ShaderInstruction instruction,
uint operation)
@ -4979,7 +5036,7 @@ public static partial class Gen5SpirvTranslator
var destination = instruction.Destinations[0].Value;
var sourceValue = GetRawSource(instruction, 0);
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(_waveLaneCount - 1));
var selectedLane = BitwiseAnd(GetRawSource(instruction, 1), UInt(LaneSelectMask));
if (_emulateWave64)
{
@ -4987,7 +5044,7 @@ public static partial class Gen5SpirvTranslator
// Read it even when the guest execution mask disables that lane.
StoreS(destination, BroadcastWave64Lane(sourceValue, selectedLane));
}
else if (_subgroupInvocationIdInput != 0)
else if (_subgroupInvocationIdInput != 0 && _stage == Gen5SpirvStage.Compute)
{
var broadcast = _module.AddInstruction(
SpirvOp.GroupNonUniformBroadcast,
@ -4995,18 +5052,13 @@ public static partial class Gen5SpirvTranslator
UInt(3),
sourceValue,
selectedLane);
StoreS(destination, broadcast);
StoreS(destination, ReadLaneSpillSlot(instruction, selectedLane, broadcast));
}
else
{
// Fallback: no subgroup ops, read current lane's value, or the
// spill slot that V_WRITELANE filled for the selected lane.
if (instruction.Sources[0].Kind == Gen5OperandKind.VectorRegister)
{
sourceValue = SelectLaneSpillSlot(instruction.Sources[0].Value, selectedLane, sourceValue);
}
StoreS(destination, sourceValue);
StoreS(destination, ReadLaneSpillSlot(instruction, selectedLane, sourceValue));
}
return true;
@ -5129,7 +5181,7 @@ public static partial class Gen5SpirvTranslator
};
if (control?.Clamp == true)
{
value = Ext(43, _floatType, value, Float(0), Float(1));
value = Ext(81, _floatType, value, Float(0), Float(1));
}
var half = EmitFloatToHalf(Bitcast(_uintType, value));
@ -5170,7 +5222,7 @@ public static partial class Gen5SpirvTranslator
};
if (clamp)
{
value = Ext(43, _floatType, value, Float(0), Float(1));
value = Ext(81, _floatType, value, Float(0), Float(1));
}
return Bitcast(_uintType, value);

View File

@ -0,0 +1,91 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.ShaderCompiler.Vulkan;
public static partial class Gen5SpirvTranslator
{
private sealed partial class CompilationContext
{
private uint EmitFlushF32DenormToSignedZero(uint value)
{
var bits = Bitcast(_uintType, value);
var absolute = _module.AddInstruction(
SpirvOp.BitwiseAnd,
_uintType,
bits,
UInt(0x7FFF_FFFF));
var sign = _module.AddInstruction(
SpirvOp.BitwiseAnd,
_uintType,
bits,
UInt(0x8000_0000));
var nonZero = IsNotZero(absolute);
var subnormal = _module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
absolute,
UInt(0x0080_0000));
var flush = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
nonZero,
subnormal);
var selected = _module.AddInstruction(
SpirvOp.Select,
_uintType,
flush,
sign,
bits);
return Bitcast(_floatType, selected);
}
private uint EmitTrigCycleF32(uint source, bool preserveSignedZero)
{
var fraction = Ext(10, _floatType, source);
var bits = Bitcast(_uintType, source);
var absolute = _module.AddInstruction(
SpirvOp.BitwiseAnd,
_uintType,
bits,
UInt(0x7FFF_FFFF));
var large = _module.AddInstruction(
SpirvOp.UGreaterThanEqual,
_boolType,
absolute,
UInt(0x4B00_0000));
var finite = _module.AddInstruction(
SpirvOp.ULessThan,
_boolType,
absolute,
UInt(0x7F80_0000));
var largeFinite = _module.AddInstruction(
SpirvOp.LogicalAnd,
_boolType,
large,
finite);
var reduced = _module.AddInstruction(
SpirvOp.Select,
_floatType,
largeFinite,
Float(0),
fraction);
if (!preserveSignedZero)
{
return reduced;
}
var zero = _module.AddInstruction(
SpirvOp.IEqual,
_boolType,
absolute,
UInt(0));
return _module.AddInstruction(
SpirvOp.Select,
_floatType,
zero,
source,
reduced);
}
}
}

View File

@ -185,6 +185,7 @@ public static partial class Gen5SpirvTranslator
private uint _layerOutput;
private uint _viewportIndexOutput;
private uint _clipDistanceCount;
private uint _invalidPositionClipDistance = uint.MaxValue;
private uint _cullDistanceCount;
private uint _vertexIndexInput;
private uint _instanceIndexInput;
@ -517,6 +518,13 @@ public static partial class Gen5SpirvTranslator
_ => SpirvExecutionModel.GLCompute,
};
_module.AddEntryPoint(model, main, "main", _interfaces);
if (_request.ShaderSignedZeroInfNanPreserveFloat32Supported)
{
_module.AddExecutionMode(
main,
SpirvExecutionMode.SignedZeroInfNanPreserve,
32);
}
if (_stage == Gen5SpirvStage.Pixel)
{
_module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
@ -554,6 +562,11 @@ public static partial class Gen5SpirvTranslator
private void DeclareModule()
{
_module.AddCapability(SpirvCapability.Shader);
if (_request.ShaderSignedZeroInfNanPreserveFloat32Supported)
{
_module.AddExtension("SPV_KHR_float_controls");
_module.AddCapability(SpirvCapability.SignedZeroInfNanPreserve);
}
_module.AddCapability(SpirvCapability.Int64);
_module.AddCapability(SpirvCapability.ImageQuery);
if (UsesSubgroupOperations())
@ -1398,6 +1411,11 @@ public static partial class Gen5SpirvTranslator
_floatType,
fragCoord,
component);
if (component == 3)
{
value = _module.AddInstruction(SpirvOp.FDiv, _floatType, Float(1f), value);
}
StoreV(vgpr, Bitcast(_uintType, value), guardWithExec: false);
}
@ -6583,6 +6601,20 @@ public static partial class Gen5SpirvTranslator
outputValue,
Load(_vec4Type, outputVariable));
Store(outputVariable, outputValue);
if (export.Target == 12 && _invalidPositionClipDistance != uint.MaxValue)
{
var equal = _module.AddInstruction(
SpirvOp.FOrdEqual,
_module.TypeVector(_boolType, 4),
outputValue,
_module.ConstantNull(_vec4Type));
var invalid = _module.AddInstruction(SpirvOp.All, _boolType, equal);
// A zero position has an undefined perspective divide. Collapse its primitive
// to the remaining edge, as the guest's clipping-error cull does.
var distance = _module.AddInstruction(
SpirvOp.Select, _floatType, invalid, Float(-1f), Float(0f));
StoreDistanceConditional(_clipDistanceOutput, _invalidPositionClipDistance, distance);
}
return true;
}
@ -6691,6 +6723,14 @@ public static partial class Gen5SpirvTranslator
}
}
if (_request.SupportsClipDistance && clipCount + cullCount < 8 &&
_request.Program.Instructions.Any(static instruction =>
instruction.Control is Gen5ExportControl { Target: 12, EnableMask: not 0 }))
{
// Use a separate plane so auxiliary position exports keep their own distances.
_invalidPositionClipDistance = clipCount++;
}
if (needPointSize)
{
_pointSizeOutput = DeclareBuiltInOutput(
@ -7436,11 +7476,12 @@ public static partial class Gen5SpirvTranslator
private List<(uint Register, uint Lane)> FindLaneSpillSlots()
{
var slots = new List<(uint Register, uint Lane)>();
if (UsesSubgroupOperations())
if (_emulateWave64)
{
return slots;
}
var ownsLaneZero = !UsesSubgroupOperations();
var readRegisters = _request.Program.Instructions
.Where(static instruction => instruction.Opcode == "VReadlaneB32" &&
instruction.Sources.Count > 0 &&
@ -7453,7 +7494,7 @@ public static partial class Gen5SpirvTranslator
TryGetVectorDestination(instruction, out var register) &&
readRegisters.Contains(register) &&
TryGetConstantLane(instruction, out var lane) &&
lane != 0 &&
(lane != 0 || !ownsLaneZero) &&
!slots.Contains((register, lane)))
{
slots.Add((register, lane));
@ -7484,10 +7525,28 @@ public static partial class Gen5SpirvTranslator
return false;
}
lane = value & (_waveLaneCount - 1);
lane = value & LaneSelectMask;
return true;
}
private uint LaneSelectMask => _stage == Gen5SpirvStage.Compute ? _waveLaneCount - 1 : 63u;
private uint ReadLaneSpillSlot(Gen5ShaderInstruction instruction, uint selectedLane, uint value)
{
if (instruction.Sources[0].Kind != Gen5OperandKind.VectorRegister)
{
return value;
}
var register = instruction.Sources[0].Value;
if (TryGetConstantLane(instruction, out var lane))
{
return _laneSpillSlots.TryGetValue((register, lane), out var slot) ? Load(_uintType, slot) : value;
}
return SelectLaneSpillSlot(register, selectedLane, value);
}
// Folds the spill slots of register into value: selected lane == slot lane reads the slot.
private uint SelectLaneSpillSlot(uint register, uint selectedLane, uint value)
{

View File

@ -213,6 +213,7 @@ public enum SpirvCapability : uint
ShaderViewportIndexLayerExt = 5254,
RuntimeDescriptorArray = 5302,
PhysicalStorageBufferAddresses = 5347,
SignedZeroInfNanPreserve = 4466,
}
public enum SpirvStorageClass : uint
@ -242,6 +243,7 @@ public enum SpirvExecutionMode : uint
OriginUpperLeft = 7,
DepthReplacing = 12,
LocalSize = 17,
SignedZeroInfNanPreserve = 4461,
}
public enum SpirvDecoration : uint

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