Compare commits

..

64 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
188 changed files with 6349 additions and 5065 deletions

View File

@ -1,32 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Rendering resolution
Options → Rendering exposes two independent sizes, globally and per game:
- **Resolution** sets the host window size or exclusive fullscreen mode.
- **Game display mode** reports a 1080p display to the game by default. Select
3840×2160 to request a 4K display. A registered presentation buffer cannot
change this setting.
The CLI equivalents are `--resolution=1280x720` and
`--guest-resolution=1920x1080` (or `3840x2160`). For example, a 1080p game display
can be presented in a 720p window; changing the window alone does not reduce the
work the game renders.
The game chooses its internal render targets. Some games adapt those targets to
the reported display mode; others render at a fixed resolution and may still
register 4K buffers with a 1080p display. Silent Hill: The Short Message does so
in current testing. The size of that presentation buffer alone also does not
establish the sizes of its intermediate scene targets.
Arbitrary internal resolution scaling is not implemented. The previous
`RenderResolutionScale` setting and its `SHARPEMU_RENDER_SCALE` export had no
renderer consumer; they have been removed. They did not change rendering work.
A correct implementation must preserve guest memory layouts while scaling host
attachments, viewports, scissors, image fetches, compute accesses, aliases and
readbacks consistently. Reducing only the output image or window is not an
internal-resolution implementation.

View File

@ -45,7 +45,6 @@ internal static partial class Program
[STAThread]
private static int Main(string[] args)
{
RosettaGcBudget.ReexecIfNeeded(args);
ConfigureManagedPluginResolution();
if (args.Length > 0 && args[0] == SharpEmu.Core.Diagnostics.WindowsCrashCapture.HelperArgument)
@ -1011,7 +1010,7 @@ internal static partial class Program
private static void PrintUsage()
{
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--guest-resolution=<1920x1080|3840x2160>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
Log.Info("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
}
@ -1076,7 +1075,6 @@ internal static partial class Program
var systemLanguage = 1;
HostWindowMode? windowModeOverride = null;
HostScalingMode? scalingModeOverride = null;
GuestDisplayResolution? guestResolutionOverride = null;
int? windowWidthOverride = null;
int? windowHeightOverride = null;
int? displayIndexOverride = null;
@ -1136,18 +1134,6 @@ internal static partial class Program
windowHeightOverride = windowHeight;
continue;
}
if (TrySplitOption(argument, "--guest-resolution", out var guestResolutionText))
{
if (guestResolutionText is not ("1920x1080" or "3840x2160"))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
guestResolutionOverride = guestResolutionText == "3840x2160"
? GuestDisplayResolution.UltraHd : GuestDisplayResolution.Hd;
continue;
}
if (TrySplitOption(argument, "--display", out var displayText))
{
if (!int.TryParse(displayText, out var displayIndex) || displayIndex < 0)
@ -1405,7 +1391,6 @@ internal static partial class Program
{
WindowMode = windowModeOverride ?? configuredVideoOptions.WindowMode,
ScalingMode = scalingModeOverride ?? configuredVideoOptions.ScalingMode,
GuestResolution = guestResolutionOverride ?? configuredVideoOptions.GuestResolution,
Width = windowWidthOverride ?? configuredVideoOptions.Width,
Height = windowHeightOverride ?? configuredVideoOptions.Height,
DisplayIndex = displayIndexOverride ?? configuredVideoOptions.DisplayIndex,
@ -1446,8 +1431,6 @@ internal static partial class Program
{
WindowMode = windowMode,
ScalingMode = scalingMode,
GuestResolution = effective.GuestResolution == "3840x2160"
? GuestDisplayResolution.UltraHd : GuestDisplayResolution.Hd,
Width = hasResolution ? configuredWidth : defaults.Width,
Height = hasResolution ? configuredHeight : defaults.Height,
DisplayIndex = effective.DisplayIndex,

View File

@ -1,100 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections;
using System.Runtime.InteropServices;
namespace SharpEmu.CLI;
/// <summary>
/// Under Rosetta 2 every garbage collection suspends the runtime with
/// FlushProcessWriteBuffers, which reads each thread's registers through a Mach call that
/// Rosetta services by parking the thread in its runtime. A signal that reaches a parked
/// thread (guest memory tracking raises thousands per second) trips Rosetta's "expected saved
/// LR to be in translated code" assertion and wedges the process. With the default gen0 budget
/// the emulator collects about twice a second; a 64 MiB budget makes that rare.
/// </summary>
/// <remarks>
/// The GC reads its gen0 budget only from the environment when the runtime starts, so the
/// process re-executes itself once with the variable set. execve keeps the process id.
/// </remarks>
internal static class RosettaGcBudget
{
private const string BudgetVariable = "DOTNET_GCgen0size";
// 64 MiB: a larger budget turns every collection into a blocking gen1/gen2 pass of
// about a second (512 MiB measured 8 such pauses in 150 s of Demon's Souls).
private const string Budget = "0x4000000";
private const string OptOutVariable = "SHARPEMU_ROSETTA_GC_BUDGET";
public static void ReexecIfNeeded(string[] args)
{
if (!OperatingSystem.IsMacOS() ||
RuntimeInformation.ProcessArchitecture != Architecture.X64 ||
Environment.GetEnvironmentVariable(BudgetVariable) is not null ||
Environment.GetEnvironmentVariable(OptOutVariable) == "0" ||
!IsTranslated())
{
return;
}
var path = Environment.ProcessPath;
if (string.IsNullOrEmpty(path))
{
return;
}
var environment = new List<string>();
foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
{
environment.Add($"{entry.Key}={entry.Value}");
}
environment.Add($"{BudgetVariable}={Budget}");
var argv = BuildArguments(path, Environment.GetCommandLineArgs()[0], args);
var nativeArguments = ToNullTerminated(argv);
var nativeEnvironment = ToNullTerminated(environment);
// Only returns on failure; the emulator then runs with the default budget.
_ = Execve(path, nativeArguments, nativeEnvironment);
var error = Marshal.GetLastPInvokeError();
foreach (var pointer in nativeArguments) Marshal.FreeCoTaskMem(pointer);
foreach (var pointer in nativeEnvironment) Marshal.FreeCoTaskMem(pointer);
Console.Error.WriteLine(
$"[LOADER][WARN] Could not restart with a larger GC budget under Rosetta: errno={error}");
}
internal static string[] BuildArguments(string executable, string entryAssembly, string[] args) =>
Path.GetFileNameWithoutExtension(executable).Equals("dotnet", StringComparison.OrdinalIgnoreCase)
? [executable, entryAssembly, .. args]
: [executable, .. args];
private static bool IsTranslated()
{
var value = 0;
var size = (nint)sizeof(int);
return SysctlByName("sysctl.proc_translated", ref value, ref size, 0, 0) == 0 && value == 1;
}
private static nint[] ToNullTerminated(IReadOnlyList<string> values)
{
var pointers = new nint[values.Count + 1];
for (var i = 0; i < values.Count; i++)
{
pointers[i] = Marshal.StringToCoTaskMemUTF8(values[i]);
}
return pointers;
}
[DllImport("libc", EntryPoint = "execve", SetLastError = true)]
private static extern int Execve(
[MarshalAs(UnmanagedType.LPUTF8Str)] string path,
nint[] argv,
nint[] envp);
[DllImport("libc", EntryPoint = "sysctlbyname")]
private static extern int SysctlByName(
[MarshalAs(UnmanagedType.LPUTF8Str)] string name,
ref int value,
ref nint size,
nint newValue,
nint newSize);
}

View File

@ -261,40 +261,23 @@ public sealed unsafe partial class DirectExecutionBackend
return code;
}
// SHARPEMU_MEMCPY_MANAGED_MIN overrides the size from which guest memcpy takes the managed path.
internal static readonly int ManagedCopyMinimumBytes =
int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_MEMCPY_MANAGED_MIN"), out var minimum) && minimum > 0 ? minimum : 4096;
/// <summary>
/// memcpy/memmove(dst, src, n) as a forward <c>rep movsb</c>, returning dst. The libc
/// export copies with memmove semantics, so a destination that starts inside the source
/// (where a forward copy would read bytes it already overwrote) takes the managed path.
/// So does a copy of at least <see cref="ManagedCopyMinimumBytes"/>: the native copy takes
/// one write-tracking fault per GPU-watched page it writes, a signal round trip of about
/// 0.2 ms under Rosetta, while the managed copy marks the whole destination written first.
/// Only RAX, RCX, RSI, RDI and the flags change - all caller-saved - and the direction
/// flag is clear on entry per the SysV ABI.
/// </summary>
internal static List<byte> EmitGuestMemoryCopyStub(nint fallbackTrampoline)
{
var code = new List<byte>(48);
var slowPathFixups = new List<int>();
var code = new List<byte>(40);
// mov rcx, rdi / sub rcx, rsi / cmp rcx, rdx / jb slow: dst - src < n (unsigned) is
// exactly a destination in (src, src + n) or equal to src with a non-empty copy.
code.AddRange([0x48, 0x89, 0xF9, 0x48, 0x29, 0xF1, 0x48, 0x39, 0xD1, 0x72, 0x00]);
slowPathFixups.Add(code.Count - 1);
// cmp rdx, imm32 / jae slow
code.AddRange([0x48, 0x81, 0xFA]);
code.AddRange(BitConverter.GetBytes(ManagedCopyMinimumBytes));
code.AddRange([0x73, 0x00]);
slowPathFixups.Add(code.Count - 1);
var slowPathFixup = code.Count - 1;
// mov rax, rdi / mov rcx, rdx / rep movsb / ret
code.AddRange([0x48, 0x89, 0xF8, 0x48, 0x89, 0xD1, 0xF3, 0xA4, 0xC3]);
foreach (var fixup in slowPathFixups)
{
code[fixup] = checked((byte)(code.Count - (fixup + 1)));
}
code[slowPathFixup] = checked((byte)(code.Count - (slowPathFixup + 1)));
// jmp qword [rip+0] with the absolute trampoline address after it.
code.AddRange([0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));

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,11 +3982,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
public bool SupportsGuestContextTransfer => true;
// The handle and context this host thread registered last; a repeat registration (every
// scePthreadSelf on an external guest thread, tens of thousands a second) then skips the
// scheduler gate.
[ThreadStatic] private static ulong _registeredExternalHandle;
[ThreadStatic] private static CpuContext? _registeredExternalContext;
public bool TryGetGuestThreadStackBounds(
ulong threadHandle,
out ulong stackBase,
@ -4017,12 +4016,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return;
}
if (_registeredExternalHandle == threadHandle && ReferenceEquals(_registeredExternalContext, context))
{
Volatile.Write(ref _currentExternalGuestThreadHandle, threadHandle);
return;
}
lock (_guestThreadGate)
{
_currentExternalGuestThreadHandle = threadHandle;
@ -4036,19 +4029,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
existing.Context = context;
Volatile.Write(ref existing.HostThreadId, hostThreadId);
}
else
{
_externalGuestThreads[threadHandle] = new ExternalGuestThreadState
{
Context = context,
Name = $"External-{threadHandle:X}",
HostThreadId = hostThreadId,
};
return;
}
_registeredExternalHandle = threadHandle;
_registeredExternalContext = context;
_externalGuestThreads[threadHandle] = new ExternalGuestThreadState
{
Context = context,
Name = $"External-{threadHandle:X}",
HostThreadId = hostThreadId,
};
}
}
@ -5386,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,
@ -5397,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]);
@ -5420,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);
@ -6397,15 +6383,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return GuestNativeCallExitReason.Exception;
}
const uint stubSize = 512u;
// The stub page and host-RSP slot are kept per host thread and nesting depth: allocating
// and releasing them on every resume cost an mmap/munmap pair and two acquisitions of the
// host-memory gate, which the GPU write tracker holds across its own protection changes.
var depth = _continuationDepth;
if (!TryAcquireContinuationStub(depth, stubSize, out var ptr, out var hostRspStorage, out reason))
void* ptr = VirtualAlloc(null, stubSize, 12288u, 4u);
if (ptr == null)
{
reason = "failed to allocate executable memory for guest thread stub";
return GuestNativeCallExitReason.Exception;
}
void* hostRspStorage = NativeMemory.AllocZeroed((nuint)HostStackStateBytes);
if (hostRspStorage == null)
{
VirtualFree(ptr, 0u, 32768u);
reason = "failed to allocate writable host-RSP storage for guest continuation stub";
return GuestNativeCallExitReason.Exception;
}
_continuationDepth = depth + 1;
var previousActiveBackend = _activeExecutionBackend;
var previousActiveContext = _activeCpuContext;
var previousSentinel = _activeEntryReturnSentinelRip;
@ -6479,7 +6469,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
reason = $"failed to patch guest continuation return slot at 0x{returnSlotAddress:X16}";
return GuestNativeCallExitReason.Exception;
}
if (!ProtectContinuationStub(ptr, stubSize, executable: true))
uint oldProtect = default(uint);
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
{
reason = "failed to seal guest continuation stub execute-read";
return GuestNativeCallExitReason.Exception;
@ -6539,85 +6530,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
previousForcedExit,
previousYieldRequested,
previousYieldReason);
_continuationDepth = depth;
NativeMemory.Free(hostRspStorage);
VirtualFree(ptr, 0u, 32768u);
}
}
[ThreadStatic] private static nint[]? _continuationStubPages;
[ThreadStatic] private static nint[]? _continuationHostRspSlots;
[ThreadStatic] private static int _continuationDepth;
// Returns this thread's stub page for the given nesting depth, writable and with a zeroed
// host-RSP slot; the first use at a depth allocates them, and they live as long as the thread.
private unsafe bool TryAcquireContinuationStub(int depth, uint stubSize, out void* stub, out void* hostRspStorage, out string? reason)
{
reason = null;
stub = null;
hostRspStorage = null;
var pages = _continuationStubPages ??= new nint[4];
var slots = _continuationHostRspSlots ??= new nint[4];
if (depth >= pages.Length)
{
Array.Resize(ref pages, depth + 2);
Array.Resize(ref slots, depth + 2);
_continuationStubPages = pages;
_continuationHostRspSlots = slots;
}
if (pages[depth] == 0)
{
var page = VirtualAlloc(null, stubSize, 12288u, 4u);
if (page == null)
{
reason = "failed to allocate executable memory for guest thread stub";
return false;
}
var slot = NativeMemory.AllocZeroed((nuint)HostStackStateBytes);
if (slot == null)
{
VirtualFree(page, 0u, 32768u);
reason = "failed to allocate writable host-RSP storage for guest continuation stub";
return false;
}
pages[depth] = (nint)page;
slots[depth] = (nint)slot;
}
else
{
if (!ProtectContinuationStub((void*)pages[depth], stubSize, executable: false))
{
reason = "failed to reopen guest continuation stub for writing";
return false;
}
NativeMemory.Clear((void*)slots[depth], (nuint)HostStackStateBytes);
}
stub = (void*)pages[depth];
hostRspStorage = (void*)slots[depth];
return true;
}
// The stub page is emulator-private, so on POSIX its protection changes bypass the tracked
// host-memory layer and its gate.
private static unsafe bool ProtectContinuationStub(void* stub, uint stubSize, bool executable)
{
if (OperatingSystem.IsWindows())
{
uint oldProtect = 0;
return VirtualProtect(stub, stubSize, executable ? 32u : 4u, &oldProtect);
}
var pageSize = (nint)Environment.SystemPageSize;
var page = (nint)stub & ~(pageSize - 1);
return PosixMprotect(page, (nuint)pageSize, executable ? 1 | 4 : 1 | 2) == 0;
}
[DllImport("libc", EntryPoint = "mprotect")]
private static extern int PosixMprotect(nint address, nuint length, int protection);
// The continuation trampoline is rebuilt on every blocked-thread resume.
// Keep its tiny writer on the stack: capturing local emit functions create a
// managed display-class allocation on this extremely hot path.

