Commit Graph

616 Commits

Author SHA1 Message Date
kuba 73bfad7ed2 Merge #939 (pr/import-patch-serialization) 2026-09-30 00:12:22 +02:00
kuba f37b033b68 Merge #938 (pr/videoout-present-races) 2026-09-30 00:12:21 +02:00
kuba d12e404f9a Merge #937 (pr/cpu-fault-handling) 2026-09-30 00:12:21 +02:00
kuba 048933bfb8 Merge #936 (pr/tbdr-render-scope) 2026-09-30 00:12:20 +02:00
kuba 2f3ee0935c Merge #935 (pr/pixel-system-inputs) 2026-09-30 00:12:20 +02:00
kuba 585bf674c6 Merge #934 (pr/native-guest-fast-paths) 2026-09-30 00:12:19 +02:00
kuba ea6ac5028f Merge #933 (pr/spirv-constant-folding) 2026-09-30 00:12:19 +02:00
kuba 77aefa2bf3 Merge #932 (pr/sigterm-shutdown) 2026-09-30 00:12:18 +02:00
kuba 5f269ac87a Merge #931 (pr/sdl-audio-drain-sleep) 2026-09-30 00:12:18 +02:00
kuba d04084b6d5 Merge #930 (pr/wave-lane-state) 2026-09-30 00:12:17 +02:00
kuba 121e6fa062 Merge #929 (pr/audioout2-grain-pacing) 2026-09-30 00:12:17 +02:00
kuba e6d011eeb6 Merge #928 (pr/moltenvk-fence-retirement) 2026-09-30 00:12:16 +02:00
kuba a982b2800d Merge #923 (pr/spirv-register-variables) 2026-09-30 00:12:15 +02:00
kuba 6a041f2407 Merge #922 (pr/gfx10-mip-tail-block) 2026-09-30 00:12:15 +02:00
kuba fa6a3b6db8 Merge #921 (pr/avplayer-active-before-start) 2026-09-30 00:12:14 +02:00
kuba c04fd9029f Merge #920 (pr/audioout2-query-memory-size) 2026-09-30 00:12:14 +02:00
kuba 5943b3c433 Merge #919 (pr/red-zone-frame-pointer-spills) 2026-09-30 00:12:13 +02:00
kuba b219a8795c Merge #918 (pr/moltenvk-descriptor-sets) 2026-09-30 00:12:13 +02:00
kuba cb8feba766 Merge #917 (pr/agc-loaded-value-fill) 2026-09-30 00:12:12 +02:00
kuba 9c4ed82388 Merge #916 (pr/compute-lds-size) 2026-09-30 00:12:12 +02:00
kuba ae7ca8dfbe [CPU] Reuse import trampolines and leave patched stubs alone on module reloads
Every runtime module load sets up the import stubs of all loaded modules
again, and each setup built a fresh trampoline and rewrote the stub's 16 bytes
-- under guest threads that may be executing exactly those bytes.

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

The trampoline ABI test builds its backend with GetUninitializedObject, which
skips field initializers, so it has to supply the new cache; both fields now
go through one helper that names a field it cannot find.
2026-09-30 00:11:15 +02:00
kuba 066df8e754 [AudioOut2] Write a single size_t from sceAudioOut2ContextQueryMemory
The export returns the context memory size through one size_t. For heap
output it also wrote a second qword (an alignment) after it, and titles keep
other locals there: Octopath Traveler II's allocator reads the dword at +12 of
that block as a divisor, so it faulted with #DE during boot. Write only the
size, wherever the output lives.
2026-09-30 00:11:14 +02:00
ParantezTech 50198a694b [Kernel] Return packed directory entries for Quake II 2026-09-30 00:56:20 +03:00
kuba 829dc815d1 [CPU] Serialize import stub and lazy TLS site patches
Patching a guest code site unprotects the page it lives on, writes, and
restores the old protection. Two threads patching sites that share a page
therefore race: one restores the page to read-execute while the other is
still writing into it, and the second write faults or is lost. Both happen in
practice -- a runtime module load sets up import stubs while guest threads are
running, and guest threads patch their own lazy TLS access sites as they first
reach them, several of which land on one page.

