The shader disk cache's writer, started with the cache at the first
shader lookup, was detached and never stopped. It stayed parked in a
timed wait on its queue until the process exit killed it, and entries
still queued at exit were never written. A process that exited right
after its first shaders could kill the writer inside its first wait,
which takes winpthreads' global condition-variable lock, as with the
driver's own threads.
The writer is now joinable. The driver's shutdown stops it after the
other driver threads: the writer writes the entries still queued and
leaves, and the shutdown joins it. A store after that is written on the
caller, and a cache first used after the shutdown starts no writer.
ShaderDiskCache::Shutdown joins the names the cache key check has
reviewed, since it changes nothing a recompile produces.
The committer of pipelined draws, started at the first pipelined
submission, was never stopped. The driver's shutdown joined the queue
workers and left the committer parked on its queue, and a commit the
last submission left queued, such as its deferred completion, could
still run after the shutdown had reset the device. A process that
exited right after its first pipelined submissions could kill the
committer inside its first wait on the queue, which takes winpthreads'
global condition-variable lock, as with the preparation workers.
The driver's shutdown now stops the committer after joining the queue
workers and before taking the GPU lock. The committer runs the commits
still queued and leaves, and the shutdown joins it. A commit enqueued
after that runs on the caller.
Since #6060 a Windows host import of private write-watched memory stays
in the write watch only after its settle, a 4-byte GPU read that the
importing thread waits for, and only imports of 1 MiB or more are
settled: SuperTuxKart imports a 64 KiB buffer around every submission,
and the wait cost it about 19% of its frame rate. A smaller import is
compared instead, and its range leaves the write watch for good.
Resident Evil Village maps its GPU heap in 64 KiB pieces, so every
texture over that heap is compared with guest memory.
An import under 1 MiB is still compared when it is made, but its read is
now queued. The next import lookup that holds the device lock records
the reads queued since into one batch on the settle queue and submits
it, so the thread that maps and imports neither submits nor waits.
Once the batch's fence has signalled, a later lookup returns the
imports' blocks to the write watch, except where storage results are
pending over them, which waits for a later lookup. An import retired
while its read is in flight waits for that batch first.
A settled import whose range had already left the write watch, such as
a whole range imported over a piece that was compared, returns to it
the same way.
A sparse heap mirror, made for a heap with uncommitted pages, is
refreshed from the write watch through its committed pieces. The check
that a reused heap mirror's range is still watched covered the heap
mirrors acquireMirror reuses and the cached space's heap runs, not
sparse ones: once a committed piece left the watch, refreshSparseMirror
threw "a sparse heap mirror's range is no longer write-watched" from
acquireMirror's reuse and from a cache hit's sparse refresh.
refreshSparseMirror now returns false when a piece is no longer
watched. Both callers already take false as not reusable: acquireMirror
makes the mirror again, finds the range unwatched, drops the stale
mirror and leaves the range to a copy, and a cache hit gives up the
cached space.
agc_sparse_heap_mirror_unwatch mirrors a 1 MiB reservation with 256 KiB
committed, takes one committed block off the write watch and stores new
bytes there from the CPU; the next build must not reuse the mirror and
must serve the new bytes. agc_sparse_heap_mirror_unwatch_no_space_cache
runs the same without the address-space cache. Without the fix both
throw. They are registered on Linux; the Windows path uses the guest
arena as the existing sparse heap test does and has not run on Windows.
A sleep of 1 ms or less returns once the AGC driver sees a GPU batch
complete (a thread waits on the recorder's timeline semaphore), runs a
batch's completion actions, or VideoOut completes a flip. A thread
returns early at most 8 times in a run of back-to-back sleeps; the run
restarts when the thread submits AGC work or did not sleep for the
requested time. APS5_NO_SLEEP_WAKE=1 keeps every sleep in full.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Linux output passed a PT_TLS whose initialization image is outside every PT_LOAD, whose file size exceeds its memory size, whose alignment is not a power of two, or that follows another PT_TLS, straight to the dynamic loader, which reads the image from p_vaddr when it sets up TLS. The Windows and macOS writers already refuse these layouts. Reject them in ElfReader so every target, and guest modules, see the same rule. An empty PT_TLS placeholder is still accepted.
GuestMemory::Rewatch takes ranges that left the write watch back into
it. Only whole tracker blocks whose pages can be collected return:
their write-watch marks are collected and dropped, and every block is
stamped as a CPU store with a new generation, so a snapshot or a proof
made before the block left the watch reads as changed, and the blocks'
untracked hints stop applying. It does nothing outside Windows.
Driver::stop reset the device and then stopped the worker. No job could
outlive the device, because ClearPipelineLibraries waits for the
device's jobs to finish, but that was only visible inside the wait.
The worker now stops first: a running job finishes, queued jobs are
dropped, and no job runs once the device's teardown starts.
agc_driver_draw_cache_reuse checks that the driver's own shutdown
leaves no library worker running.
sceAgcLinkShaders resolves the rect-list pairing of a pipeline when the
title creates it, and three cases ended the title there although the
rect-list draw that uses the pipeline builds its pair itself:
- a pipeline without a pixel shader reached ResolveGraphicsAbi with a
null pixel, which required both stages, although the draw pairs the
front stage with the null pixel program;
- a stage with no artifact yet, such as a pixel shader that exports
stencil on a device without VK_EXT_shader_stencil_export, failed the
missing stage artifacts check;
- a pair that BuildRectListShaders refuses, such as a pixel input with
passthrough interpolation, threw from the builder.
The ABI now skips the first two, and logs the third once as a deferral
of the pixel shader without recording the pair, so the pair's draw
builds it again and fails with the builder's reason. When a draw builds
the rectangles of its front stage, only the pair of its own fragment can
fail it; the pair of another fragment that has no artifact or that the
builder refuses is left to that fragment's draw.
A new mode resolves a front stage with a null pixel, with a pixel before
it has an artifact, with a pixel that never gets one and with the first
pixel's passthrough variant, and expects one deferral with the
builder's reason. It then draws the front stage as a rect list with the
null pixel program, which succeeds although the other two pairs cannot
be built, and with the passthrough pixel, which fails with the
builder's reason.
PrepareGraphicsStages decodes each front-end stage's vertex inputs from
its AGC header before it prepares the stage, outside the catch that
defers a stage the recompiler cannot prepare. A header the decode
refuses, such as a semantic with a statically bound vertex buffer,
threw out of sceAgcDriverResolveGraphicsStagesAbi and ended a title that
creates its pipelines ahead of drawing. Registration leaves such a stage
alone when it is an NGG vertex shader that uses its workgroup, or a
mesh, hull or domain stage, so the pipeline ABI is the first to decode
it.
A decode failure now defers the stage the same way a preparation
failure does. The draw that uses it decodes the same header with the
draw's own user data and fails there with the same reason.
A new mode registers an NGG vertex shader with vertex buffer and
attribute tables and one statically bound semantic, resolves its
pipeline ABI twice inside a scope, and expects one deferral with the
decode's reason and no front-end build, then the same reason from its
first draw.
A failure stored by a shader's background registration preparation was
rethrown by every pipeline ABI call that touched the stage. The
ShaderPreparationScope that sceAgcLinkShaders, sceAgcCreatePrimState and
sceAgcCreateInterpolantMapping open settled the stages and threw first,
and ResolveShaderAbi, ResolveGraphicsStagesAbi and ResolveGraphicsAbi
threw it again from their reads of the stage. A title that creates its
pipelines ahead of drawing ended there, although the draw or dispatch
that uses the stage rethrows the same stored failure.
Settling a scope no longer rethrows. Each ABI call logs such a stage
once with its stored reason, marks it deferred and prepares the other
stages of the call as before. ResolveGraphicsStagesAbi keeps its ABI
record on the first stage that did not fail, PrepareGraphicsStages skips
a failed program, and ResolvePreparedGraphics records no rectangle for
a failed stage and never adds a failed fragment to a front stage, where
the rectangle draw of another fragment would meet it.
The compute mode no longer expects the stored failure from the ABI call.
A new mode registers a compute, a pixel and a vertex shader whose
registration preparation fails, resolves them through every ABI entry
point inside a scope twice, and expects three deferrals with the stored
reason the first time and none the second, then the stored reason from
the compute shader's dispatch.
agc_heap_mirror_unwatch registers a 2 MiB heap, a 1 MiB idle range and a
64 KiB busy range, imports the idle and busy ranges and sets the import
budget so the heap's import is refused: the first address-based build
mirrors the heap. One 64 KiB block of the heap then leaves the write
watch and the CPU stores new bytes there, so the second build must drop
the mirror and serve the new bytes, here from a copy.
agc_heap_mirror_unwatch_no_space_cache runs the same without the
address-space cache.
agc_heap_mirror_unwatched_import takes the heap off the watch through a
host import instead: once the budget holds the heap, an import that does
not keep the watch (ImportWatch::Unwatch) covers it, and the next build
must take that import instead of the cached space's mirror. It is not
registered on Windows, where an import of private memory may keep the
watch through a shared alias or a settle, which the mock does not model.
Without the fix each one throws "a heap mirror's range is no longer
write-watched": the first and third from the cached space's heap runs,
the second from the build's own heap refresh.
inflate returns Z_BUF_ERROR both when the output is full and when the
input ends early. sceZlibInflate treated every Z_BUF_ERROR with no
output space left as insufficient output, so a stream cut off after
its last data byte, such as one missing its Adler-32 trailer, reported
0x8112001C when the output capacity equalled its decoded length, and
0x811200FF with any larger buffer. Report insufficient output only
while input remains, as zlib's uncompress2 and miniz's mz_uncompress2
do and as the technical debt entry describes.
With the queue-0 draw pipeline on, the default since #1202, the packet
loop hands every label-writing packet to the pipeline and skips the rest
of the loop. A RELEASE_MEM that completes a reserved EOP flip
(sceVideoOutSubmitEopFlip, #5539) is a label write, so its label landed
but the flip branch never ran: the reservation was consumed and the flip
never presented. video_out_flip_eop and video_out_flip_eop_main failed
and video_out_flip_eop_aftergpu timed out.
Keep packets that carry a reserved flip out of the pipeline. They take
the normal path, which drains the pipeline before the flip as it does
for flip packets.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An FDE whose address range is 0 describes no instructions, and an
unwinder never matches a pc in it. The exception-table reader rejected
it with "inconsistent FDE function range", which stopped the
conversion of Limbo (CUSA01664): one of its 17328 FDEs covers 0 bytes
at 0x47dd0, where the next record starts. Such a record is now read
and left out of the function list.
A fifth ABI deferral mode, abi_deferred_undecodable_state. A vertex
shader whose specials route it through geometry (VGT_SHADER_STAGES_EN
0x2030), which registration leaves alone, is resolved through
ResolveGraphicsStagesAbi with the context and primitive registers that
sceAgcCreatePrimState passes for it. DecodeShaderStages refuses its
VGT_GS_OUT_PRIM_TYPE of 1.
The call must not throw and must log the stage once with the decode's
reason, and a second call must neither prepare nor log it again. A draw
that routes the stage through the vertex path then prepares it at its
first use without reporting it as missing a registration artifact.
A lea of a table base recorded only the exact base address as address-taken, so import slots after it in a function-pointer or vtable table were treated as dead. unused-filter=2 then stripped those NIDs and indexed calls hit unbound slots. The base walk now marks every consecutive import slot or relocated pointer starting at the base.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
An elevated Windows process can bypass the parent directory DACL while holding SeBackupPrivilege or SeRestorePrivilege. Remove both privileges before DeniedParentWatch so the test exercises the denied-open path reliably.
Co-Authored-By: Adam Franke <[email protected]>
A title's graphics pipeline ABI call decodes the stage routing and programs
from the registered state and the call's own context and primitive
registers. When that decode refused the state, the exception left the
call and ended the title, although the draws that later use the stages
decode their own state.
The decode failure now defers every stage of the call the same way a
stage whose preparation fails is deferred: each is prepared when a draw
uses it.
Two more detached driver threads were never stopped: the helpers of the
parallel compare pool, started at the first large compare, and the
pipeline library's job thread, started at the first link-time optimized
pipeline. A process that exited right after starting them killed them
inside the first waits that take winpthreads' global condition-variable
lock, as with the preparation workers.
The driver's shutdown now stops both and joins them, after the device
is reset. A compare in progress finishes first and later compares run
on the caller. The library worker finishes the job it is running, the
jobs still queued are dropped and counted off their device, and later
pipelines are not optimized in the background.
Closing a preparation pool waited until its queue was empty, but its
detached workers stayed parked on the queue until ExitProcess killed
them. A pool first used shortly before shutdown still had workers
starting at exit, inside the first waits that take winpthreads' global
condition-variable lock, which the next DLL-detach destructor of an
unwaited static condition variable then waits for forever.
The workers are now joinable and leave once their pool is closed and
idle, and Close joins them, so after the driver's shutdown no
preparation worker is running or starting.
CloseRegistrationPreparation took RegistrationPreparePool::Instance(),
which creates the pool and starts its detached workers (half the
hardware threads, at least two) when nothing was queued for preparation
yet. In a process that prepared nothing in the background, the driver's
shutdown started the workers only to close the pool, and the process
exited while they were still starting. Their first waits take the
global lock of winpthreads' statically initialized condition variables;
ExitProcess killed them holding it, and the destructor of the next
static condition variable that was never waited on spun on it forever
at DLL detach (~DeviceUseGate in ~Driver, or one of libkernel's). On
GitHub's Windows runners agc_interpolant_mapping hung that way in 15 of
60 rounds of eight test runs.
Shutdown now closes only the pools that exist, and a pool first used
after it starts no workers, so later preparations run on the caller as
they did on a closed pool.
kevent threw for any descriptor that is not a guest socket. The read end
of a pipe now gets the descriptor knote, following FreeBSD 11's
filt_piperead: data is the number of buffered bytes and EV_EOF is set
once the write end is closed. GuestSockets::Ready falls back to the new
PipeReadState for a descriptor that is not a socket, and WaitAny waits on
the pipe with ppoll on Linux or in 1 ms WSAPoll slices on Windows, only
when the queue holds a pipe. On Windows the equeue waker now starts
Winsock itself, since a queue can wait on a pipe before any socket exists.