View File

@ -322,43 +322,20 @@ internal static unsafe class PosixHostStubs
private static nint EmitSleep(byte* page, ref int offset)
{
// void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep
// would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the
// runtime's thread activations, for one) cuts either short: a sleep that returned at once
// on an ignored error turned UE's idle HTTP thread into a hot spin.
var nanosleep = ResolveLibcExport("nanosleep");
var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location");
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
var usleep = ResolveLibcExport("usleep");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x53); // push rbx
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16]
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx
Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000
Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec)
Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000
Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec)
// loop:
Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp
Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16]
EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
Emit(page, ref offset, 0x76, 0x05); // jbe +5
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x85, 0xC0); // test eax, eax
Emit(page, ref offset, 0x74, 0x26); // jz done
EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR
Emit(page, ref offset, 0x75, 0x15); // jne done
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16]
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24]
Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax
Emit(page, ref offset, 0xEB, 0xC2); // jmp loop
// done:
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20
Emit(page, ref offset, 0x5B); // pop rbx
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xC3); // ret

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

@ -70,11 +70,6 @@ public sealed class GuiSettings
public string Resolution { get; set; } = "1920x1080";
public string GuestResolution { get; set; } = "1920x1080";
internal static string NormalizeGuestResolution(string? value) =>
string.Equals(value, "3840x2160", StringComparison.OrdinalIgnoreCase) ? "3840x2160" : "1920x1080";
public int DisplayIndex { get; set; }
public int RefreshRate { get; set; }
@ -94,6 +89,9 @@ public sealed class GuiSettings
public string BinkPlaybackMode { get; set; } = "Guest";
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
public double RenderResolutionScale { get; set; } = 1.0;
/// <summary>
/// Discord application ID used for Rich Presence; the default is the
/// SharpEmu application. Override to rebrand what Discord shows as
@ -186,7 +184,6 @@ public sealed class GuiSettings
"Ukrainian");
settings.WindowMode = NormalizeChoice(settings.WindowMode, "Windowed", "Borderless", "Exclusive");
settings.Resolution = NormalizeResolution(settings.Resolution);
settings.GuestResolution = NormalizeGuestResolution(settings.GuestResolution);
settings.ScalingMode = NormalizeChoice(settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
settings.HdrMode = NormalizeChoice(settings.HdrMode, "Auto", "On", "Off");
settings.BinkPlaybackMode = NormalizeChoice(settings.BinkPlaybackMode, "Guest", "Host", "Skip");

View File

@ -219,8 +219,6 @@
"Options.WindowMode.Exclusive": "حصري",
"Options.Resolution.Label": "الدقة",
"Options.Resolution.Desc": "حجم النافذة الأولي أو دقة ملء الشاشة الحصرية.",
"Options.GuestResolution.Label": "وضع عرض اللعبة",
"Options.GuestResolution.Desc": "دقة العرض المُبلّغ عنها للعبة. الافتراضي 1080p. تختار الألعاب دقة التصيير الداخلية؛ وقد تتجاهل الألعاب ذات الدقة الثابتة هذا الإعداد.",
"Options.Display.Label": "الشاشة",
"Options.Display.Desc": "الشاشة المستخدمة للتوسيط وملء الشاشة.",
"Options.RefreshRate.Label": "معدل التحديث",

View File

@ -74,8 +74,6 @@
"Options.WindowMode.Exclusive": "Eksklüziv",
"Options.Resolution.Label": "Təsvir ölçüsü",
"Options.Resolution.Desc": "Pəncərənin başlanğıc ölçüsü və ya eksklüziv tam ekranın təsvir ölçüsü.",
"Options.GuestResolution.Label": "Oyunun ekran rejimi",
"Options.GuestResolution.Desc": "Oyuna bildirilən ekran təsvir ölçüsü. Standart 1080p-dir. Daxili render ölçüsünü oyun seçir; sabit ölçülü oyunlar bu ayarı nəzərə almaya bilər.",
"Options.Display.Label": "Ekran",
"Options.Display.Desc": "Pəncərənin mərkəzə yerləşdirilməsi və tam ekran üçün istifadə olunan monitor.",
"Options.RefreshRate.Label": "Yenilənmə tezliyi",

View File

@ -220,8 +220,6 @@
"Options.WindowMode.Exclusive": "Exclusiva",
"Options.Resolution.Label": "Resolução",
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de tela cheia exclusiva.",
"Options.GuestResolution.Label": "Modo de exibição do jogo",
"Options.GuestResolution.Desc": "Resolução informada ao jogo. O padrão é 1080p. O jogo escolhe a resolução interna de renderização; jogos com resolução fixa podem ignorar esta opção.",
"Options.Display.Label": "Tela",
"Options.Display.Desc": "Monitor usado para centralização e tela cheia.",
"Options.RefreshRate.Label": "Taxa de atualização",

View File

@ -219,8 +219,6 @@
"Options.WindowMode.Exclusive": "Exklusiv",
"Options.Resolution.Label": "Auflösung",
"Options.Resolution.Desc": "Anfängliche Fenstergröße oder exklusive Vollbildauflösung.",
"Options.GuestResolution.Label": "Anzeigemodus des Spiels",
"Options.GuestResolution.Desc": "Dem Spiel gemeldete Bildschirmauflösung. Standard ist 1080p. Spiele wählen ihre interne Renderauflösung selbst; Spiele mit fester Auflösung können diese Einstellung ignorieren.",
"Options.Display.Label": "Anzeige",
"Options.Display.Desc": "Monitor für Zentrierung und Vollbilddarstellung.",
"Options.RefreshRate.Label": "Bildwiederholrate",

View File

@ -219,8 +219,6 @@
"Options.WindowMode.Exclusive": "Eksklusiv",
"Options.Resolution.Label": "Opløsning",
"Options.Resolution.Desc": "Oprindelig vinduesstørrelse eller opløsning i eksklusiv fuldskærm.",
"Options.GuestResolution.Label": "Spillets skærmtilstand",
"Options.GuestResolution.Desc": "Skærmopløsning oplyst til spillet. Standard er 1080p. Spillet vælger den interne renderopløsning; spil med fast opløsning kan ignorere denne indstilling.",
"Options.Display.Label": "Skærm",
"Options.Display.Desc": "Skærm, der bruges til centrering og fuldskærm.",
"Options.RefreshRate.Label": "Opdateringshastighed",

View File

@ -85,8 +85,6 @@
"Options.WindowMode.Exclusive": "Eksklusiivne",
"Options.Resolution.Label": "Eraldusvõime",
"Options.Resolution.Desc": "Algne akna suurus või eksklusiivse täisekraani eraldusvõime.",
"Options.GuestResolution.Label": "Mängu kuvarežiim",
"Options.GuestResolution.Desc": "Mängule teatatud ekraani eraldusvõime. Vaikimisi 1080p. Mäng valib sisemise renderduse eraldusvõime; fikseeritud eraldusvõimega mängud võivad seda sätet eirata.",
"Options.Display.Label": "Kuvar",
"Options.Display.Desc": "Monitor, mida kasutatakse tsentreerimiseks ja täisekraanrežiimiks.",
"Options.RefreshRate.Label": "Värskendussagedus",

View File

@ -85,8 +85,6 @@
"Options.WindowMode.Exclusive": "Exclusive",
"Options.Resolution.Label": "Resolution",
"Options.Resolution.Desc": "Initial window size or exclusive fullscreen resolution.",
"Options.GuestResolution.Label": "Game display mode",
"Options.GuestResolution.Desc": "Display resolution reported to the game. Defaults to 1080p. Games choose their internal rendering resolution; fixed-resolution games may ignore this setting.",
"Options.Display.Label": "Display",
"Options.Display.Desc": "Monitor used for centering and fullscreen.",
"Options.RefreshRate.Label": "Refresh rate",

View File

@ -220,8 +220,6 @@
"Options.WindowMode.Exclusive": "Exclusiva",
"Options.Resolution.Label": "Resolución",
"Options.Resolution.Desc": "Tamaño inicial de la ventana o resolución de pantalla completa exclusiva.",
"Options.GuestResolution.Label": "Modo de pantalla del juego",
"Options.GuestResolution.Desc": "Resolución de pantalla comunicada al juego. El valor predeterminado es 1080p. El juego elige su resolución interna de renderizado; los juegos con resolución fija pueden ignorar este ajuste.",
"Options.Display.Label": "Pantalla",
"Options.Display.Desc": "Monitor utilizado para centrar y mostrar a pantalla completa.",
"Options.RefreshRate.Label": "Frecuencia de actualización",

View File

@ -220,8 +220,6 @@
"Options.WindowMode.Exclusive": "Exclusif",
"Options.Resolution.Label": "Résolution",
"Options.Resolution.Desc": "Taille initiale de la fenêtre ou résolution du plein écran exclusif.",
"Options.GuestResolution.Label": "Mode d’affichage du jeu",
"Options.GuestResolution.Desc": "Résolution d’écran indiquée au jeu. La valeur par défaut est 1080p. Le jeu choisit sa résolution de rendu interne ; les jeux à résolution fixe peuvent ignorer ce réglage.",
"Options.Display.Label": "Écran",
"Options.Display.Desc": "Moniteur utilisé pour le centrage et le plein écran.",
"Options.RefreshRate.Label": "Fréquence de rafraîchissement",

View File

@ -220,8 +220,6 @@
"Options.WindowMode.Exclusive": "Kizárólagos",
"Options.Resolution.Label": "Felbontás",
"Options.Resolution.Desc": "Kezdeti ablakméret vagy kizárólagos teljes képernyős felbontás.",
"Options.GuestResolution.Label": "A játék megjelenítési módja",
"Options.GuestResolution.Desc": "A játéknak jelzett képernyőfelbontás. Alapértelmezés: 1080p. A belső renderelési felbontást a játék választja; a rögzített felbontású játékok figyelmen kívül hagyhatják ezt a beállítást.",
"Options.Display.Label": "Kijelző",
"Options.Display.Desc": "A középre igazításhoz és teljes képernyőhöz használt monitor.",
"Options.RefreshRate.Label": "Frissítési gyakoriság",

View File

@ -224,8 +224,6 @@
"Options.WindowMode.Exclusive": "Esclusiva",
"Options.Resolution.Label": "Risoluzione",
"Options.Resolution.Desc": "Dimensione iniziale della finestra o risoluzione dello schermo intero esclusivo.",
"Options.GuestResolution.Label": "Modalità schermo del gioco",
"Options.GuestResolution.Desc": "Risoluzione dello schermo comunicata al gioco. Il valore predefinito è 1080p. Il gioco sceglie la risoluzione interna di rendering; i giochi con risoluzione fissa possono ignorare questa impostazione.",
"Options.Display.Label": "Schermo",
"Options.Display.Desc": "Monitor utilizzato per il centraggio e lo schermo intero.",
"Options.RefreshRate.Label": "Frequenza di aggiornamento",

View File

@ -219,8 +219,6 @@
"Options.WindowMode.Exclusive": "排他",
"Options.Resolution.Label": "解像度",
"Options.Resolution.Desc": "初期ウィンドウサイズまたは排他フルスクリーンの解像度。",
"Options.GuestResolution.Label": "ゲームの表示モード",
"Options.GuestResolution.Desc": "ゲームに通知する画面解像度。既定値は1080pです。内部描画解像度はゲームが決定するため、解像度が固定されたゲームではこの設定が反映されない場合があります。",
"Options.Display.Label": "ディスプレイ",
"Options.Display.Desc": "中央配置とフルスクリーンに使用するモニター。",
"Options.RefreshRate.Label": "リフレッシュレート",

View File

@ -219,8 +219,6 @@
"Options.WindowMode.Exclusive": "독점",
"Options.Resolution.Label": "해상도",
"Options.Resolution.Desc": "초기 창 크기 또는 독점 전체 화면 해상도.",
"Options.GuestResolution.Label": "게임 디스플레이 모드",
"Options.GuestResolution.Desc": "게임에 전달하는 화면 해상도입니다. 기본값은 1080p입니다. 내부 렌더링 해상도는 게임이 결정하며, 해상도가 고정된 게임은 이 설정을 무시할 수 있습니다.",
"Options.Display.Label": "디스플레이",
"Options.Display.Desc": "가운데 배치와 전체 화면에 사용할 모니터.",
"Options.RefreshRate.Label": "새로 고침 빈도",

View File

@ -219,8 +219,6 @@
"Options.WindowMode.Exclusive": "Exclusief",
"Options.Resolution.Label": "Resolutie",
"Options.Resolution.Desc": "Initiële venstergrootte of resolutie voor exclusief volledig scherm.",
"Options.GuestResolution.Label": "Weergavemodus van het spel",
"Options.GuestResolution.Desc": "Schermresolutie die aan het spel wordt doorgegeven. Standaard 1080p. Het spel kiest de interne renderresolutie; spellen met een vaste resolutie kunnen deze instelling negeren.",
"Options.Display.Label": "Beeldscherm",
"Options.Display.Desc": "Monitor die wordt gebruikt voor centrering en volledig scherm.",
"Options.RefreshRate.Label": "Verversingssnelheid",

View File

@ -85,8 +85,6 @@
"Options.WindowMode.Exclusive": "Wyłączne",
"Options.Resolution.Label": "Rozdzielczość",
"Options.Resolution.Desc": "Początkowy rozmiar okna lub rozdzielczość pełnego ekranu wyłącznego.",
"Options.GuestResolution.Label": "Tryb wyświetlania gry",
"Options.GuestResolution.Desc": "Rozdzielczość ekranu zgłaszana grze. Domyślnie 1080p. Gra wybiera wewnętrzną rozdzielczość renderowania; gry ze stałą rozdzielczością mogą zignorować to ustawienie.",
"Options.Display.Label": "Wyświetlacz",
"Options.Display.Desc": "Monitor używany do centrowania i trybu pełnoekranowego.",
"Options.RefreshRate.Label": "Częstotliwość odświeżania",

View File

@ -220,8 +220,6 @@
"Options.WindowMode.Exclusive": "Exclusivo",
"Options.Resolution.Label": "Resolução",
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de ecrã inteiro exclusivo.",
"Options.GuestResolution.Label": "Modo de apresentação do jogo",
"Options.GuestResolution.Desc": "Resolução do ecrã comunicada ao jogo. A predefinição é 1080p. O jogo escolhe a resolução interna de renderização; jogos com resolução fixa podem ignorar esta opção.",
"Options.Display.Label": "Ecrã",
"Options.Display.Desc": "Monitor utilizado para centrar e apresentar em ecrã inteiro.",
"Options.RefreshRate.Label": "Taxa de atualização",

View File

@ -81,8 +81,6 @@
"Options.WindowMode.Exclusive": "Эксклюзивный",
"Options.Resolution.Label": "Разрешение",
"Options.Resolution.Desc": "Начальный размер окна или разрешение эксклюзивного полноэкранного режима.",
"Options.GuestResolution.Label": "Режим экрана игры",
"Options.GuestResolution.Desc": "Разрешение экрана, сообщаемое игре. По умолчанию 1080p. Игра выбирает внутреннее разрешение рендеринга; игры с фиксированным разрешением могут игнорировать эту настройку.",
"Options.Display.Label": "Монитор",
"Options.Display.Desc": "Монитор, используемый для центрирования окна и полноэкранного режима.",
"Options.RefreshRate.Label": "Частота обновления",

View File

@ -63,8 +63,6 @@
"Options.WindowMode.Desc": "Normal pencere, kenarlıksız masaüstü veya özel tam ekran.",
"Options.Resolution.Label": "Çözünürlük",
"Options.Resolution.Desc": "Başlangıç pencere boyutu veya özel tam ekran çözünürlüğü.",
"Options.GuestResolution.Label": "Oyunun ekran modu",
"Options.GuestResolution.Desc": "Oyuna bildirilen ekran çözünürlüğü. Varsayılan 1080p’dir. Dahili işleme çözünürlüğünü oyun seçer; sabit çözünürlüklü oyunlar bu ayarı yok sayabilir.",
"Options.Display.Label": "Ekran",
"Options.Display.Desc": "Ortalama ve tam ekran için kullanılan monitör.",
"Options.RefreshRate.Label": "Yenileme hızı",

View File

@ -45,7 +45,6 @@ public partial class MainWindow
GameLogLevelBox.ItemsSource = _logLevelChoices;
GameWindowModeBox.ItemsSource = _windowModeChoices;
GameScalingModeBox.ItemsSource = _scalingModeChoices;
GameGuestResolutionBox.ItemsSource = new[] { "1920x1080", "3840x2160" };
GameHdrModeBox.ItemsSource = _hdrModeChoices;
GameOverlayModeBox.ItemsSource = _overlayModeChoices;
GameOverlayCornerBox.ItemsSource = _overlayCornerChoices;
@ -116,7 +115,6 @@ public partial class MainWindow
GameRefreshRateBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
GameScalingModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
GameVSyncToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
GameGuestResolutionBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
GameHdrModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
GameOverlayEnabledToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
GameOverlayModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
@ -218,7 +216,6 @@ public partial class MainWindow
_scalingModeChoices,
effective.ScalingMode,
"Fit");
GameGuestResolutionBox.SelectedItem = effective.GuestResolution;
GameVSyncToggle.IsChecked = effective.VSync;
GameOverlayEnabledToggle.IsChecked = effective.OverlayEnabled;
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "TitleBar");
@ -280,7 +277,6 @@ public partial class MainWindow
LogToFile = GameLogToFileToggle.IsChecked == true,
WindowMode = SelectedComboText(GameWindowModeBox, "Windowed"),
Resolution = SelectedComboText(GameResolutionBox, "1920x1080"),
GuestResolution = SelectedComboText(GameGuestResolutionBox, "1920x1080"),
DisplayIndex = GameDisplayBox.SelectedItem is HostDisplayOption display
? display.Index
: 0,