Every import stub patch and every lazy TLS site patch now runs under a single
gate, so the unprotect/write/restore sequence is never interleaved.
2026-09-29 23:19:20 +02:00
kuba 73bf602680 [Cpu] Resolve GPU-tracked page faults before building the exception context
A CPU access to a page the GPU memory tracker protects faults on every first
touch after an upload or a GPU write, thousands of times a second in a frame.
The POSIX bridge converted each one into a full Win64 CONTEXT (general and
vector registers), then walked the interrupt, image-tracker, lazy-commit and
allocator-hole handlers before the tracker ever saw it.

The tracker needs only the fault address and the access kind, so it now runs
right after the image tracker, straight off the raw signal. An env var
restores the old handler order.
2026-09-29 23:18:39 +02:00
kuba 333f7f0c3a [CPU] Bridge SIGFPE into the guest fault handler so #DE crashes are reported
The POSIX bridge installed handlers for SIGSEGV, SIGBUS, SIGILL, SIGTRAP and
SIGABRT, but not SIGFPE. A guest integer divide by zero therefore took the
runtime default and killed the process with no exception record and no guest
context, so the faulting instruction was invisible.

SIGFPE now routes to the same handler and is reported as an integer divide by
zero. Like SIGILL, it skips the image write tracker, which only ever resolves
data-access faults.
2026-09-29 23:18:39 +02:00
kuba 2f4dcba99c [VideoOut] Pool the per-flip display-surface snapshot images
CaptureFlip created a 1920x1080 RGBA16F VkImage with its own VkDeviceMemory
for every flip and destroyed both when the flip retired: around twenty
vkCreateImage plus vkAllocateMemory pairs a second for the same extent, and on
MoltenVK as many Metal texture allocations.

Retired snapshots are now kept in a small per-format, per-extent pool and
handed back instead. A snapshot is fully overwritten by the copy that follows
it and its barrier takes it from Undefined, so neither its previous contents
nor its layout matter. The pool holds at most eight images, is guarded because
the command-stream thread takes snapshots while the render thread retires
them, and is drained at teardown. SHARPEMU_FLIP_SNAPSHOT_POOL=0 turns it off.
2026-09-29 23:18:07 +02:00
kuba 133dfb4857 [VideoOut] Present a flip only after the tick that copied it
CaptureFlip recorded the display-surface copy into the guest batch, flushed
it, and then read the shared _submitTimeline field for the presentation's
RequiredTick. The presenter thread writes that same field from its own
submission, so the flip could be tagged with an older tick that was already
complete. The render loop then blitted the freshly created (never written)
flip snapshot, which presents as fully zeroed RGBA - a black frame with an
alpha of 0. Take the tick that this flush returned instead.
2026-09-29 23:18:07 +02:00
kuba 4512688301 [VideoOut] Give every swapchain image its own sRGB-encode intermediate
A float guest flip is blitted through an sRGB image before it is copied into
the UNORM swapchain image, and one such intermediate served every frame. Each
frame discards its contents by transitioning it Undefined -> TransferDst
before blitting, so with two frames in flight the next frame's blit raced the
previous frame's copy-out and the presented image came out fully zeroed. The
guest's own display buffers held the correct picture the whole time, so the
window simply went black for long stretches.

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

On Superliminal the same run went from 60 of 84 presented-swapchain readbacks
black to 10 of 187, all of them from before the menu appears, and the menu no
longer turns black after a couple of minutes.
2026-09-29 23:18:07 +02:00
kuba 5112bb74c4 [CPU] Run the POSIX signal handler on the host stack for guest faults
Guest faults are delivered on the guest stack, and the managed handler ran
there. The runtime only knows the thread's own stack; Windows swaps the TEB
stack bounds while guest code runs, but POSIX has no equivalent. A garbage
collection during a guest fault then misread the thread's frames and
reported garbage object references, and the process failed with "attempted
to call a UnmanagedCallersOnly method from managed code". A small native
trampoline now moves to the host stack below the frames that entered guest
code before it calls the managed handler.
2026-09-29 23:16:46 +02:00
kuba 223a1b7873 [SpirvTranslator] Fill the SAMPLE_COVERAGE and POS_FIXED_PT pixel inputs
The two remaining reserved pixel system inputs were also left reading zero.
SAMPLE_COVERAGE now reads the first element of the SampleMask built-in, and
POS_FIXED_PT packs the integer pixel position taken from FragCoord, x in bits
[15:0] and y in bits [31:16], so no enabled pixel system input is left unset.
2026-09-29 23:15:26 +02:00
kuba 77d5881e25 [SpirvTranslator] Fill the FRONT_FACE and ANCILLARY pixel inputs
A pixel shader that enables FRONT_FACE or ANCILLARY reads those VGPRs, but the
translator only reserved their compact slots and left them reading zero.

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