View File

@ -856,14 +856,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="GameGuestResolutionRow"
Classes="optionRow"
Label="{Binding [Options.GuestResolution.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.GuestResolution.Desc], Source={x:Static local:Localization.Instance}}">
<ComboBox x:Name="GameGuestResolutionBox"
Classes="optionValue"
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="GameDisplayRow"
Classes="optionRow"
Label="{Binding [Options.Display.Label],
@ -1498,12 +1490,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
Classes="optionValue"
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="GuestResolutionRow"
Classes="optionRow"
Label="{Binding [Options.GuestResolution.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.GuestResolution.Desc], Source={x:Static local:Localization.Instance}}">
<ComboBox x:Name="GuestResolutionBox" Classes="optionValue" VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="DisplayRow"
Classes="optionRow"
Label="{Binding [Options.Display.Label], Source={x:Static local:Localization.Instance}}"

View File

@ -322,7 +322,6 @@ public partial class MainWindow : Window
WindowModeBox.SelectionChanged += (_, _) => _settings.WindowMode = SelectedComboText(WindowModeBox, "Windowed");
DisplayBox.SelectionChanged += (_, _) => OnHostDisplayChanged();
ResolutionBox.SelectionChanged += (_, _) => OnHostResolutionChanged();
GuestResolutionBox.SelectionChanged += (_, _) => _settings.GuestResolution = SelectedComboText(GuestResolutionBox, "1920x1080");
RefreshRateBox.SelectionChanged += (_, _) => OnHostRefreshRateChanged();
ScalingModeBox.SelectionChanged += (_, _) => _settings.ScalingMode = SelectedComboText(ScalingModeBox, "Fit");
VSyncToggle.IsCheckedChanged += (_, _) => _settings.VSync = VSyncToggle.IsChecked == true;
@ -1250,7 +1249,6 @@ public partial class MainWindow : Window
LogLevelBox.ItemsSource = _logLevelChoices;
WindowModeBox.ItemsSource = _windowModeChoices;
ScalingModeBox.ItemsSource = _scalingModeChoices;
GuestResolutionBox.ItemsSource = new[] { "1920x1080", "3840x2160" };
HdrModeBox.ItemsSource = _hdrModeChoices;
BinkPlaybackBox.ItemsSource = _binkPlaybackChoices;
OverlayModeBox.ItemsSource = _overlayModeChoices;
@ -1326,7 +1324,6 @@ public partial class MainWindow : Window
LoadHostDisplayOptions();
ScalingModeBox.SelectedIndex = ChoiceIndex(_settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
VSyncToggle.IsChecked = _settings.VSync;
GuestResolutionBox.SelectedItem = GuiSettings.NormalizeGuestResolution(_settings.GuestResolution);
HdrModeBox.SelectedIndex = ChoiceIndex(_settings.HdrMode, "Auto", "On", "Off");
OverlayEnabledToggle.IsChecked = _settings.OverlayEnabled;
OverlayModeBox.SelectedIndex = ChoiceIndex(_settings.OverlayMode, "Full", "Minimal", "TitleBar");
@ -2563,6 +2560,13 @@ public partial class MainWindow : Window
});
_appliedEnvironmentVariables.Add(BinkModeEnvironmentName);
Environment.SetEnvironmentVariable(
"SHARPEMU_RENDER_SCALE",
_settings.RenderResolutionScale.ToString(
"0.###",
System.Globalization.CultureInfo.InvariantCulture));
_appliedEnvironmentVariables.Add("SHARPEMU_RENDER_SCALE");
foreach (var entry in customEnvironment)
{
if (!CustomEnvironmentVariables.TryParseEntry(entry, out var name, out var value))
@ -2780,7 +2784,6 @@ public partial class MainWindow : Window
arguments.Add($"--window-mode={launch.Settings.WindowMode.ToLowerInvariant()}");
arguments.Add($"--resolution={launch.Settings.Resolution}");
arguments.Add($"--guest-resolution={launch.Settings.GuestResolution}");
arguments.Add($"--display={launch.Settings.DisplayIndex}");
arguments.Add($"--refresh-rate={launch.Settings.RefreshRate}");
arguments.Add($"--scaling={launch.Settings.ScalingMode.ToLowerInvariant()}");

View File

@ -26,8 +26,6 @@ public sealed class PerGameSettings
public string? Resolution { get; set; }
public string? GuestResolution { get; set; }
public int? DisplayIndex { get; set; }
public int? RefreshRate { get; set; }
@ -54,7 +52,6 @@ public sealed class PerGameSettings
LogToFile is null &&
WindowMode is null &&
Resolution is null &&
GuestResolution is null &&
DisplayIndex is null &&
RefreshRate is null &&
ScalingMode is null &&
@ -148,11 +145,6 @@ public sealed class PerGameSettings
Resolution = null;
}
if (string.Equals(GuestResolution, global.GuestResolution, StringComparison.OrdinalIgnoreCase))
{
GuestResolution = null;
}
if (DisplayIndex == global.DisplayIndex)
{
DisplayIndex = null;
@ -280,7 +272,6 @@ public sealed record EffectiveLaunchSettings(
bool LogToFile,
string WindowMode,
string Resolution,
string GuestResolution,
int DisplayIndex,
int RefreshRate,
string ScalingMode,
@ -298,7 +289,6 @@ public sealed record EffectiveLaunchSettings(
perGame?.LogToFile ?? global.LogToFile,
perGame?.WindowMode ?? global.WindowMode,
perGame?.Resolution ?? global.Resolution,
GuiSettings.NormalizeGuestResolution(perGame?.GuestResolution ?? global.GuestResolution),
Math.Max(0, perGame?.DisplayIndex ?? global.DisplayIndex),
Math.Clamp(perGame?.RefreshRate ?? global.RefreshRate, 0, 1000),
perGame?.ScalingMode ?? global.ScalingMode,

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

@ -462,43 +462,20 @@ internal static unsafe class PosixHostStubs
private static nint EmitSleep(byte* page, ref int offset)
{
// void Sleep(DWORD milliseconds in ecx) -> nanosleep(&req, &rem), resumed on EINTR. usleep
// would be shorter, but macOS rejects usleep(>= 1 s) with EINVAL, and a signal (the
// runtime's thread activations, for one) cuts either short: a sleep that returned at once
// on an ignored error turned UE's idle HTTP thread into a hot spin.
var nanosleep = ResolveLibcExport("nanosleep");
var errorLocation = ResolveLibcExport(OperatingSystem.IsMacOS() ? "__error" : "__errno_location");
// void Sleep(DWORD milliseconds in ecx) -> usleep(microseconds in edi).
var usleep = ResolveLibcExport("usleep");
var start = (nint)(page + offset);
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x53); // push rbx
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x20); // sub rsp, 0x20: req at [rsp], rem at [rsp+16]
Emit(page, ref offset, 0x89, 0xC8); // mov eax, ecx
Emit(page, ref offset, 0x31, 0xD2); // xor edx, edx
Emit(page, ref offset, 0xB9, 0xE8, 0x03, 0x00, 0x00); // mov ecx, 1000
Emit(page, ref offset, 0xF7, 0xF1); // div ecx: eax = seconds, edx = milliseconds
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax (tv_sec)
Emit(page, ref offset, 0x69, 0xD2, 0x40, 0x42, 0x0F, 0x00); // imul edx, edx, 1000000
Emit(page, ref offset, 0x48, 0x89, 0x54, 0x24, 0x08); // mov [rsp+8], rdx (tv_nsec)
// loop:
Emit(page, ref offset, 0x48, 0x89, 0xE7); // mov rdi, rsp
Emit(page, ref offset, 0x48, 0x8D, 0x74, 0x24, 0x10); // lea rsi, [rsp+16]
EmitMovRaxImm64(page, ref offset, nanosleep); // mov rax, imm64
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
Emit(page, ref offset, 0x89, 0xCF); // mov edi, ecx
Emit(page, ref offset, 0x81, 0xFF, 0xFF, 0x0F, 0x00, 0x00); // cmp edi, 0xFFF
Emit(page, ref offset, 0x76, 0x05); // jbe +5
Emit(page, ref offset, 0xBF, 0xFF, 0x0F, 0x00, 0x00); // mov edi, 0xFFF (cap at ~4s)
Emit(page, ref offset, 0x69, 0xFF, 0xE8, 0x03, 0x00, 0x00); // imul edi, edi, 1000
EmitMovRaxImm64(page, ref offset, usleep); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x85, 0xC0); // test eax, eax
Emit(page, ref offset, 0x74, 0x26); // jz done
EmitMovRaxImm64(page, ref offset, errorLocation); // mov rax, imm64
Emit(page, ref offset, 0xFF, 0xD0); // call rax
Emit(page, ref offset, 0x83, 0x38, 0x04); // cmp dword [rax], EINTR
Emit(page, ref offset, 0x75, 0x15); // jne done
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x10); // mov rax, [rsp+16]
Emit(page, ref offset, 0x48, 0x89, 0x04, 0x24); // mov [rsp], rax
Emit(page, ref offset, 0x48, 0x8B, 0x44, 0x24, 0x18); // mov rax, [rsp+24]
Emit(page, ref offset, 0x48, 0x89, 0x44, 0x24, 0x08); // mov [rsp+8], rax
Emit(page, ref offset, 0xEB, 0xC2); // jmp loop
// done:
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x20); // add rsp, 0x20
Emit(page, ref offset, 0x5B); // pop rbx
Emit(page, ref offset, 0x48, 0x83, 0xC4, 0x08); // add rsp, 8
Emit(page, ref offset, 0x5F); // pop rdi
Emit(page, ref offset, 0x5E); // pop rsi
Emit(page, ref offset, 0xC3); // ret

View File

@ -1,8 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Threading;
namespace SharpEmu.HLE.Host.Posix;
// View mappings bypass the anonymous-allocation tables. Keep their query state together.
@ -13,12 +11,6 @@ internal static class PosixViewRegions
uint[]? PageProtections, ulong PageBase);
private static readonly List<Region> Regions = new();
// The regions in address order, replaced whenever the list changes. Queries read it without
// Gate: every HLE import queries the guest stack's protection first, and Gate is held across
// mprotect by the GPU write tracker, so a locked query made each import wait on page traffic.
// Page protections change in place and are read racily, as a concurrent change always was.
private static Region[] _snapshot = [];
internal static void Replace(ulong address, ulong size, uint state, uint protection)
{
HostMemory.OnMappingChanged();
@ -54,8 +46,6 @@ internal static class PosixViewRegions
else
Regions.Add(region);
}
Volatile.Write(ref _snapshot, [.. Regions]);
}
// Mapping creates the storage. Fault-time protection updates must not allocate.
@ -76,8 +66,9 @@ internal static class PosixViewRegions
internal static bool TryQuery(ulong address, out HostMemory.BasicInfo info)
{
lock (Gate)
{
foreach (var region in Volatile.Read(ref _snapshot))
foreach (var region in Regions)
{
if (region.Start > address) break;
if (address >= region.End) continue;

View File

@ -149,14 +149,6 @@ public static unsafe class HostMemory
private static readonly object Gate = new();
private static readonly SortedList<ulong, Region> Regions = new();
// The regions in address order, replaced under Gate whenever a region is added or removed.
// Queries read it without the lock: Protect holds Gate across its mprotect call, and GPU
// write tracking protects pages constantly, so a locked query (one per HLE call reading
// stack arguments) waited tens of microseconds behind those syscalls.
private static Region[] RegionSnapshot = [];
private static void PublishRegionsLocked() => Volatile.Write(ref RegionSnapshot, [.. Regions.Values]);
private sealed class Region
{
public ulong Base;
@ -164,10 +156,6 @@ public static unsafe class HostMemory
public uint DefaultProtect;
public Dictionary<ulong, uint>? PageProtects;
// The overridden pages in address order, so a query finds where a protection run ends
// without visiting every override: write tracking leaves tens of thousands of them.
public SortedSet<ulong>? OverridePages;
public ulong End => Base + Size;
public uint ProtectAt(ulong pageAddress)
@ -281,7 +269,6 @@ public static unsafe class HostMemory
Size = alignedSize,
DefaultProtect = protect
};
PublishRegionsLocked();
return (void*)result;
}
@ -301,7 +288,6 @@ public static unsafe class HostMemory
HostMemory.OnMappingChanged();
Regions.Remove((ulong)address);
PublishRegionsLocked();
return munmap((nint)address, (nuint)region.Size) == 0;
}
}
@ -339,10 +325,6 @@ public static unsafe class HostMemory
{
info = default;
var pageAddress = AlignDown((ulong)address, PageSize);
if (TryQueryUnlocked(pageAddress, ref info))
{
return (nuint)sizeof(BasicInfo);
}
lock (Gate)
{
@ -358,37 +340,30 @@ public static unsafe class HostMemory
var pageProtects = region.PageProtects;
uint protect;
ulong runEnd;
if (pageProtects is null || pageProtects.Count == 0 || region.OverridePages is not { } overridePages)
if (pageProtects is null || pageProtects.Count == 0)
{
protect = region.DefaultProtect;
runEnd = region.End;
}
else if (pageProtects.TryGetValue(pageAddress, out protect))
{
// Overrides run while the next page is overridden with the same protection.
runEnd = pageAddress + PageSize;
foreach (var overrideAddress in runEnd < region.End ? overridePages.GetViewBetween(runEnd, region.End - 1) : [])
while (runEnd < region.End &&
pageProtects.TryGetValue(runEnd, out var nextProtect) &&
nextProtect == protect)
{
if (overrideAddress != runEnd || pageProtects[overrideAddress] != protect)
{
break;
}
runEnd += PageSize;
}
}
else
{
// The default protection runs to the next override.
protect = region.DefaultProtect;
runEnd = region.End;
if (pageAddress + PageSize < region.End)
foreach (var overrideAddress in pageProtects.Keys)
{
// The first element only: a view's Count walks the whole view.
foreach (var overrideAddress in overridePages.GetViewBetween(pageAddress + PageSize, region.End - 1))
if (overrideAddress > pageAddress && overrideAddress < runEnd)
{
runEnd = overrideAddress;
break;
}
}
}
@ -407,18 +382,12 @@ public static unsafe class HostMemory
// reported as a free block reaching to the next tracked region
// so scanning callers keep advancing.
var nextBase = ulong.MaxValue;
var keys = Regions.Keys;
for (int low = 0, high = keys.Count - 1; low <= high;)
foreach (var regionBase in Regions.Keys)
{
var middle = low + ((high - low) >> 1);
if (keys[middle] > pageAddress)
if (regionBase > pageAddress)
{
nextBase = keys[middle];
high = middle - 1;
}
else
{
low = middle + 1;
nextBase = regionBase;
break;
}
}
@ -447,56 +416,6 @@ public static unsafe class HostMemory
return false;
}
// Answers without Gate for untracked memory and for regions without per-page overrides;
// a region with overrides needs their dictionary, which only Gate protects. A concurrent
// Protect may land just before or after either way, as with the locked query.
private static bool TryQueryUnlocked(ulong pageAddress, ref BasicInfo info)
{
var regions = Volatile.Read(ref RegionSnapshot);
Region? candidate = null;
var nextBase = ulong.MaxValue;
for (int low = 0, high = regions.Length - 1; low <= high;)
{
var middle = low + ((high - low) >> 1);
if (regions[middle].Base <= pageAddress)
{
candidate = regions[middle];
low = middle + 1;
}
else
{
nextBase = regions[middle].Base;
high = middle - 1;
}
}
if (candidate is not null && pageAddress < candidate.End)
{
if (Volatile.Read(ref candidate.PageProtects) is { Count: > 0 })
{
return false;
}
info.BaseAddress = pageAddress;
info.AllocationBase = candidate.Base;
info.AllocationProtect = candidate.DefaultProtect;
info.RegionSize = candidate.End - pageAddress;
info.State = MEM_COMMIT;
info.Protect = candidate.DefaultProtect;
info.Type = MEM_PRIVATE;
return true;
}
info.BaseAddress = pageAddress;
info.AllocationBase = 0;
info.AllocationProtect = PAGE_NOACCESS;
info.RegionSize = (nextBase == ulong.MaxValue ? pageAddress + PageSize : nextBase) - pageAddress;
info.State = MEM_FREE_STATE;
info.Protect = PAGE_NOACCESS;
info.Type = 0;
return true;
}
private static bool TryFindRegionLocked(ulong address, out Region region)
{
region = null!;
@ -535,26 +454,20 @@ public static unsafe class HostMemory
{
region.DefaultProtect = protect;
region.PageProtects = null;
region.OverridePages = null;
return;
}
region.PageProtects ??= new Dictionary<ulong, uint>();
region.OverridePages ??= new SortedSet<ulong>();
var end = start + size;
for (var pageAddress = start; pageAddress < end; pageAddress += PageSize)
{
if (protect == region.DefaultProtect)
{
if (region.PageProtects.Remove(pageAddress))
{
region.OverridePages.Remove(pageAddress);
}
region.PageProtects.Remove(pageAddress);
}
else
{
region.PageProtects[pageAddress] = protect;
region.OverridePages.Add(pageAddress);
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
@ -134,15 +133,13 @@ public static partial class AgcExports
var commandBufferAddress = ctx[CpuRegister.Rdi];
var indexCount = (uint)ctx[CpuRegister.Rsi];
var indexAddress = ctx[CpuRegister.Rdx];
var modifier = ctx[CpuRegister.Rcx];
var modifier = (uint)ctx[CpuRegister.Rcx];
if (commandBufferAddress == 0)
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
{
return ReturnPointer(ctx, 0);
}
NoteDrawModifier("sceAgcDcbDrawIndex", modifier);
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) ||
!TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) ||
!TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) ||
@ -242,13 +239,11 @@ public static partial class AgcExports
var commandBufferAddress = ctx[CpuRegister.Rdi];
var indexCount = (uint)ctx[CpuRegister.Rsi];
var modifier = ctx[CpuRegister.Rdx];
if (commandBufferAddress == 0)
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
{
return ReturnPointer(ctx, 0);
}
NoteDrawModifier("sceAgcDcbDrawIndexAuto", modifier);
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) ||
!TryWriteUInt32(ctx, commandAddress + 4, indexCount) ||
@ -462,19 +457,4 @@ public static partial class AgcExports
#pragma warning restore SHEM006
private static long _indirectDrawEmitRejectCount;
private static readonly ConcurrentDictionary<(string, ulong), byte> _reportedDrawModifiers = new();
// The modifier names the shader stage that receives the draw and the user registers the
// command processor fills for indirect draws. A direct draw emits the same packet for every
// modifier; rejecting the unfamiliar ones dropped whole passes, such as Unreal's volume
// rasterization through the geometry stage, without a trace.
private static void NoteDrawModifier(string export, ulong modifier)
{
if (modifier != 0x4000_0000 && _reportedDrawModifiers.TryAdd((export, modifier), 0))
{
Console.Error.WriteLine(
$"[LOADER][INFO] agc.draw_modifier {export} modifier=0x{modifier:X} stage={(uint)modifier >> 29} flags=0x{(uint)modifier & 7u:X}");
}
}
}

View File

@ -64,24 +64,14 @@ public static partial class AgcExports
var commandBufferAddress = ctx[CpuRegister.Rdi];
var eventType = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
var eventAddress = ctx[CpuRegister.Rdx];
if (commandBufferAddress == 0 || eventType > 0x3F || (eventAddress != 0 && (eventType & ~1u) != 0x38))
if (commandBufferAddress == 0 || eventType > 0x3F || eventAddress != 0)
{
return ReturnPointer(ctx, 0);
}
// Counter selection and counter dumps carry a payload in the address fields.
var hasAddress = (eventType & ~1u) == 0x38;
var packetDwords = hasAddress ? 4u : 2u;
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItEventWrite, 0)) ||
!TryWriteUInt32(ctx, commandAddress + 4, hasAddress ? eventType | 0x100u : eventType))
{
return ReturnPointer(ctx, 0);
}
if (hasAddress &&
(!TryWriteUInt32(ctx, commandAddress + 8, (uint)eventAddress & ~7u) ||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(eventAddress >> 32))))
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) ||
!TryWriteUInt32(ctx, commandAddress + 4, eventType))
{
return ReturnPointer(ctx, 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

@ -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,7 @@ 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;
@ -88,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();
@ -212,7 +228,7 @@ public static class AudioOut2Exports
public uint Ambisonics = AmbisonicsNone;
public uint Passthrough;
public int PcmPending;
public byte[]? PcmData;
}
@ -226,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",
@ -252,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.
@ -305,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);
}
@ -324,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);
}
@ -391,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);
}
@ -401,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",
@ -494,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
@ -550,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);
@ -622,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}");
}
}
@ -645,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);
@ -669,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..]);
@ -719,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 _))
@ -735,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);
}
@ -972,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);
}
@ -1064,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)
{
@ -1088,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)
{
@ -1155,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
{
@ -1164,29 +1329,42 @@ 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) ||
!TryGetStereoGains(port.PortType, ch, port.Gain, port.Ambisonics, port.Passthrough,
out var leftGain, out var rightGain))
!TryDecodeDataFormat(port.DataFormat, out var ch, out var bps, out var isFloat))
{
continue;
}
var pcmData = TakePortPcmSnapshot(port);
if (pcmData is null)
{
continue;
}
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 > source.Length)
if (byteLength <= 0 || byteLength > checked(frames * 16 * sizeof(float)))
{
continue;
}
var sourceSpan = source.AsSpan(0, byteLength);
if (!ctx.Memory.TryRead(port.PcmAddress, sourceSpan))
if (pcmData.Length < byteLength)
{
continue;
}
MixPortIntoStereo(
sourceSpan,
pcmData.AsSpan(0, byteLength),
mix.AsSpan(0, frames * 2),
frames,
ch,
@ -1238,12 +1416,70 @@ 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;
}
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) =>
@ -1298,6 +1534,11 @@ public static class AudioOut2Exports
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,
@ -1453,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

@ -149,9 +149,6 @@ public unsafe class GpuBuffer : IDisposable
public bool IsCoherent { get; }
// The host mapping of the whole buffer, or null for memory the host cannot map.
public byte* MappedPointer => _mapped;
public GpuBufferUsage Usage { get; }
public ulong CpuAddress { get; }
@ -342,7 +339,6 @@ public unsafe class GpuBuffer : IDisposable
var (required, preferred, avoided) = usage switch
{
GpuBufferUsage.DeviceLocal => (MemoryPropertyFlags.None, MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.None),
GpuBufferUsage.Unified => (MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.HostCachedBit, MemoryPropertyFlags.None),
GpuBufferUsage.Upload => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit, MemoryPropertyFlags.DeviceLocalBit),
GpuBufferUsage.Download => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.HostCachedBit, MemoryPropertyFlags.DeviceLocalBit),
_ => (MemoryPropertyFlags.HostVisibleBit, MemoryPropertyFlags.HostCoherentBit | MemoryPropertyFlags.DeviceLocalBit, MemoryPropertyFlags.None),

View File

@ -9,6 +9,4 @@ public enum GpuBufferUsage : byte
Upload,
Download,
Stream,
// Device-local memory the host can map: guest buffers on unified-memory devices, read back without a copy.
Unified,
}

View File