ANCILLARY is now the Layer built-in placed at bit 16, and FRONT_FACE the
FrontFacing built-in mapped to 1.0f for a front face and 0 for a back face,
which satisfies both the float greater-than and the integer not-equal tests
compilers emit for it.
2026-09-29 23:15:26 +02:00
kuba 5fb1cbacdc [CPU] Run libc memcpy/memmove as a native guest-side stub
Guest memcpy went through the managed gateway on every call: two write
tracking notifications, the memory reader lock and two region lookups, about
0.8 us per call under Rosetta even for a copy of a few bytes. Dreaming Sarah
makes roughly 45k of those calls a second.

Guest memory is identity-mapped, and every fault a copy can take -- write
tracking, lazy commit -- is resolved from the fault address alone, so the copy
can run as a forward rep movsb in a fast-path stub. A destination that sits
inside the source, where a forward copy would read bytes it has already
written, and the write-watch diagnostic both keep the managed export.
2026-09-29 23:15:25 +02:00
kuba 6db42fe981 [Cpu] Answer pthread_self and pthread_getspecific in native stubs
The managed import gateway costs a guest-to-host stack switch, a full guest
register pack, a GCHandle dereference and the whole dispatch bookkeeping
before the HLE export even runs. For the two hottest trivial imports that is
three orders of magnitude more work than the answer itself: one UE5 title's
load measured 12.9M scePthreadGetspecific calls (38 s of wall time) and 4.3M
scePthreadSelf calls, at roughly 2.9 us each.

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

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

The stubs are gated to macOS x64, where the emitted TLS read matches the
platform's pthread key array, and switched off whenever import or pthread
tracing needs every call to pass through the gateway.
2026-09-29 23:15:00 +02:00
kuba 73712e0d1f [SpirvTranslator] Skip host-guaranteed bounds checks and unused LDS
Two sources of instructions the driver has to compile but can never need.

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

ds_swizzle and ds_bpermute exchange data between lanes and GDS instructions
address the global data share, so none of them touches LDS. A graphics stage
that uses only those was still given the 8 KiB zero-initialized private LDS
array, which Metal pays for in compile time and in private memory per
invocation. The array is now declared only for instructions that really
address LDS.
2026-09-29 23:13:59 +02:00
kuba 3bd5cf7d32 [SpirvTranslator] Fold constant integer and boolean instructions
The translator materializes a good deal of translation-time knowledge as
SPIR-V instructions: a specialized descriptor's buffer format, swizzles,
strides, and the conditions that guard host-guaranteed ranges all end up as
arithmetic, comparisons and selections over operands the builder itself just
emitted as constants. That reaches the driver as runtime work, and on Metal
every one of those instructions costs pipeline-creation time.

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

The buffer format decoder uses this directly: a specialized descriptor makes
the unified format a constant, so it decodes on the host instead of emitting
a module-level lookup table and an indexed read.
The test that covered that path asserted the table's presence and now asserts
its absence.
2026-09-29 23:13:59 +02:00
kuba 28cd701242 [CLI] Shut down cooperatively on SIGTERM
SIGTERM (launchd, process managers, kill) used the runtime default and
exited with GPU work in flight. It now takes the same host-interrupt path
as Ctrl+C: the presenter closes and tears Vulkan down before exit.
2026-09-29 23:11:26 +02:00
kuba fa87874883 [Audio] Sleep for the SDL queue's drain time instead of polling every ms
Submit's backpressure wait is what paces every audio grain on the SDL
output, and it polled the queued byte count with Thread.Sleep(1), costing
the audio thread several wakeups per grain.