@ -41,7 +41,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
private readonly SubmissionScheduler _scheduler;
private readonly IGpuQueueRelay _relay;
private readonly GuestBufferUploader _uploader;
private readonly bool _unifiedBuffers;
private readonly IGuestBackedSpace _backing;
private readonly BdaFaultProcessor _faults;
private readonly GpuBuffer _gds;
@ -73,7 +72,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
_relay = relay;
_backing = backing;
_faults = new BdaFaultProcessor(device, scheduler, this, CachingPageBits, CachingPageCount);
_unifiedBuffers = UnifiedBuffersEnabled && HasUnifiedMemoryType(device);
_gds = new GpuBuffer(device, scheduler, GpuBufferUsage.Stream, 0, GpuBuffer.AllFlags, GdsBufferSize);
_bdaPageTable = new GpuBuffer(device, scheduler, GpuBufferUsage.DeviceLocal, 0, GpuBuffer.AllFlags, BdaPageTableSize);
_tracker = new GuestPageTracker(pages);
@ -898,45 +896,24 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
$"unsupported buffer readback from an asynchronous GPU completion, addr=0x{guestAddress:X16} size=0x{size:X16}");
}
if (!_relay.IsGpuQueueThread && GuestReadsAwaitOffQueue && (AsyncReadback is not null || MainQueuePendingReads))
if (!_relay.IsGpuQueueThread && AsyncReadback is { } readback && GuestReadsAwaitOffQueue)
{
// A GPU write that lands after the copy was recorded makes the read stale. Retrying
// synchronously stalls the command worker until the GPU drains, so re-issue the read and
// wait here again; only the last attempt falls back to the synchronous read.
for (var attempt = 1; ; attempt++)
PendingDownload? pending = null;
if (_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
{
PendingDownload? pending = null;
if (!_relay.TryRunOnGpuQueue(() => pending = BeginReadMemoryOnGpu(guestAddress, size, isWrite, source, allowPending: true)))
{
return AwaitShutdown();
}
if (pending is null)
{
return true;
}
if (pending.Ticket is { } ticket)
{
(AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.")).Wait(ticket);
}
else
{
_scheduler.WaitForSubmittedTick(pending.MainQueueTick);
}
var finalAttempt = attempt >= PendingReadAttempts;
var applied = false;
if (!_relay.TryRunOnGpuQueue(() => applied = CompleteReadMemoryOnGpu(pending, retrySynchronously: finalAttempt)))
{
return AwaitShutdown();
}
if (applied || finalAttempt)
readback.Wait(pending.Ticket);
if (_relay.TryRunOnGpuQueue(() => CompleteReadMemoryOnGpu(pending, retrySynchronously: true)))
{
return true;
}
}
return AwaitShutdown();
}
var onQueue = _relay.IsGpuQueueThread;
@ -1167,14 +1144,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
public static string TakeAsyncReadbackReport() => FormattableString.Invariant(
$"[PERF][ASYNC_READBACK] applied={Interlocked.Exchange(ref _reportedApplied, 0)} retried={Interlocked.Exchange(ref _reportedRetried, 0)} eager_started={Interlocked.Exchange(ref _reportedEagerStarted, 0)} eager_used={Interlocked.Exchange(ref _reportedEagerUsed, 0)} eager_stale={Interlocked.Exchange(ref _reportedEagerStale, 0)}");
// Ticket is set when the readback queue carries the download; otherwise the main queue
// does, into MainQueueBuffer, and MainQueueTick is the submission the guest thread waits for.
private sealed record PendingDownload
{
public VulkanAsyncReadback.Ticket? Ticket { get; init; }
public ulong MainQueueTick { get; init; }
public GpuBuffer? MainQueueBuffer { get; init; }
public ulong[] MainQueueOffsets { get; init; } = [];
public required VulkanAsyncReadback.Ticket Ticket { get; init; }
public required List<DownloadPiece> Copies { get; init; }
public required ulong[] WriteTicks { get; init; }
public required ulong WindowBegin { get; init; }
@ -1184,9 +1156,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
public required bool IsWrite { get; init; }
public required GuestMemoryProfile.ReadbackSource Source { get; init; }
public required long Started { get; init; }
// Read from the buffers' own host mappings once the writer tick retires; no copy was recorded.
public bool Mapped { get; init; }
}
private PendingDownload? BeginReadMemoryOnGpu(ulong guestAddress, ulong size, bool isWrite, GuestMemoryProfile.ReadbackSource source,
@ -1210,53 +1179,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
{
NoteHotReadback(guestAddress, size);
}
if (copies.Count != 0 && _unifiedBuffers && AreMapped(copies, out var writer))
{
if (writer == 0 || _scheduler.IsTickComplete(writer))
{
ApplyMappedRead(copies, windowBegin, windowEnd, guestAddress, size, isWrite, readbackStarted, source);
return null;
}
if (allowPending && writer < _scheduler.CurrentTick)
{
// A guest thread had to wait for this writer: submit later writes to the window right
// after their command, so the next wait ends with them instead of with their whole batch.
NoteHotReadback(guestAddress, size);
// The writer is already submitted: wait for it alone, not for the work queued after it.
var mappedTicks = new ulong[copies.Count];
for (var index = 0; index < copies.Count; index++)
{
mappedTicks[index] = copies[index].Buffer.LastGpuWriteTick;
copies[index].Buffer.RetainForeignRead();
}
return new PendingDownload
{
Mapped = true,
MainQueueTick = writer,
Copies = copies,
WriteTicks = mappedTicks,
WindowBegin = windowBegin,
WindowEnd = windowEnd,
GuestAddress = guestAddress,
Size = size,
IsWrite = isWrite,
Source = source,
Started = readbackStarted,
};
}
if (!allowPending)
{
// On the command worker: submit the writer if it is still recording, then read the mapping.
_scheduler.Wait(writer);
ApplyMappedRead(copies, windowBegin, windowEnd, guestAddress, size, isWrite, readbackStarted, source);
return null;
}
}
if (copies.Count != 0 && allowPending && TryBeginDownloadAsync(copies, out var ticket, out var writeTicks))
{
foreach (var copy in copies)
@ -1279,31 +1201,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
};
}
if (copies.Count != 0 && allowPending &&
TryBeginDownloadOnMainQueue(copies, out var mainTick, out var mainBuffer, out var mainOffsets, out var mainWriteTicks))
{
foreach (var copy in copies)
{
copy.Buffer.RetainForeignRead();
}
return new PendingDownload
{
MainQueueTick = mainTick,
MainQueueBuffer = mainBuffer,
MainQueueOffsets = mainOffsets,
Copies = copies,
WriteTicks = mainWriteTicks,
WindowBegin = windowBegin,
WindowEnd = windowEnd,
GuestAddress = guestAddress,
Size = size,
IsWrite = isWrite,
Source = source,
Started = readbackStarted,
};
}
if (copies.Count != 0)
{
DownloadBufferMemory(copies);
@ -1319,75 +1216,9 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
return null;
}
// Without a readback queue the main queue copies the pieces into a buffer of their own and
// submits at once; the guest thread then waits for that submission instead of the worker
// waiting for the whole GPU (a guest read of GPU-written memory otherwise idled the worker
// for tens of milliseconds, several times per frame in Silent Hill: The Short Message).
private bool TryBeginDownloadOnMainQueue(List<DownloadPiece> copies, out ulong tick, out GpuBuffer buffer, out ulong[] offsets, out ulong[] writeTicks)
{
tick = 0;
buffer = null!;
offsets = [];
writeTicks = [];
if (!MainQueuePendingReads || copies.Count == 0)
{
return false;
}
var ticks = new ulong[copies.Count];
var placements = new ulong[copies.Count];
var total = 0UL;
for (var index = 0; index < copies.Count; index++)
{
var written = copies[index].Buffer.LastGpuWriteTick;
if (written == 0)
{
return false;
}
ticks[index] = written;
placements[index] = total;
total += (copies[index].Size + BufferDownloadBatchPlanner.Alignment - 1) & ~(BufferDownloadBatchPlanner.Alignment - 1);
}
if (total > AsyncReadbackLimit)
{
return false;
}
buffer = new GpuBuffer(_device, _scheduler, GpuBufferUsage.Download, 0, BufferUsageFlags.TransferDstBit, total);
for (var index = 0; index < copies.Count; index++)
{
buffer.CopyFrom(
_scheduler.Current, copies[index].Buffer, copies[index].SourceOffset, placements[index], copies[index].Size,
AccessFlags.MemoryWriteBit, AccessFlags.None, AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit, AccessFlags.HostReadBit);
}
tick = _scheduler.Flush();
offsets = placements;
writeTicks = ticks;
return true;
}
private static readonly bool MainQueuePendingReads =
Environment.GetEnvironmentVariable("SHARPEMU_MAIN_QUEUE_PENDING_READS") != "0";
// Pending reads a guest thread re-issues before it lets the worker read synchronously.
private const int PendingReadAttempts = 4;
private bool CompleteReadMemoryOnGpu(PendingDownload pending, bool retrySynchronously)
{
using var readbackScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.BufferReadback);
if (pending.Mapped)
{
return CompleteMappedRead(pending, retrySynchronously);
}
if (pending.Ticket is null)
{
return CompleteMainQueueRead(pending, retrySynchronously);
}
var readback = AsyncReadback ?? throw SubmissionScheduler.Fatal("The pending readback lost its queue.");
try
{
@ -1438,155 +1269,6 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
}
}
// Guest buffers live in host-visible device memory on unified-memory devices (Apple GPUs), so a
// readback can copy straight out of the buffer once the last GPU write to it retired. That skips
// the copy command at the tail of the queue and the wait for every draw recorded after the writer.
// SHARPEMU_UNIFIED_BUFFERS=0 keeps device-local buffers and copy readbacks.
private static readonly bool UnifiedBuffersEnabled =
Environment.GetEnvironmentVariable("SHARPEMU_UNIFIED_BUFFERS") != "0";
private static bool HasUnifiedMemoryType(GpuDeviceInfo device)
{
const MemoryPropertyFlags unified = MemoryPropertyFlags.DeviceLocalBit | MemoryPropertyFlags.HostVisibleBit | MemoryPropertyFlags.HostCoherentBit;
for (uint index = 0; index < device.MemoryTypeCount; index++)
{
if ((device.GetMemoryTypeFlags(index) & unified) == unified)
{
return true;
}
}
return false;
}
private static bool AreMapped(List<DownloadPiece> copies, out ulong writer)
{
writer = 0;
foreach (var copy in copies)
{
if (copy.Buffer.MappedPointer == null || !copy.Buffer.IsCoherent)
{
return false;
}
writer = Math.Max(writer, copy.Buffer.LastGpuWriteTick);
}
return true;
}
private void ApplyMappedRead(List<DownloadPiece> copies, ulong windowBegin, ulong windowEnd, ulong guestAddress, ulong size,
bool isWrite, long started, GuestMemoryProfile.ReadbackSource source)
{
foreach (var copy in copies)
{
var bytes = new ReadOnlySpan<byte>(copy.Buffer.MappedPointer + copy.SourceOffset, checked((int)copy.Size));
if (!_backing.TryWriteBacking(copy.Address, bytes))
{
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copy.Address:X16} size=0x{copy.Size:X16}");
}
_gpuModifiedRanges.Remove(copy.Address, copy.Size);
}
_tracker.ClearGpuDirtyPages(windowBegin, windowEnd - windowBegin);
if (isWrite)
{
_tracker.MarkCpuDirtyPages(guestAddress, size);
}
MappedReadbacks++;
RecordReadback(windowBegin, windowEnd, isWrite, copies, started, source);
}
// Readbacks served from a buffer's own mapping, without a copy command.
internal long MappedReadbacks { get; private set; }
internal bool UnifiedBuffers => _unifiedBuffers;
private bool CompleteMappedRead(PendingDownload pending, bool retrySynchronously)
{
try
{
if (!IsPendingDownloadCurrent(pending))
{
if (retrySynchronously)
{
PendingReadbacksRetried++;
Interlocked.Increment(ref _reportedRetried);
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
}
return false;
}
PendingReadbacksApplied++;
Interlocked.Increment(ref _reportedApplied);
ApplyMappedRead(pending.Copies, pending.WindowBegin, pending.WindowEnd, pending.GuestAddress, pending.Size,
pending.IsWrite, pending.Started, pending.Source);
return true;
}
finally
{
foreach (var copy in pending.Copies)
{
copy.Buffer.ReleaseForeignRead();
}
}
}
private bool CompleteMainQueueRead(PendingDownload pending, bool retrySynchronously)
{
var buffer = pending.MainQueueBuffer ?? throw SubmissionScheduler.Fatal("The pending readback lost its buffer.");
try
{
if (!IsPendingDownloadCurrent(pending))
{
if (retrySynchronously)
{
PendingReadbacksRetried++;
Interlocked.Increment(ref _reportedRetried);
ReadMemoryOnGpu(pending.GuestAddress, pending.Size, pending.IsWrite, pending.Source);
}
return false;
}
PendingReadbacksApplied++;
Interlocked.Increment(ref _reportedApplied);
var copies = pending.Copies;
buffer.Invalidate(0, buffer.Size);
for (var index = 0; index < copies.Count; index++)
{
var bytes = buffer.Mapped.Slice((int)pending.MainQueueOffsets[index], (int)copies[index].Size);
if (!_backing.TryWriteBacking(copies[index].Address, bytes))
{
throw SubmissionScheduler.Fatal($"Could not write the required direct backing: addr=0x{copies[index].Address:X16} size=0x{copies[index].Size:X16}");
}
_gpuModifiedRanges.Remove(copies[index].Address, copies[index].Size);
}
_tracker.ClearGpuDirtyPages(pending.WindowBegin, pending.WindowEnd - pending.WindowBegin);
if (pending.IsWrite)
{
_tracker.MarkCpuDirtyPages(pending.GuestAddress, pending.Size);
}
RecordReadback(pending.WindowBegin, pending.WindowEnd, pending.IsWrite, copies, pending.Started, pending.Source);
return true;
}
finally
{
// The guest thread waited for the submission, so the GPU is done with the buffer.
buffer.Dispose();
foreach (var copy in pending.Copies)
{
copy.Buffer.ReleaseForeignRead();
}
}
}
private bool IsPendingDownloadCurrent(PendingDownload pending)
{
var total = 0UL;
@ -2004,7 +1686,7 @@ public sealed unsafe class GuestBufferCache : IGuestBufferStore, IDisposable
}
var bufferIdentifier = _registry.AllocateBuffer(new GpuBuffer(
_device, _scheduler, _unifiedBuffers ? GpuBufferUsage.Unified : GpuBufferUsage.DeviceLocal, overlap.Begin,
_device, _scheduler, GpuBufferUsage.DeviceLocal, overlap.Begin,
GpuBuffer.AllFlags | BufferUsageFlags.ShaderDeviceAddressBit, overlap.End - overlap.Begin, allowSlab: true), overlap.Begin, overlap.End - overlap.Begin);
foreach (var oldId in overlapping)
{

View File

@ -103,16 +103,6 @@ public sealed partial class GpuCommandInterpreter
private void DrawIndirectArguments(ulong packetAddress, uint opcode, ulong argumentsAddress, bool indexed)
{
if (!indexed && _host.ResolvesNonIndexedIndirectDrawOnGpu)
{
// The host reads the four arguments on the GPU, so a GPU-written argument buffer
// costs no wait for the GPU here; it reads them itself when the draw needs counts.
_deferredInstanceCountAddress = argumentsAddress + 4;
_host.DrawAuto(SubmitId, new DrawAutoArguments(packetAddress, opcode, 1, 1, 0, 0,
DrawOffsetSource.IndirectArguments, argumentsAddress));
return;
}
if (!indexed)
{
var vertexCount = ReadDword(argumentsAddress);

View File

@ -435,21 +435,15 @@ public sealed partial class GpuCommandInterpreter
throw _host.Fatal($"The occlusion-counter dump is invalid: index=0x{eventIndex:X8} address=0x{eventAddress:X16}.");
}
// Until real sample queries are implemented, conservatively report visibility.
// A one-sample delta can disappear when the guest scales or reduces the counts.
const ulong visibleCounterStep = 0x2FFFFFF;
// Publish visible occlusion results for each depth block.
const ulong readyBit = 1UL << 63;
var result = readyBit | SyntheticOcclusionCounter;
for (var depthBlock = 0u; depthBlock < 16u; depthBlock++)
{
// Query reducers can read a GPU-cached copy of this range. Use the same
// ordered transfer path as clears instead of changing only CPU backing.
var destination = eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong));
_host.FillBuffer(destination, sizeof(uint), (uint)result, false);
_host.FillBuffer(destination + sizeof(uint), sizeof(uint), (uint)(result >> 32), false);
WriteQword(eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong)), result);
}
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + visibleCounterStep) & (readyBit - 1);
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + 1) & (readyBit - 1);
break;
}

View File

@ -38,10 +38,7 @@ public readonly record struct DrawAutoArguments(
uint InstanceCount,
uint FirstVertex,
uint FirstInstance,
DrawOffsetSource OffsetSource,
// Nonzero when the counts are still in guest memory at this address, laid out as
// (vertexCount, instanceCount, firstVertex, firstInstance); the other counts are then 1/0.
ulong IndirectArgumentsAddress = 0);
DrawOffsetSource OffsetSource);
public enum EndOfPipeWriteKind
{
@ -139,9 +136,6 @@ public interface ICommandStreamHost
// guest memory (DrawIndexedArguments.IndirectArgumentsAddress).
bool ResolvesIndirectDrawOnGpu => false;
// The host also draws non-indexed indirect draws from their arguments in guest memory.
bool ResolvesNonIndexedIndirectDrawOnGpu => false;
// Called when a queue reset packet clears the processor.
void OnQueueReset(int queueId);

View File

@ -14,14 +14,6 @@ public sealed unsafe partial class CachedImage
{
private const AccessFlags WriteAccess = AccessFlags.TransferWriteBit | AccessFlags.ShaderWriteBit | AccessFlags.MemoryWriteBit;
private const AccessFlags TransferAccess = AccessFlags.TransferReadBit | AccessFlags.TransferWriteBit;
private const AccessFlags AnyWriteAccess = WriteAccess | AccessFlags.ColorAttachmentWriteBit | AccessFlags.DepthStencilAttachmentWriteBit;
// Reads after reads in one layout need no barrier: the state keeps every reader, so the next
// writer still waits for all of them. (A sampled depth attachment followed by a draw that only
// tests depth would otherwise end the rendering scope.)
private static bool IsReadAfterRead(ImageAccessState state, ImageLayout layout, AccessFlags access) =>
state.Layout == layout && state.Layout != ImageLayout.Undefined && access != 0 && state.Access != 0 &&
(state.Access & AnyWriteAccess) == 0 && (access & AnyWriteAccess) == 0;
private const AccessFlags MemoryAccess = AccessFlags.MemoryReadBit | AccessFlags.MemoryWriteBit;
private const ImageLayout ReadyLayout = ImageLayout.General;
private const AccessFlags ReadyAccess = AccessFlags.ShaderReadBit | AccessFlags.TransferReadBit;
@ -89,11 +81,7 @@ public sealed unsafe partial class CachedImage
var state = states[index];
var repeatedWrite = (state.Access & WriteAccess) != 0;
if (IsReadAfterRead(state, layout, access))
{
states[index] = new ImageAccessState(state.Stage | stage, state.Access | access, layout);
}
else if (state.Layout != layout || state.Access != access || repeatedWrite)
if (state.Layout != layout || state.Access != access || repeatedWrite)
{
barriers.Add(MakeBarrier(state, layout, access, level, 1, layer, 1));
sourceStages |= state.Stage;
@ -116,12 +104,6 @@ public sealed unsafe partial class CachedImage
return (barriers, sourceStages);
}
if (IsReadAfterRead(state, layout, access))
{
Backing.State = new ImageAccessState(state.Stage | stage, state.Access | access, layout);
return (barriers, sourceStages);
}
barriers.Add(MakeBarrier(state, layout, access, 0, Vk.RemainingMipLevels, 0, Vk.RemainingArrayLayers));
sourceStages |= state.Stage;
}
@ -130,39 +112,6 @@ public sealed unsafe partial class CachedImage
return (barriers, sourceStages);
}
// The layout every subresource in the range is in, or null when they differ.
public ImageLayout? UniformLayout(SubresourceRange range)
{
var states = Backing.SubresourceStates;
if (states == null)
{
return Backing.State.Layout;
}
var layers = Description.Resources.Layers;
if (Description.IsVolume)
{
range = range with { BaseLayer = 0, LayerCount = 1 };
}
ImageLayout? layout = null;
for (var level = range.BaseLevel; level < range.BaseLevel + range.LevelCount; level++)
{
for (var layer = range.BaseLayer; layer < range.BaseLayer + range.LayerCount; layer++)
{
var index = (int)(level * layers + layer);
if (index >= states.Count || (layout is { } known && known != states[index].Layout))
{
return null;
}
layout = states[index].Layout;
}
}
return layout;
}
public void Transition(ImageLayout layout, AccessFlags access, SubresourceRange? range, CommandBuffer command)
{
PipelineStageFlags stage = 0;

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

@ -703,39 +703,11 @@ public sealed unsafe partial class GuestImageCache
}
}
// The mip layout can end before the tile plan does (Silent Hill: a 128x128 slice planned at
// the exact end of the mip chain aborted the process). Cover the whole plan when the buffer
// holds it; otherwise keep the guest bytes instead of copying a partial image.
if (!plan.Depth && plan.Color.Tiled)
{
var tiledEnd = TiledEndOf(plan.Color.Tiles);
if (tiledEnd > copySize)
{
if (tiledEnd > available)
{
return false;
}
copySize = tiledEnd;
}
}
DownloadToBuffer(image, buffer, bufferOffset, copySize, plan);
image.MarkBufferHoldsGpuContents();
return true;
}
private static ulong TiledEndOf(List<TileTransfer> tiles)
{
var end = 0UL;
foreach (var tile in tiles)
{
end = Math.Max(end, tile.TiledOffset + tile.TiledSize);
}
return end;
}
// Publishes a GPU-owned image to guest memory after its tick completes; false when it cannot.
private bool TryDownloadToGuest(ResourceSlotIdentifier imageIdentifier)
{

View File

@ -123,28 +123,48 @@ public sealed unsafe partial class GuestImageCache : IGuestImageCache, IGuestIma
return GetNullImage(request);
}
var found = LookUpImage(ref request, exactFormat);
var metadataBaseLayer = request.View.BaseLayer;
var result = LookUpImage(ref request, exactFormat);
if (request.Role is ImageRole.Texture or ImageRole.StorageImage)
{
if (request.Description.Metadata.Kind == MetadataKind.Dcc)
ulong metadataAddress;
ulong sliceSize;
using (var held = _lock.Hold())
{
using var held = _lock.Hold();
var image = _slots[found];
if (image.Description.Metadata.Kind == MetadataKind.None)
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.Kind = MetadataKind.Dcc;
image.Description.Metadata.Range = request.Description.Metadata.Range;
image.Description.Metadata = request.Description.Metadata;
}
ref readonly var description = ref image.Description;
metadataAddress = description.Metadata.Range.Address;
sliceSize = description.DccSliceSize;
}
ref readonly var description = ref _slots[found].Description;
if (description.DccSliceSize is var sliceSize and not 0)
if (sliceSize != 0)
{
SynchronizeGuestDccMetadata(description.Metadata.Range.Address, sliceSize, request.View.BaseLayer, request.View.LayerCount);
SynchronizeGuestDccMetadata(metadataAddress, sliceSize, request.View.BaseLayer, request.View.LayerCount);
}
}
return found;
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)
@ -231,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 };
@ -353,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

@ -300,12 +300,15 @@ public static partial class ImageRequestBuilders
description.BytesPerBlock = blockBytes != 0 ? blockBytes : GuestPixelFormats.BytesPerElement(format);
description.Samples = samples;
description.TileMode = tile;
var metadataAddress = descriptor.MetadataAddress << 8;
if (!storage && type == GuestImageType.Color2D && samples == 1 && levels == 1 && tile == GuestTileMode.RenderTarget && descriptor.MetadataCompress &&
metadataAddress != 0 && metadataAddress < TrackerLayout.SpaceBytes)
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(metadataAddress, 0);
description.Metadata.Range = new GuestSpan(descriptor.MetadataAddress << 8, metadataSize.Size);
description.Metadata.DccAlphaMsb = descriptor.DccAlphaMsb;
}
if (samples > 1)

View File

@ -78,6 +78,8 @@ public readonly struct TextureDescriptorWords
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

@ -166,8 +166,6 @@ internal sealed partial class MetalCommandStreamHost : IRenderHost, IShaderPipel
// Metal has no 64-bit workgroup atomics here; keep the non-atomic 32-bit pair.
bool IShaderPipelineHost.SharedInt64AtomicsEnabled => false;
// The Metal backend runs no f16 conversion probe, so it keeps the exact emulation.
bool IShaderPipelineHost.NativeHalfConversionExact => false;
RenderHostLimits IShaderPipelineHost.Limits => new(MaxDimension, MaxDimension, MaxDimension, MaxDimension);

View File

@ -41,19 +41,12 @@ 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;
// True when this device's GLSL UnpackHalf2x16 / PackHalf2x16 were measured bit-exact
// against the translator's own f16 conversion. False for every host that did not measure it.
bool NativeHalfConversionExact => false;
// True only when the host measured that a storage-buffer read past its descriptor range
// returns zero on this device; the translator then stops bounds-checking every guest word.
bool ZeroOutOfBoundsBufferReads => false;
RenderHostLimits Limits { get; }
// The sample counts a pipeline without attachments can rasterize at.

View File

@ -210,22 +210,15 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
}
// One past the highest parameter location the pixel program reads, resolved as its translator does.
// The attributes a pixel program interpolates, ascending; scanned once per decoded program
// instead of on every draw.
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<Gen5ShaderProgram, uint[]> _interpolatedAttributes = new();
private static uint[] InterpolatedAttributes(Gen5ShaderProgram program) =>
_interpolatedAttributes.GetValue(program, static program => program.Instructions
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
{
var attributes = pixelProgram.Instructions
.Select(static instruction => instruction.Control)
.OfType<Gen5InterpolationControl>()
.Select(static control => control.Attribute)
.Distinct()
.Order()
.ToArray());
private static uint ReadVertexOutputCount(Gen5ShaderProgram pixelProgram, PixelInputInfo info)
{
var attributes = InterpolatedAttributes(pixelProgram);
.ToArray();
if (attributes.Length == 0)
{
return 0;
@ -244,8 +237,16 @@ internal sealed partial class ShaderPipelineCache : IShaderPipelineProvider
private static uint InterpolatedAttributeCount(Gen5ShaderProgram program)
{
var attributes = InterpolatedAttributes(program);
return attributes.Length == 0 ? 0u : attributes[^1] + 1;
var maxAttribute = -1;
foreach (var instruction in program.Instructions)
{
if (instruction.Control is Gen5InterpolationControl interpolation)
{
maxAttribute = Math.Max(maxAttribute, (int)interpolation.Attribute);
}
}
return (uint)(maxAttribute + 1);
}
// The bound colour slots in order; each output mode names the kind the pixel program exports.

View File

@ -593,8 +593,7 @@ internal sealed class ShaderProgramCache
{
var enableGraphicsSubgroups = _host.GraphicsSubgroupOperationsEnabled;
var sharedInt64Atomics = _host.SharedInt64AtomicsEnabled;
var nativeHalfConversion = _host.NativeHalfConversionExact;
var zeroOutOfBoundsReads = _host.ZeroOutOfBoundsBufferReads;
var signedZeroInfNanPreserve = _host.ShaderSignedZeroInfNanPreserveFloat32Supported;
switch (source.Stage)
{
case ShaderStage.Vertex:
@ -607,8 +606,7 @@ internal sealed class ShaderProgramCache
ScratchDwords = info.ScratchDwords,
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
SupportsSharedInt64Atomics = sharedInt64Atomics,
NativeHalfConversionExact = nativeHalfConversion,
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
RequiredVertexOutputCount = options.RequiredVertexOutputCount,
VertexInputs = entry.VertexInputs,
PositionExportControl = info.PositionExportControl,
@ -636,8 +634,7 @@ internal sealed class ShaderProgramCache
ScratchDwords = info.ScratchDwords,
EnableGraphicsSubgroupOperations = enableGraphicsSubgroups,
SupportsSharedInt64Atomics = sharedInt64Atomics,
NativeHalfConversionExact = nativeHalfConversion,
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
PixelOutputs = options.PixelOutputs,
PixelInputEnable = options.PixelInputEnable,
PixelCustomInterpolationMask = info.CustomInterpolationMask,
@ -649,7 +646,7 @@ internal sealed class ShaderProgramCache
default:
return BuildComputeRequest(entry.Plan, resources, layout, options.ComputeInfo!, options.ComputeSystemRegisters,
sharedInt64Atomics, _host.ExecGuardElisionEnabled, nativeHalfConversion, zeroOutOfBoundsReads);
sharedInt64Atomics, _host.ExecGuardElisionEnabled, signedZeroInfNanPreserve);
}
}
@ -666,16 +663,15 @@ internal sealed class ShaderProgramCache
private static ShaderCompileRequest BuildComputeRequest(ShaderResourcePlan plan, SpecializedResourceInfo resources, BindingLayout layout,
ComputeInputInfo info, Gen5ComputeSystemRegisters? systemRegisters, bool sharedInt64Atomics, bool execGuardElision,
bool nativeHalfConversion, bool zeroOutOfBoundsReads) =>
bool signedZeroInfNanPreserve) =>
new(plan, resources, layout)
{
NativeHalfConversionExact = nativeHalfConversion,
ZeroOutOfBoundsBufferReads = zeroOutOfBoundsReads,
WaveSize = info.WaveSize,
EnableExecGuardElision = info.WaveSize != 64 || execGuardElision,
TraceDeviceAddressFaults = SharpEmu.HLE.GpuMemory.GuestGpuMemoryHook.TraceEnabled,
ScratchDwords = info.ScratchDwords,
SupportsSharedInt64Atomics = sharedInt64Atomics,
ShaderSignedZeroInfNanPreserveFloat32Supported = signedZeroInfNanPreserve,
ComputeSystemRegisters = systemRegisters,
LocalDataShareDwords = info.LocalDataShareDwords,
LocalSizeX = Math.Max(info.ThreadsX, 1),
@ -684,8 +680,8 @@ internal sealed class ShaderProgramCache
};
internal static bool TryCompilePrewarm(ComputePrewarmRecord record, ShaderCodeCapture code, IGuestGpuBackend compiler,
bool sharedInt64Atomics, bool execGuardElision, bool nativeHalfConversion, bool zeroOutOfBoundsReads,
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;
@ -702,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, nativeHalfConversion, zeroOutOfBoundsReads);
sharedInt64Atomics, execGuardElision, signedZeroInfNanPreserve);
return compiler.TryCompileProgram(request, out compiled, out error) && compiled is not null;
}
catch (Exception exception)

View File

@ -360,24 +360,7 @@ public static class VertexInputResolver
for (var bufferIndex = 0; bufferIndex < buffers.Count; bufferIndex++)
{
var buffer = buffers[bufferIndex];
uint minimumFetchBytes = 0;
foreach (var resourceIndex in buffer.AttributeIndices)
{
var resource = resources[resourceIndex];
var descriptor = resource.Descriptor;
// OOB_SELECT=2 only tests for an unbound (zero-record) descriptor.
// A constant stream still reads the whole formatted element, even
// when NUM_RECORDS is smaller than that element's byte width.
if (descriptor.Stride == 0 && descriptor.OutOfBounds == 2 && descriptor.RecordCount != 0 &&
VertexAttributeFormats.TryResolve(in descriptor, 4, out var format, out _))
{
minimumFetchBytes = Math.Max(minimumFetchBytes, Images.ViewFormatRules.BlockBytes(format));
}
}
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0)
{
MinimumFetchBytes = minimumFetchBytes,
};
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0);
foreach (var resourceIndex in buffer.AttributeIndices)
{
var resource = resources[resourceIndex];

View File

@ -29,13 +29,6 @@ public static class ColorTargetResolver
public static ColorTargetResolution? Resolve(ContextRegisters context, uint slot, uint drawLayerOffset, bool ignoreTargetMask, out uint resolvedSlot)
{
resolvedSlot = slot == FirstBoundSlot ? FirstBound(context) : slot;
// Disabling CB takes precedence over stale target addresses and masks.
// Depth-only passes may leave the preceding color targets bound.
if (context.ColorControl.Mode == 0)
{
return null;
}
return ImageRequestBuilders.ColorTarget(in context.ColorTargets[resolvedSlot], context.RenderTargetMaskForSlot(resolvedSlot), drawLayerOffset, ignoreTargetMask);
}
}

View File

@ -76,20 +76,6 @@ public readonly record struct DepthStencilState(
: ImageLayout.DepthAttachmentOptimal;
}
// A sampled attachment needs the read-only layouts; a draw that writes needs the writable
// one. A draw that neither samples nor writes keeps the read-only layout it is already in,
// so sampling and depth-testing draws can alternate inside one rendering scope.
public static ImageLayout AttachmentLayoutFor(DepthStencilState state, Format format, bool sampled, ImageLayout? current)
{
var readOnly = state.AttachmentLayout(format);
if (sampled)
{
return readOnly;
}
return state.IsReadOnly(format) && current == readOnly ? readOnly : WritableAttachmentLayout(format);
}
public ImageLayout AttachmentLayout(Format format)
{
var available = ViewFormatRules.DepthAspects(format);

View File

@ -140,9 +140,6 @@ public interface IRenderHost
// Whether a texture bound for the draw being prepared reads the depth attachment's subresources.
bool SamplesDepthAttachment(in DepthAttachmentState depth) => true;
// The layout the depth attachment's subresources are in now, or null when unknown or mixed.
ImageLayout? DepthAttachmentLayout(in DepthAttachmentState depth) => null;
// The next draw stores to buffers or storage images; called before its BeginRendering.
void PrepareMemoryWritingDraw() { }
@ -156,10 +153,6 @@ public interface IRenderHost
void DrawIndexedIndirect(BufferBinding arguments) =>
throw new NotSupportedException("The render host does not draw from indirect arguments.");
// One draw whose counts the GPU reads from the buffer (VkDrawIndirectCommand layout).
void DrawIndirect(BufferBinding arguments) =>
throw new NotSupportedException("The render host does not draw from indirect arguments.");
void Dispatch(uint groupsX, uint groupsY, uint groupsZ);
// Executes a dispatch from three uint32 group counts in guest memory when the backend can

View File

@ -251,7 +251,7 @@ public sealed partial class RenderExecutor
var vertexBuffers = AcquireVertexBuffers(vertexInput);
var indexBuffer = AcquireIndexBuffer(in indexSource);
var indirectArguments = emission.IndirectArgumentsAddress != 0
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, emission.Indexed ? IndexedIndirectArgumentsSize : AutoIndirectArgumentsSize, isWritten: false)
? _host.ObtainBuffer(emission.IndirectArgumentsAddress, IndexedIndirectArgumentsSize, isWritten: false)
: default;
DropUnwrittenColorTargets(context, ref state, pixelProgram);
state.Rendering = AcquireAttachments(ref state);
@ -314,14 +314,7 @@ public sealed partial class RenderExecutor
{
// Uploads and shader writes end with barriers to all commands, so the
// indirect read sees them.
if (emission.Indexed)
{
_host.DrawIndexedIndirect(indirectArguments);
}
else
{
_host.DrawIndirect(indirectArguments);
}
_host.DrawIndexedIndirect(indirectArguments);
}
else
{

View File

@ -310,11 +310,9 @@ public sealed partial class RenderExecutor
}
var loadState = depth.LoadState;
var layout = DepthStencilState.AttachmentLayoutFor(
loadState,
target.Format,
_host.SamplesDepthAttachment(in depth),
_host.DepthAttachmentLayout(in depth));
var layout = _host.SamplesDepthAttachment(in depth)
? loadState.AttachmentLayout(target.Format)
: DepthStencilState.WritableAttachmentLayout(target.Format);
_host.TransitionDepthAttachment(in depth, layout, loadState.AttachmentWriteAspects(target.Format));
var view = target.Request.View;
rendering.Width = Math.Min(rendering.Width, target.Width);