It now sleeps for the time the device needs to drain back under the queue
cap, still bounded by the existing deadline and the maximum wait.
2026-09-29 23:11:06 +02:00
kuba 4e536b18d9 [Vk] Keep one depth attachment layout for draws that do not sample it
The depth attachment layout followed each draw's depth/stencil write mask, so
a draw sequence that toggles depth writes — opaque geometry, then decals and
translucency — changed the layout every few draws, and every layout change
ended the rendering scope.

The read-only attachment layouts are only required when the draw also samples
the attachment. Every other draw now uses the writable layout for the
aspects the format has, and the write mask alone selects the access, so the
layout stays constant and the scope survives. The presenter answers whether a
texture bound for the draw being prepared views the depth attachment's
subresources, defaulting to yes for hosts that do not track it.
2026-09-29 23:10:05 +02:00
kuba e7c7c27cbb [AudioOut2] Pace one grain per Advance+Push cycle and report the queue level
sceAudioOut2ContextAdvance and sceAudioOut2ContextPush each submitted the
context's pending PCM and, whenever nothing reached a host device, each slept
a whole grain in the software pacing fallback. A title that calls Advance then
Push once per grain therefore spent two grain periods per grain, so its audio
thread ran at half real time and held up every thread waiting on it.

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

sceAudioOut2ContextGetQueueLevel reported an empty queue with the full depth
available on every call. Titles size their rendering by those two values --
Wwise renders exactly puiAvailableQueues grains per update and pushes them
non-blocking -- so a queue that never filled made it render a full queue every
update and then block inside Push for a grain per push while holding its audio
lock, which starved the game thread. The level now comes from the context's
grain clock, or, once a grain has reached a host device, from that device's
queued milliseconds beyond a short cushion.
2026-09-29 23:09:59 +02:00
kuba 355127b4b8 [SpirvTranslator] Take wave32 EXEC/VCC lane state from the low register
In wave32 a lane mask is its low register alone: EXEC_HI and VCC_HI are
ordinary SGPRs. Writing one of them therefore must not recompute the wave
lane state, and the lane state has to be tested against the low dword rather
than against a 64-bit mask composed from the low and high registers.

The lane-bit test that guards this covered the wave64 constant width only, so
it now accepts a mask of either width.
2026-09-29 23:09:45 +02:00
kuba ff3221c770 [Vk] Keep the rendering scope open across barriers and guest cache flushes
Tile-based GPUs pay for every rendering scope: ending one stores the
attachments out of tile memory and beginning the next one loads them back.
The presenter ended the scope for three things that do not actually need it.

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

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

SHARPEMU_DEFER_GLOBAL_BARRIERS=0 restores ending the scope at every flush.
2026-09-29 23:09:34 +02:00
kuba dc5a6a9d59 [SpirvTranslator] Order emulated wave64 LDS phases in every block
A guest wave64 spans two host subgroups. The guest orders one lane's LDS
write before another lane's read by executing in lockstep, so the translator
puts a workgroup barrier between the halves wherever the wave switches
between writing and reading LDS. It only did so for programs of a single
block, which left every program with branches -- an NGG geometry stage
compute program has seventeen exec-skip branches around its ES to GS LDS
exchange -- reading the other half's LDS words with no ordering at all, so
the result depended on timing.

Every guest branch condition is wave-uniform in this emulation (EXEC and VCC
tests go through the wave mask and SCC is scalar), so every block is entered
by the whole wave and may hold a barrier, exactly as the wave64 ballot
exchanges already do. A block can be reached from either phase, so its first
LDS access always starts with a barrier.
2026-09-29 23:07:28 +02:00
kuba dbb88bcf22 [Vk] Refuse push descriptors on macOS for every device through one policy
The guard that refuses push descriptors on MoltenVK keyed on the
VK_KHR_portability_subset device extension, but that extension is not
enumerated for every device: the headless device the draw replay and the
device tests create took the raw driver limit, so replays on macOS read
guest storage buffers past their first bytes as zero (MoltenVK reports a
zero OpArrayLength for buffers bound through push descriptors).