View File

@ -309,48 +309,6 @@ public sealed partial class RenderExecutor
}
public void DrawAuto(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
{
if (arguments.IndirectArgumentsAddress != 0 && !CanDrawAutoIndirectOnGpu(banks))
{
DrawAutoWithCpuArguments(submitId, banks, in arguments);
return;
}
DrawAutoCore(submitId, banks, in arguments);
}
// Fans, polygons and legacy rectangle/quad lists are drawn from their vertex counts, so
// those read the indirect arguments on the CPU. Rectangle lists draw through the host's
// general rectangle-list variant, as indexed indirect draws do.
private static bool CanDrawAutoIndirectOnGpu(RegisterBanks banks) =>
(GuestPrimitiveType)banks.UserConfig.PrimitiveType is
GuestPrimitiveType.PointList or GuestPrimitiveType.LineList or GuestPrimitiveType.LineStrip or
GuestPrimitiveType.TriangleList or GuestPrimitiveType.TriangleStrip or GuestPrimitiveType.RectangleList;
private void DrawAutoWithCpuArguments(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
{
Span<byte> bytes = stackalloc byte[(int)AutoIndirectArgumentsSize];
if (!_host.TryReadGuest(arguments.IndirectArgumentsAddress, bytes))
{
throw _host.Fatal($"The indirect draw arguments are unreadable: address=0x{arguments.IndirectArgumentsAddress:X16}.");
}
var words = System.Runtime.InteropServices.MemoryMarshal.Cast<byte, uint>(bytes);
var resolved = arguments with
{
VertexCount = words[0],
InstanceCount = words[1],
FirstVertex = words[2],
FirstInstance = words[3],
IndirectArgumentsAddress = 0,
};
DrawAutoCore(submitId, banks, in resolved);
}
private const ulong AutoIndirectArgumentsSize = 16;
private void DrawAutoCore(ulong submitId, RegisterBanks banks, in DrawAutoArguments arguments)
{
using var profileScope = RenderPhaseProfile.MeasureDetail(RenderPhaseProfile.Phase.DrawExecutor);
if (!_host.IsRecording)
@ -406,13 +364,6 @@ public sealed partial class RenderExecutor
return;
}
if (arguments.IndirectArgumentsAddress != 0 && state.ColorCount == 0 && !state.Depth.HasTarget)
{
// A targetless draw may be retained and replayed later; it needs its counts.
DrawAutoWithCpuArguments(submitId, banks, in arguments);
return;
}
ResolveShaderPrograms(banks, ref state);
var vertexInput = state.Programs.VertexInput;
var pixelInput = state.Programs.PixelInput;
@ -445,8 +396,7 @@ public sealed partial class RenderExecutor
false,
0,
(uint)vertexOffset,
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput),
arguments.IndirectArgumentsAddress);
indirect ? arguments.FirstInstance : ResolveInstanceOffset(vertexInput));
RecordDraw(submitId, banks, in draw, ref state, topology, in emission, default, primitiveRestart: false, setBindDebug: false, setAutoDebug: true);
_host.ResetBindings();
}
@ -606,8 +556,7 @@ public sealed partial class RenderExecutor
{
var context = banks.Context;
var shaderInterface = context.ShaderInterface;
var hasColorOutput = context.ColorControl.Mode != 0 &&
(context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
var hasColorOutput = (context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
return banks.Shader.Pixel.Address != 0 && (hasColorOutput || PixelShaderHasDepthOrCoverageSideEffects(shaderInterface));
}

View File

@ -100,9 +100,7 @@ public readonly record struct DispatchThreadLimits(uint X, uint Y, uint Z);
// The vertex buffer words of one fetch slot as the vertex program declares them.
public readonly record struct VertexInputBuffer(ulong Address, uint Stride, uint RecordCount, bool PerInstance = false)
{
public uint MinimumFetchBytes { get; init; }
public ulong Size => Math.Max(Stride != 0 ? (ulong)Stride * RecordCount : RecordCount, MinimumFetchBytes);
public ulong Size => Stride != 0 ? (ulong)Stride * RecordCount : RecordCount;
}
// One attribute of the vertex tables: its buffer words, the registers it fills and its buffer slot.

View File

@ -272,18 +272,6 @@ public sealed class SubmissionScheduler : IGpuTickScheduler, IDisposable
_lastMemoryDrainTick = nextTick;
}
// Waits for a tick that has already been submitted; safe from any thread, since it only
// waits on the timeline and records nothing.
public void WaitForSubmittedTick(ulong tick)
{
if (tick >= CurrentTick)
{
throw Fatal($"Cannot wait off the queue for a tick that is still recording: {tick} >= {CurrentTick}.");
}
_timeline.Wait(tick);
}
public void Wait(ulong tick)
{
if (tick > CurrentTick)

View File

@ -1,392 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Numerics;
using System.Runtime.InteropServices;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
namespace SharpEmu.Libs.Gpu.Vulkan;
// The CPU mirror of the two integer f16 conversion sequences the Gen5 translator emits by
// default, plus the vectors the device probe measures the native GLSL ext instructions with.
// Kept next to the probe because it is the reference the probe compares against: native is
// only used when it reproduces these results bit for bit.
internal static class HalfConversionReference
{
// Mirrors Gen5SpirvTranslator.EmitHalfToFloat: the f32 bits of the f16 in the low 16 bits.
public static uint HalfToFloatBits(uint halfBits)
{
var sign = (halfBits & 0x8000u) << 16;
var exponent = (halfBits >> 10) & 0x1Fu;
var mantissa = halfBits & 0x3FFu;
var normal = ((exponent + 112u) << 23) | (mantissa << 13);
var infinityNan = 0x7F80_0000u | (mantissa << 13);
var highBit = (uint)BitOperations.Log2(mantissa | 1u);
var shift = (int)(23u - highBit);
var subFraction = (mantissa << shift) & 0x7F_FFFFu;
var subnormal = mantissa != 0 ? (((highBit + 103u) << 23) | subFraction) : 0u;
var magnitude = exponent == 0 ? subnormal : exponent == 31 ? infinityNan : normal;
return sign | magnitude;
}
// Mirrors Gen5SpirvTranslator.EmitFloatToHalf: the f16 bits, round to nearest even.
// NaN collapses to one quiet payload (sign | 0x7C00 | 0x200) by construction.
public static uint FloatToHalfBits(uint bits)
{
var sign = (bits >> 16) & 0x8000u;
var absolute = bits & 0x7FFF_FFFFu;
var isInfinityNan = absolute >= 0x7F80_0000u;
var isNan = absolute > 0x7F80_0000u;
var infinityNan = sign | 0x7C00u | (isNan ? 0x200u : 0u);
var exponent = absolute >> 23;
var mantissa = absolute & 0x7F_FFFFu;
var significand = mantissa | 0x80_0000u;
var roundBit = (significand >> 13) & 1u;
var rounded = (significand + 0xFFFu + roundBit) >> 13;
var halfExponent = exponent - 112u;
var normalBits = (halfExponent << 10) + (rounded - 0x400u);
var normal = exponent >= 113u ? normalBits >= 0x7C00u ? 0x7C00u : normalBits : 0u;
var distance = 126u - exponent;
var shift = (int)(distance > 25u ? 25u : distance);
var shiftMask = (1u << shift) - 1u;
var halfWay = 1u << (shift - 1);
var lowBits = significand & shiftMask;
var quotient = significand >> shift;
var roundUp = lowBits > halfWay || (lowBits == halfWay && (quotient & 1u) != 0u);
var subnormal = quotient + (roundUp ? 1u : 0u);
var finite = exponent <= 112u ? subnormal : normal;
return isInfinityNan ? infinityNan : sign | finite;
}
// Every f16 bit pattern, every f16 value widened to f32, the midpoint between adjacent
// f16 values and that midpoint one f32 ulp either way (the rounding decisions), the
// overflow boundary, signed zeroes, infinities, NaNs and f32 subnormals. Padded to a
// multiple of 64 so the probe dispatch needs no bounds check. Each entry is measured
// twice: as the f16 in its low 16 bits, and as an f32 bit pattern.
public static uint[] BuildTestVectors()
{
var values = new List<uint>(1 << 19);
for (uint bits = 0; bits <= 0xFFFFu; bits++)
{
values.Add(bits);
values.Add(HalfToFloatBits(bits));
}
// 0x7BFF is the largest finite f16; pair it with its successor to cover the overflow edge.
for (uint bits = 0; bits <= 0x7BFFu; bits++)
{
var low = (double)BitConverter.UInt32BitsToSingle(HalfToFloatBits(bits));
var high = (double)BitConverter.UInt32BitsToSingle(HalfToFloatBits(bits + 1));
var midpoint = BitConverter.SingleToUInt32Bits((float)(low + (high - low) / 2));
for (var offset = -1; offset <= 1; offset++)
{
var candidate = (uint)(midpoint + offset);
values.Add(candidate);
values.Add(candidate | 0x8000_0000u);
}
}
float[] specials =
[
0f, -0f, 1f, -1f, 65504f, -65504f, 65520f, -65520f, 65536f, -65536f,
1e30f, -1e30f, float.MaxValue, float.MinValue, float.Epsilon, -float.Epsilon,
float.PositiveInfinity, float.NegativeInfinity,
5.96046448e-8f, 6.09755516e-5f, -6.09755516e-5f, 3.05175781e-5f, 2.98023224e-8f,
];
foreach (var value in specials)
{
values.Add(BitConverter.SingleToUInt32Bits(value));
}
uint[] rawSpecials =
[
0x7F80_0001u, 0xFF80_0001u, 0x7FC0_0000u, 0xFFC0_0000u, 0x7FFF_FFFFu, 0xFFFF_FFFFu,
0x0000_0001u, 0x8000_0001u, 0x007F_FFFFu, 0x807F_FFFFu, 0x0080_0000u,
];
values.AddRange(rawSpecials);
while (values.Count % 64 != 0)
{
values.Add(0);
}
return [.. values];
}
// The probe's match rule. Every finite value, every zero and every infinity must be
// bit-exact, sign included. A NaN only has to stay a NaN: the payload and the sign of a
// NaN are not defined by either side. The emulation picks its own payload for f32->f16
// (sign | 0x7C00 | 0x200) and carries the f16 payload through for f16->f32, while the
// measured hardware convert canonicalizes to a positive quiet NaN and drops the sign
// (Apple M4: every one of the 2314 disagreements out of 643200 is a negative NaN whose
// sign the hardware cleared). Nothing a shader computes from a NaN can see that
// difference short of inspecting the bits, and the alternative is paying ~20 integer
// ops on every f16 conversion in the program to define a NaN payload the ISA leaves open.
public static bool Matches(uint expected, uint actual, bool asHalf)
{
if (expected == actual)
{
return true;
}
return asHalf
? IsHalfNan(expected) && IsHalfNan(actual)
: IsFloatNan(expected) && IsFloatNan(actual);
}
private static bool IsHalfNan(uint bits) => (bits & 0x7C00u) == 0x7C00u && (bits & 0x3FFu) != 0;
private static bool IsFloatNan(uint bits) => (bits & 0x7F80_0000u) == 0x7F80_0000u && (bits & 0x7F_FFFFu) != 0;
}
// Runs the fixed f16 conversion probe once per device: the native GLSL UnpackHalf2x16 /
// PackHalf2x16 are used by the translator only when this says the device reproduces the
// emulator's own conversion. Never fatal: a failure leaves the exact emulation in place.
internal static unsafe class NativeHalfConversionProbe
{
private const int ReportedExamples = 6;
// Note carries the reason the probe could not run at all; Detail the first few disagreements.
public readonly record struct Result(bool Exact, int Mismatches, int Tested, string? Note, string? Detail = null);
// SHARPEMU_NATIVE_HALF=0 forces the emulation, =1 forces native without probing.
public static bool? ReadOverride() =>
Environment.GetEnvironmentVariable("SHARPEMU_NATIVE_HALF") switch
{
"0" => false,
"1" => true,
_ => null,
};
public static Result Run(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
{
var vectors = HalfConversionReference.BuildTestVectors();
try
{
var results = Measure(device, scheduler, beginCommand, vectors);
var mismatches = 0;
var examples = new List<string>(ReportedExamples);
for (var index = 0; index < vectors.Length; index++)
{
var wide = HalfConversionReference.HalfToFloatBits(vectors[index] & 0xFFFFu);
if (!HalfConversionReference.Matches(wide, results[2 * index], asHalf: false))
{
mismatches++;
if (examples.Count < ReportedExamples)
{
examples.Add($"h2f(0x{vectors[index] & 0xFFFF:X4})=0x{results[2 * index]:X8} want 0x{wide:X8}");
}
}
var narrow = HalfConversionReference.FloatToHalfBits(vectors[index]);
if (!HalfConversionReference.Matches(narrow, results[2 * index + 1], asHalf: true))
{
mismatches++;
if (examples.Count < ReportedExamples)
{
examples.Add($"f2h(0x{vectors[index]:X8})=0x{results[2 * index + 1]:X4} want 0x{narrow:X4}");
}
}
}
return new Result(
mismatches == 0,
mismatches,
2 * vectors.Length,
null,
examples.Count == 0 ? null : string.Join("; ", examples));
}
catch (Exception exception)
{
return new Result(false, -1, 2 * vectors.Length, exception.Message);
}
}
private static uint[] Measure(
GpuDeviceInfo device,
SubmissionScheduler scheduler,
Func<CommandBuffer> beginCommand,
uint[] vectors)
{
var vk = device.Vk;
var inputBytes = (ulong)vectors.Length * sizeof(uint);
var outputBytes = 2 * inputBytes;
using var input = new GpuBuffer(device, scheduler, GpuBufferUsage.Upload, 0, GpuBuffer.AllFlags, inputBytes);
using var output = new GpuBuffer(device, scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, outputBytes);
MemoryMarshal.AsBytes<uint>(vectors).CopyTo(input.Mapped);
input.Flush(0, inputBytes);
output.Mapped.Clear();
output.Flush(0, outputBytes);
var bindings = stackalloc DescriptorSetLayoutBinding[2];
for (uint index = 0; index < 2; index++)
{
bindings[index] = new DescriptorSetLayoutBinding
{
Binding = index,
DescriptorType = DescriptorType.StorageBuffer,
DescriptorCount = 1,
StageFlags = ShaderStageFlags.ComputeBit,
};
}
var layoutInfo = new DescriptorSetLayoutCreateInfo
{
SType = StructureType.DescriptorSetLayoutCreateInfo,
BindingCount = 2,
PBindings = bindings,
};
Require(vk.CreateDescriptorSetLayout(device.Device, &layoutInfo, null, out var setLayout), "descriptor layout");
DescriptorPool pool = default;
PipelineLayout pipelineLayout = default;
Pipeline pipeline = default;
try
{
var poolSize = new DescriptorPoolSize { Type = DescriptorType.StorageBuffer, DescriptorCount = 2 };
var poolInfo = new DescriptorPoolCreateInfo
{
SType = StructureType.DescriptorPoolCreateInfo,
MaxSets = 1,
PoolSizeCount = 1,
PPoolSizes = &poolSize,
};
Require(vk.CreateDescriptorPool(device.Device, &poolInfo, null, out pool), "descriptor pool");
var boundLayout = setLayout;
var allocateInfo = new DescriptorSetAllocateInfo
{
SType = StructureType.DescriptorSetAllocateInfo,
DescriptorPool = pool,
DescriptorSetCount = 1,
PSetLayouts = &boundLayout,
};
Require(vk.AllocateDescriptorSets(device.Device, &allocateInfo, out DescriptorSet set), "descriptor set");
var infos = stackalloc DescriptorBufferInfo[2];
infos[0] = new DescriptorBufferInfo(input.Handle, 0, inputBytes);
infos[1] = new DescriptorBufferInfo(output.Handle, 0, outputBytes);
var writes = stackalloc WriteDescriptorSet[2];
for (uint binding = 0; binding < 2; binding++)
{
writes[binding] = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
DstSet = set,
DstBinding = binding,
DescriptorCount = 1,
DescriptorType = DescriptorType.StorageBuffer,
PBufferInfo = infos + binding,
};
}
vk.UpdateDescriptorSets(device.Device, 2, writes, 0, null);
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
{
SType = StructureType.PipelineLayoutCreateInfo,
SetLayoutCount = 1,
PSetLayouts = &boundLayout,
};
Require(vk.CreatePipelineLayout(device.Device, &pipelineLayoutInfo, null, out pipelineLayout), "pipeline layout");
var spirv = SpirvFixedShaders.CreateHalfConversionProbe();
ShaderModule module;
fixed (byte* code = spirv)
{
var moduleInfo = new ShaderModuleCreateInfo
{
SType = StructureType.ShaderModuleCreateInfo,
CodeSize = (nuint)spirv.Length,
PCode = (uint*)code,
};
Require(vk.CreateShaderModule(device.Device, &moduleInfo, null, out module), "shader module");
}
ReadOnlySpan<byte> entryPoint = "main\0"u8;
Silk.NET.Vulkan.Result created;
fixed (byte* entry = entryPoint)
{
var pipelineInfo = new ComputePipelineCreateInfo
{
SType = StructureType.ComputePipelineCreateInfo,
Layout = pipelineLayout,
Stage = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.ComputeBit,
Module = module,
PName = entry,
},
};
created = vk.CreateComputePipelines(device.Device, default, 1, &pipelineInfo, null, out pipeline);
}
vk.DestroyShaderModule(device.Device, module, null);
Require(created, "compute pipeline");
var command = beginCommand();
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, pipeline);
var boundSet = set;
vk.CmdBindDescriptorSets(command, PipelineBindPoint.Compute, pipelineLayout, 0, 1, &boundSet, 0, null);
vk.CmdDispatch(command, (uint)vectors.Length / 64, 1, 1);
var barrier = new BufferMemoryBarrier2
{
SType = StructureType.BufferMemoryBarrier2,
SrcAccessMask = AccessFlags2.ShaderWriteBit,
DstAccessMask = AccessFlags2.HostReadBit,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Buffer = output.Handle,
Offset = 0,
Size = outputBytes,
};
VulkanSynchronization.PipelineBarrier(vk, command,
PipelineStageFlags.ComputeShaderBit, PipelineStageFlags.HostBit, DependencyFlags.None,
0, null, 1, &barrier, 0, null);
scheduler.FlushAndWait();
output.Invalidate(0, outputBytes);
return MemoryMarshal.Cast<byte, uint>(output.Mapped).ToArray();
}
finally
{
if (pipeline.Handle != 0)
{
vk.DestroyPipeline(device.Device, pipeline, null);
}
if (pipelineLayout.Handle != 0)
{
vk.DestroyPipelineLayout(device.Device, pipelineLayout, null);
}
if (pool.Handle != 0)
{
vk.DestroyDescriptorPool(device.Device, pool, null);
}
vk.DestroyDescriptorSetLayout(device.Device, setLayout, null);
}
}
private static void Require(Silk.NET.Vulkan.Result result, string operation)
{
if (result != Silk.NET.Vulkan.Result.Success)
{
throw new InvalidOperationException($"The f16 conversion probe could not create its {operation}: {result}");
}
}
}

View File

@ -1,242 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using SharpEmu.Libs.Gpu.Buffers;
using SharpEmu.Libs.Gpu.Scheduling;
using SharpEmu.ShaderCompiler.Vulkan;
using Silk.NET.Vulkan;
namespace SharpEmu.Libs.Gpu.Vulkan;
// Measures whether a storage-buffer read past the end of its descriptor range reads zero on this
// device. Every guest buffer word the translator emits carries a range test, an address clamp and
// a zero select, which exist only to produce that zero; with robustBufferAccess2 the device owes
// it already and the three operations are pure overhead. The capability bit alone does not settle
// it — MoltenVK runs on Metal, which has no hardware bounds checking — so the probe reads a bound
// range it deliberately undershoots and checks the words past it. Never run on a device that did
// not enable robustBufferAccess2: an out-of-range read there is undefined and can fault.
internal static unsafe class ZeroOutOfBoundsReadProbe
{
// The descriptor binds this many words of a larger allocation; the words past it must read
// zero, and the words inside it must still read the sentinel the probe wrote.
private const uint BoundWords = 256;
private const uint ProbeWords = 1024;
private const uint Sentinel = 0xA5C3_F00Du;
public readonly record struct Result(bool Zeroed, int Mismatches, int Tested, string? Note);
// SHARPEMU_ZERO_OOB_READS=0 keeps the per-access checks, =1 drops them without measuring.
public static bool? ReadOverride() =>
Environment.GetEnvironmentVariable("SHARPEMU_ZERO_OOB_READS") switch
{
"0" => false,
"1" => true,
_ => null,
};
public static Result Run(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
{
try
{
var results = Measure(device, scheduler, beginCommand);
var mismatches = 0;
for (var index = 0u; index < ProbeWords; index++)
{
var expected = index < BoundWords ? Sentinel + index : 0u;
if (results[index] != expected)
{
mismatches++;
}
}
return new Result(mismatches == 0, mismatches, (int)ProbeWords, null);
}
catch (Exception exception)
{
return new Result(false, -1, (int)ProbeWords, exception.Message);
}
}
private static uint[] Measure(GpuDeviceInfo device, SubmissionScheduler scheduler, Func<CommandBuffer> beginCommand)
{
var vk = device.Vk;
var allocationBytes = (ulong)ProbeWords * sizeof(uint);
var boundBytes = (ulong)BoundWords * sizeof(uint);
var outputBytes = allocationBytes;
// The allocation is larger than the bound range and carries a sentinel everywhere, so a
// device that simply ignores the range reads the sentinel back instead of zero.
using var input = new GpuBuffer(device, scheduler, GpuBufferUsage.Upload, 0, GpuBuffer.AllFlags, allocationBytes);
using var output = new GpuBuffer(device, scheduler, GpuBufferUsage.Download, 0, GpuBuffer.AllFlags, outputBytes);
var words = new uint[ProbeWords];
for (var index = 0u; index < ProbeWords; index++)
{
words[index] = Sentinel + index;
}
MemoryMarshal.AsBytes<uint>(words).CopyTo(input.Mapped);
input.Flush(0, allocationBytes);
output.Mapped.Clear();
output.Flush(0, outputBytes);
var bindings = stackalloc DescriptorSetLayoutBinding[2];
for (uint index = 0; index < 2; index++)
{
bindings[index] = new DescriptorSetLayoutBinding
{
Binding = index,
DescriptorType = DescriptorType.StorageBuffer,
DescriptorCount = 1,
StageFlags = ShaderStageFlags.ComputeBit,
};
}
var layoutInfo = new DescriptorSetLayoutCreateInfo
{
SType = StructureType.DescriptorSetLayoutCreateInfo,
BindingCount = 2,
PBindings = bindings,
};
Require(vk.CreateDescriptorSetLayout(device.Device, &layoutInfo, null, out var setLayout), "descriptor layout");
DescriptorPool pool = default;
PipelineLayout pipelineLayout = default;
Pipeline pipeline = default;
try
{
var poolSize = new DescriptorPoolSize { Type = DescriptorType.StorageBuffer, DescriptorCount = 2 };
var poolInfo = new DescriptorPoolCreateInfo
{
SType = StructureType.DescriptorPoolCreateInfo,
MaxSets = 1,
PoolSizeCount = 1,
PPoolSizes = &poolSize,
};
Require(vk.CreateDescriptorPool(device.Device, &poolInfo, null, out pool), "descriptor pool");
var boundLayout = setLayout;
var allocateInfo = new DescriptorSetAllocateInfo
{
SType = StructureType.DescriptorSetAllocateInfo,
DescriptorPool = pool,
DescriptorSetCount = 1,
PSetLayouts = &boundLayout,
};
Require(vk.AllocateDescriptorSets(device.Device, &allocateInfo, out DescriptorSet set), "descriptor set");
var infos = stackalloc DescriptorBufferInfo[2];
infos[0] = new DescriptorBufferInfo(input.Handle, 0, boundBytes);
infos[1] = new DescriptorBufferInfo(output.Handle, 0, outputBytes);
var writes = stackalloc WriteDescriptorSet[2];
for (uint binding = 0; binding < 2; binding++)
{
writes[binding] = new WriteDescriptorSet
{
SType = StructureType.WriteDescriptorSet,
DstSet = set,
DstBinding = binding,
DescriptorCount = 1,
DescriptorType = DescriptorType.StorageBuffer,
PBufferInfo = infos + binding,
};
}
vk.UpdateDescriptorSets(device.Device, 2, writes, 0, null);
var pipelineLayoutInfo = new PipelineLayoutCreateInfo
{
SType = StructureType.PipelineLayoutCreateInfo,
SetLayoutCount = 1,
PSetLayouts = &boundLayout,
};
Require(vk.CreatePipelineLayout(device.Device, &pipelineLayoutInfo, null, out pipelineLayout), "pipeline layout");
var spirv = SpirvFixedShaders.CreateUncheckedBufferCopyProbe();
ShaderModule module;
fixed (byte* code = spirv)
{
var moduleInfo = new ShaderModuleCreateInfo
{
SType = StructureType.ShaderModuleCreateInfo,
CodeSize = (nuint)spirv.Length,
PCode = (uint*)code,
};
Require(vk.CreateShaderModule(device.Device, &moduleInfo, null, out module), "shader module");
}
ReadOnlySpan<byte> entryPoint = "main\0"u8;
Silk.NET.Vulkan.Result created;
fixed (byte* entry = entryPoint)
{
var pipelineInfo = new ComputePipelineCreateInfo
{
SType = StructureType.ComputePipelineCreateInfo,
Layout = pipelineLayout,
Stage = new PipelineShaderStageCreateInfo
{
SType = StructureType.PipelineShaderStageCreateInfo,
Stage = ShaderStageFlags.ComputeBit,
Module = module,
PName = entry,
},
};
created = vk.CreateComputePipelines(device.Device, default, 1, &pipelineInfo, null, out pipeline);
}
vk.DestroyShaderModule(device.Device, module, null);
Require(created, "compute pipeline");
var command = beginCommand();
vk.CmdBindPipeline(command, PipelineBindPoint.Compute, pipeline);
var boundSet = set;
vk.CmdBindDescriptorSets(command, PipelineBindPoint.Compute, pipelineLayout, 0, 1, &boundSet, 0, null);
vk.CmdDispatch(command, ProbeWords / 64, 1, 1);
var barrier = new BufferMemoryBarrier2
{
SType = StructureType.BufferMemoryBarrier2,
SrcAccessMask = AccessFlags2.ShaderWriteBit,
DstAccessMask = AccessFlags2.HostReadBit,
SrcQueueFamilyIndex = Vk.QueueFamilyIgnored,
DstQueueFamilyIndex = Vk.QueueFamilyIgnored,
Buffer = output.Handle,
Offset = 0,
Size = outputBytes,
};
VulkanSynchronization.PipelineBarrier(vk, command,
PipelineStageFlags.ComputeShaderBit, PipelineStageFlags.HostBit, DependencyFlags.None,
0, null, 1, &barrier, 0, null);
scheduler.FlushAndWait();
output.Invalidate(0, outputBytes);
return MemoryMarshal.Cast<byte, uint>(output.Mapped).ToArray();
}
finally
{
if (pipeline.Handle != 0)
{
vk.DestroyPipeline(device.Device, pipeline, null);
}
if (pipelineLayout.Handle != 0)
{
vk.DestroyPipelineLayout(device.Device, pipelineLayout, null);
}
if (pool.Handle != 0)
{
vk.DestroyDescriptorPool(device.Device, pool, null);
}
vk.DestroyDescriptorSetLayout(device.Device, setLayout, null);
}
}
private static void Require(Silk.NET.Vulkan.Result result, string operation)
{
if (result != Silk.NET.Vulkan.Result.Success)
{
throw new InvalidOperationException($"The out-of-range read probe could not create its {operation}: {result}");
}
}
}

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

@ -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

@ -1988,7 +1988,7 @@ public static class KernelPthreadCompatExports
break;
}
var waitMilliseconds = 10;
var waitDuration = TimeSpan.FromMilliseconds(10);
if (timed)
{
var remaining = GetRemainingTimeout(deadline);
@ -1998,14 +1998,13 @@ public static class KernelPthreadCompatExports
break;
}
// Monitor.Wait truncates to whole milliseconds, so a sub-millisecond remainder
// became a zero timeout and the wait spun until the deadline. UE's game thread
// waits for the render fence in 1 ms slices, which kept a core busy. A timed wait
// may wake late, so round up instead.
waitMilliseconds = (int)Math.Min(waitMilliseconds, Math.Ceiling(remaining.TotalMilliseconds));
if (remaining < waitDuration)
{
waitDuration = remaining;
}
}
_ = Monitor.Wait(state.SyncRoot, waitMilliseconds);
_ = Monitor.Wait(state.SyncRoot, waitDuration);
if (waiter.CompletionState == 0 &&
timed &&
GetRemainingTimeout(deadline) <= TimeSpan.Zero)

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

@ -144,8 +144,7 @@ public static class KernelSyncOnAddressCompatExports
{
if (Volatile.Read(ref _wakeRequested) == 0)
{
// Round up so a sub-millisecond remainder does not spin.
Monitor.Wait(_hostGate, (int)Math.Ceiling(waitSlice.TotalMilliseconds));
Monitor.Wait(_hostGate, waitSlice);
}
}

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

@ -44,8 +44,6 @@ internal static class HostMovieBridge
private static bool _frameBufferPresented;
private static MediaFramePlayback? _playback;
private static Timer? _playbackWatchdog;
private static long _watchdogFrameIndex = -1;
private static readonly TimeSpan StalledPlaybackTimeout = TimeSpan.FromSeconds(60);
private static long _frameSerial;
private static uint _presentationWidth = MaxHostVideoWidth;
private static uint _presentationHeight = MaxHostVideoHeight;
@ -328,7 +326,6 @@ internal static class HostMovieBridge
private static void ArmPlaybackWatchdogLocked(string hostPath)
{
_playbackWatchdog?.Dispose();
_watchdogFrameIndex = -1;
var timeout = TryReadBinkInfo(hostPath, out var info)
? GetPlaybackWatchdogTimeout(info)
: TimeSpan.FromSeconds(150);
@ -343,17 +340,6 @@ internal static class HostMovieBridge
return;
}
// Playback follows the guest's own decode and audio clock, so a slow host plays a
// movie slower than real time. Expire only a movie that stopped advancing: forcing EOF
// under a guest decoder that is still mid-stream wedges it on the last frame.
var frameIndex = _playback?.PlaybackProgress.FrameIndex ?? -1;
if (_playback is not null && frameIndex > _watchdogFrameIndex)
{
_watchdogFrameIndex = frameIndex;
_playbackWatchdog?.Change(StalledPlaybackTimeout, Timeout.InfiniteTimeSpan);
return;
}
Console.Error.WriteLine(
"[LOADER][WARN] Bink2 host watchdog expired for " +
Path.GetFileName(path) + "; advancing to the next movie.");

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

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

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