The limit now comes from one policy both the presenter and the headless
device use, and macOS, whose only driver is MoltenVK, pushes none
regardless of which extensions are listed.
2026-09-29 23:07:11 +02:00
kuba bd840c422b [Vk] On macOS, retire a GPU tick only once its submission's fence signals
MoltenVK records Metal command buffers with unretained resource references
and signals a Vulkan timeline semaphore from a Metal event scheduled at the
end of the command buffer. That event can fire before Metal has finished
executing the command buffer, so a host thread that treats the timeline
value as authoritative can free resources the GPU is still reading. Metal
API validation reports this as "destroyed while still required by a command
buffer", and the resulting use-after-free showed up as intermittent GPU
tick stalls.

Every submission that signals the timeline now also signals a VkFence, which
MoltenVK signals from the Metal command buffer's completion handler. The
timeline value a host thread is allowed to act on is clamped to just below
the oldest tick whose fence has not signaled yet, and a timeline wait is
followed by a blocking wait on the last fence covering that tick. Fences are
pooled, tracked before the submit so no reader can observe a signaled tick
without its fence, and reset only once no thread is blocked on them. Fences
are polled at all only when the timeline already claims the tick is done, so
the common path costs one queue peek. The behaviour is macOS-only and can be
turned off with SHARPEMU_FENCE_RETIREMENT=0.
2026-09-29 23:06:58 +02:00
kuba de135ddad8 [Gpu] Recognize AGC's constant buffer fill as an image/metadata clear
Octopath clears the DCC of its 1920x2200 DOF/bokeh accumulation target with a
third fill kernel: v_mov_b32 v1, 0 and buffer_store_format_x v1 at the thread
index, with only the destination descriptor in user data. It was not
recognized, the DCC fast clear was lost, and the additive bokeh pass kept
blending onto the previous frames until the target held thousands and inf,
which showed as noise and banded garbage over every 3D scene.

Program info now carries the constant a program stores when it is only index
arithmetic, one v_mov of a constant and one buffer store; a one-buffer 32-bit
formatted fill over every record with that value becomes a clear.
2026-09-29 23:06:51 +02:00
kuba fbacc494ed [AvPlayer] Start a player on its own when the title registers no event callback
Reporting a loaded source as active before Start regressed Quake, which
waits for its player to go inactive before starting its queued intro
videos. A player left loaded but unstarted stays active forever. Only a
title that registers an event callback can see the ready state and call
sceAvPlayerStart itself, so a player whose title asks for autoStart or
registers no callback now plays by itself once its source is ready,
reaches the end of the stream and reports inactive. Titles that start
playback themselves keep seeing a loaded source as active.
2026-09-29 22:57:20 +02:00
kuba c13e09130c [ShaderCompiler] Resolve V_MOVREL* with selects instead of a spilled VGPR array
A program with any V_MOVREL* instruction declared its whole VGPR file as a
512-entry private array indexed at run time, which Metal spills to thread
memory, so every register access became a memory access. Keep one variable
per register and resolve relative accesses with a select over the registers
the program names. Octopath's instanced vertex shader (1536 x 8124): 112 ms ->
6.4 ms per draw in replay, output byte-identical.
2026-09-29 22:47:40 +02:00
kuba ff9c069d44 [SpirvTranslator] Give each used register its own private variable
The translator kept the scalar and vector register files as two private arrays
(128 and 512 uints) in every module. MoltenVK lowers a private array that size
to per-thread memory, so every register read and write in every invocation went
through memory. Declare one private variable per register the program touches,
on first use; the SPIR-V is otherwise unchanged.
2026-09-29 22:47:40 +02:00
kuba 5f01c4fa6e [Gpu] Copy the mips of a GFX10 mip tail block written through its own view into the chain
GFX10 packs every mip smaller than half a swizzle block into one block at the
start of the chain. A title can render into that block through a single-level
view exactly the size of the block: Unity's screen-space reflection blur writes
mips 3 and up of its 1024x512 color pyramid through one 128x64 view. The cache
keeps that view as its own image, so the chain's tail mips kept stale data and
sampling them produced rainbow blobs over Superliminal's menu.

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