Each case re-activates the test device's recorder, and the DCB submit re-activates the recorder of the second device it creates, so the cases no longer depend on the order they run in. PlaneFillCase checks that a plane fill before a UAV clear is overwritten by it and a plane fill after it wins.
The [dispmove] line printed refused 0 next to a rebuild count that came mostly from PrepareDispatch dropping the hit. The line now reports the refusals at stage A and at record separately, each with its reasons.
A compute kernel that stores one colour to every texel of a storage image over a depth plane (a shadow atlas clear) copied
the whole plane depth image -> staging buffer -> mirror before the dispatch and mirror -> staging buffer -> depth image
after it, although the kernel reads nothing and writes every texel.
A dispatch of the 15-word 8x8x1 image clear kernel (direct, no partial last group) whose T# lies on a depth surface now
reads its colour from guest memory under the GPU mutex, the place the kernel reads it. When the dispatch covers the whole
plane, the plane is one layer of level 0, the colour is a value the plane's format holds exactly (0 or 1 for a 16-bit
unorm view, 0 or 0xffff for a 16-bit uint view, 0 or a normal float in (0, 1] for a 32-bit view) and nothing is waiting to
write the colour, the dispatch still writes the mirror but the copy-in is left out, and the copy-out becomes a clear of the
depth image to that value. The colour is read again when the copy-out is replaced; a changed value keeps the copy-out.
Every other dispatch, stencil planes and declined values keep both copies.
agc_driver_depth_clear_hle_tests runs the clear sequences with the clear taken and with the copies, and compares the depth
image texel by texel through the mirror, the guest memory and the depth tests.
Read-only addresses are not part of the dispatch key, so the fresh build after a refused
move replaced the template that serves the other address, and the two addresses
rebuilt each other. A refused dispatch now uses or builds an entry under the exact key
(read-only bases included) and leaves the movable template alone. The [dispmove] line
counts the rebuilds after a refusal, the builds inserted beside the template and the
dispatches served by an exact entry. Restores the MarkGpuWrites comment.
A read-only V# the build clamps (size above the descriptor range limit cut to the
mapped end, stride overhang cut to the allocation end) was compared with the template
by its raw size, so an unchanged buffer counted as moved, was refused over the limit
and the dispatch rebuilt every time; a moved one with an overhang was snapshotted past
the allocation end. The build and the move proof now take the range from one function
(ShaderResources::guestBufferBytes), compare clamped with clamped and snapshot the
clamped range.
An overlay without a descriptor set is refused like one that could not be made, and
recording one throws instead of binding the template's set.
The content key of a compute stage leaves the base address and size of its
read-only guest buffers out, as a draw's key does. A template hit whose such
buffers moved is proved the way a draw's is (ShaderResources::MovedDispatchBuffers:
read-only, read in place when built, in reach, nothing pending over the new range)
and binds snapshots of the new ranges in a descriptor set of its own, kept with the
batch that records the dispatch. A refused proof builds the dispatch fresh, as
before. APS5_NO_DISPATCH_MOVE=1 turns the path off, APS5_DISPMOVE_MAX_KIB caps the
bytes snapshotted per dispatch.
A template whose storage image lies over memory that a depth surface covers is no
longer proved from stamps (fastRevalidate): cachedStorageTexture answers such a
binding with the plane's colour mirror, copied in from the depth image for every use,
and nothing stamps the appearance of a depth surface after the build. The full walk
decides, and a stale image can no longer be copied over the depth image.
Tests: Graphics.cpp (verdicts of the proof), execution DispatchMove (real device:
moved, exact, shortened, stored-over, own-output and written-range refusals) and
DispatchMoveDepth (a depth surface appearing over a template's plain storage image,
planes inside and outside the write watch, imports watched and unwatched).
The automatic window needed 8 logical processors, which a 4-core CPU with SMT passes while the waited-for thread may run on a sibling thread, and ignored the process affinity. It now needs more than 8 processors in the affinity mask (CpuTopology), falling back to hardware_concurrency.
At the Minecraft Dungeons II title the queue-0 worker, the draw committer and the compute worker
hand work to each other 49,000 times per 10 s through the GPU mutex, the committer's condition
variable and DrawPipeline::Drain, and each hand-off that has to wait costs a kernel wake-up. The
three workers are busy 21-34 % of the time: the GPU idles because the chain of hand-offs is slow,
not because a thread runs out of CPU.
GpuMutexType::lock retries try_lock for a window before it blocks, the committer polls its item
count for the window before it waits on its condition variable, and Drain polls for twice the
window before it waits for the committer. A hand-off that completes within the window costs no
wake-up. The window is 150 us on machines with at least 8 logical processors and 0 below that;
APS5_HANDOFF_SPIN_US sets it (0 turns it off). Ordering and the lock's semantics are unchanged.
The test agc_handoff_spin covers the window policy, the spin helpers and contended locks, and
docs/dev/ARCHITECTURE.md describes the spin.
The stage-A pre-sync of a dispatch listed only the surfaces that stage B reads on the CPU. The DCC
keys of an image that a recorded kernel writes were not in the list, so stage B waited for that
kernel in WaitForKeyWriters with the GPU mutex held and every other worker queued behind the wait.
At the Minecraft Dungeons II title the pre-sync fired once in 296,000 dispatch checks while
WaitForKeyWriters synchronized 400 times per 10 s.
PresyncSurfaces now also returns the key range of every image binding (PresyncSurface::dccKeys).
presync waits for a key range only while recorded work writes it, the key-store memo does not
already give its value (DccKeyWritersPending) and, under the mutex it takes for a moment anyway, the
pending stores do not give the keys either (DccKeyWriterNeedsSync, the test WaitForKeyWriters makes
before it synchronizes). Which keys the dispatch reads, and when, is unchanged: the locked lookup runs
as before and finds nothing pending.
APS5_NO_DCC_PRESYNC=1 leaves the key ranges out. The [presync] line counts key ranges checked,
pre-syncs that only the keys required and the outcomes of WaitForKeyWriters. The test
agc_driver_dcc_key_presync covers a kernel that writes the keys, a recorded fill of the keys and
nothing pending, and skips with the reasons check_skips.py expects when the device has no timeline
semaphores or no host import.
The logger thread, the bounded queue that blocks its caller, the LogSink
hook, APS5_LOG_SYNC and APS5_LOG_STATS, where the queue is flushed and
what stays outside it: direct fprintf(stderr) and printf calls are written
at once, so they can appear before older queued lines.
Lines queued behind the logger were lost when the process ended through
abort(), std::terminate, the Windows crash report or a guest _Exit,
because only driver failure, Driver::stop and a normal exit flushed the
queue.
libc keeps one end flush (LibcSetEndFlush_nid_no_patch) and runs it from
those paths. Its first registration chains a SIGABRT handler in front of
the previous one, so every abort() and std::terminate in the process is
covered, on Linux as well; with the default disposition the handler
restores it and raises again, so raise(SIGABRT) still ends the process.
Guest abort() and _Exit, and ReportCrash for the first fatal exception,
run the flush directly. AsyncLog registers a bounded flush (FlushFor, 2 s)
that polls two counters and takes no lock, so a dying process cannot hang
on a blocked stream or on the queue lock; after its first timeout it
returns at once. Flush counts queued and finished entries instead of
watching the queue and the writer flag, so it waits for the lines queued
before the call and not for a moment in which no thread is logging.
agc_async_log_fatal runs the test executable as a child that queues lines
behind a held stream and ends in abort(), std::terminate(), raise(SIGABRT)
(POSIX) or an access violation (Windows), and once more with the stream
never released. The lines of the first four must arrive in order, the last
child must end after the bound, and every child has a watchdog.
agc_async_log gains the bounded flush case.
Flush must not return while the logger still waits for a stream that
another thread holds (the regression reported on the pull request: it gave
up after two seconds), and a full queue must block its caller instead of
dropping a line. AsyncLog::Capacity becomes public so the test sizes its
load from it.
Texture creation logged one stdout line per texture and per mip through
WriteLogLine, which formatted with the mingw printf and flushed every
line on the queue worker (about 786k lines in a 25 minute cave run).
AsyncLog keeps a bounded queue and a logger thread. AGC_LOG_DEFERRED
captures numeric arguments on the caller and formats them on the logger
thread; WriteLogLine hands finished lines to a per-module sink in
libc/LogMacros.hpp that AgcDriver installs, so all AgcDriver stdout
lines keep their order and are written one flush per batch. A full queue
blocks the caller, so no line is dropped. Flush runs on driver failure,
shutdown and at exit. APS5_LOG_SYNC=1 restores the synchronous path.
Identical "deferred write-back failed" notes are printed three times per
10 s and then counted into one "repeated N more times" line. A
"[log] 10 s" line reports lines, bytes, caller time and logger time
(APS5_LOG_STATS=0 hides it).
The headless device is replaced by the windowed one at window attach and then only
lingers for in-flight workers, but it parsed its own full copy of the cache file.
It now starts empty (it still merges what it compiles into the file). Load reuses
the file buffer as the initial data instead of copying it, and save() writes the
live data directly when the file still carries the header this object loaded or
wrote, skipping the read-back and the merge cache.
A multisampled depth target is created as, or already is, the native depth owner, and a target whose HTILE overlaps a resident depth guard aliases one. Neither may have its HTILE read or rewritten by the generic store-back: the expanded words would be a guest write over the owner's range. DepthSurfaceView decides this before the store-back, which still runs before GpuMutex() and the surfaces mutex are taken. NativeMultisampleDepth binds the owner and a single-sample alias of its HTILE and checks the words stay untouched.
A fill or clear the driver recognizes over an HTILE drops the range's last-read generation, so the
next bind reads the words and stores them back even when the write tracker did not see the fill's
stores. A depth view of slice 1 or later reads the HTILE at its own address
(DB_HTILE_DATA_BASE + slice * HtileMetadataBytes); the HTILE word layout is only modelled for slice 0,
so such a slice takes the clear and the store-back only when all its words are equal and keeps its
fill-time fast clears otherwise. DepthTarget::slice carries DB_DEPTH_VIEW SLICE_START.
agc_driver_depth_fast_clear_submit: an unrecognized kernel clears the HTILE on two frames in a row and
both clears are taken; slice 1 with words that are not all equal is refused and stored back untouched;
slice 1 with equal words takes the clear. agc_driver_graphics checks the decoded slice.
The bind-time HTILE read covered one word per 8x8 tile from DB_HTILE_DATA_BASE, but main models the
HTILE as the pipe-aligned SW_64KB_Z_X metadata (HtileMetadataBytes, the extent HtileFillCovers takes):
at 1920x1080, 1856 of the 32400 tile words lie past the first tiles*4 bytes, so they kept ZMask 0
after a store-back. The read and the store-back now cover the whole metadata and run before the
surfaces mutex is taken, since both wait for recorded work over the range. The tests' HTILE arrays
hold the whole metadata.
PPSA25344 (Unreal Engine 5) decodes its depth through HTILE in its Nanite depth export after the
depth prepass and takes ZMask 0 as a tile holding the clear value; the new
agc_driver_depth_fast_clear_submit case checks that a draw after a fast clear leaves every word
expanded.
Every address-based build copied the cached address space's imported
ranges (about 9000 in Sword of the Sea gameplay) into its batch as pending
reads, only for the copy HLE's reader check to scan them. The space now
builds that list once, and a build that took the space whole notes it by
reference (Recorder::NotePendingReadList); its own ranges follow as before.
The space is immutable while its serial lives, the list equals the space's
part of InPlaceReads in its order, and the batch scans plain reads and lists
in note order, a list by bisection, so every PendingReadOverlaps and
DescribePendingRead answer and reader kind stay the same. A capture trace or
APS5_FLIP_READ_CHECK expands the list as before.
Tests: lists noted by reference answer like the same ranges noted one by one
(open and in-flight scans, kinds, a list noted twice, empty and null lists);
the space's list is shared while the registry stands and rebuilt when a range
is added or removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zero fill kernel (v_mov_b32 v1, 0; buffer_store_format_x) is also emitted with other inline
integer constants as its source. Run as compute dispatches, those writes are pending writes of
unknown value, so a later read of DCC keys over their range waits for the dispatch. A constant of
0 to 64 or -1 to -16 is now matched like the zero fill and stored as a buffer fill of that value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A join runs before any region confirms an import and a retired import lives
through the retire-hold, so refusing it for held overlaps kept the joined run
copied for the rest of the build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After an exact two-piece join, a bind across the join's end got the
joined import from importAllocation and the view failed as "rounded up to
its view range exceeds its GPU owner". UploadFinish now asks importJoined
for a region past the import's end and drops an import that still does
not cover the region, so the region is copied, as on main. The join test
binds across the end of an exact join after a third piece is registered.
Co-authored-by: TideOPL <57710757+TideOPL@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bound buffer is joined across adjacent registered ranges only when every
one of them is larger than the 8 MiB window. PPSA25344's loading binds 16 MiB
and 4 MiB buffers whose backing was registered in two to six adjacent
pieces with the same access, some of them smaller than the window, so each
use of such a buffer was copied on the CPU: up to 348 GB in the first 300 s
of loading.
A bound buffer that starts at the first crossed range's start and ends at the
last one's end now joins those ranges whatever their size. A heap grown in
pieces matches that only for a binding over exactly its current pieces, so
growth beside a joined import does not re-import it. joinedCovers, used when a
joined import retires, accepts any adjacent run of ranges.
importJoinTests binds exactly over two 64 KiB pieces of a partly filled
window and requires one import of both; a binding across them that starts
inside the first piece stays apart.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-miss component comparison allocates and walks the whole key, and
it ran under APS5_PROFILE_DRAW as well, where it cost queue time on every
resource-content miss. It now runs only with the trace variable, which is
also the only one that prints its [keymiss] lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 9-word kernel #3816 matches stores one dword per thread, loaded with
s_buffer_load_dword through the descriptor in user[4..7]. Run as a
compute dispatch, its stores are pending writes of unknown value, so a
later read of DCC keys in that range waits for the GPU.
fillBuffer now takes it as a uniform buffer fill when all of these hold:
- the output descriptor's format is 32_UINT, 32_SINT or 32_FLOAT (20-22),
so buffer_store_format_x stores the loaded dword unchanged;
- the value descriptor covers the loaded dword: num_records times the
stride (num_records alone when the stride is 0) is at least 4 bytes,
the range the recompiler gives a scalar buffer load;
- exactly 8 user SGPRs and group id X enabled, so the group id is s8;
- a direct, non-partial dispatch, 64x1x1 threads, one group in Y and Z,
and a 16-byte aligned range of whole 16-byte units.
The value is read when the fill is recorded, after the labels queued
before it, as the pattern-fill kernel's source is. When the value is one
byte repeated and that byte is a DCC key code, the fill goes into the GPU
key-store memo whatever ClassifyFill reports: the memo answers a key
read only while this fill is still the newest pending write over the
keys, and a fill over a color target's keys is not a storage image's
keys fill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A build mapping only the cached address space but writing through V#s got a
table keyed by its writes: a new key for nearly every set of written
addresses, so each such build merged the space's imported ranges, stored them,
then hashed and compared them in BdaResources.
AddressRanges splits only ranges without write permission around the written
intervals. When no written interval overlaps such a range of the space, its
result is the space's own ranges, so the build binds the space's table by the
space serial, as a build without writes does. A write overlapping a range
without write permission keeps the per-writes table.
Test: builds with one and two writes inside a writable import bind the serial
and the ranges of the build without writes, equal to AddressRanges(), and keep
the read-only import without write permission.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8 MiB window, so a range larger than the window is always imported alone. A
buffer bound across two such ranges then has no import and is copied whole on
every use: PPSA25344 binds a 368 MiB buffer across a 320.75 MiB and a 47.3 MiB
range and copies it on almost every frame.
When UploadFinish meets such a region, it imports the run of the ranges the
region crosses, provided every one of them is larger than the window, and
retires their own imports. It does this before any region of the build
confirms an import, so the retire moves the epoch ahead of every pointer the
build would use. A range that large is registered whole, not grown in 64 KiB
pieces like a heap, so the joined import is not re-imported as a heap grows; it
stands in refreshImports while exactly its ranges stand. A later join that
reaches a joined import takes it in, so two joins that share a range never
retire each other in turn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The relative-ancillary test expected the whole-word read through
v_movrels to be refused, which was main's behaviour before the word was
composed. It now converts that read to float and exports it: the
composed word of a triangle draw is 2 (primitive class 2, sample 0,
layer 0), so the pixel is 255, with and without the implicit position
inputs in front of the ancillary VGPR. TechnicalDebt's relative
addressing entry no longer says that whole-word uses throw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A packed ancillary word that still reaches an export, or any other read
the masked-select pass cannot drop, no longer stops the recompile. After
that pass every remaining use gets the composed word: the primitive class
at bits 0-1, SampleId at bits 8-11 and Layer at bits 16-28, with every
other bit zero. Those lanes do export the ancillary VGPR on the console,
so the word has to be right, not just present.
The primitive class (0 point, 1 line, 2 triangle, 3 rectangle) comes from
the draw. RasterPrimitiveOf maps rect lists to 3, line lists on the vertex
path to 1 and everything else the driver accepts to 2, because its
geometry and tessellation paths only output triangles. It reaches the
shader as pipeline specialization constant 8233, the way dual-source
blending does: a request field that is folded into the specialized
module and is part of the result memo, the draw key and the serialized
request (version 17).
Only shaders that read the class pay for it. ShaderInfo records the read
(disk cache format 28), and only those shaders put the class in their
module key, so a shader that reads just the sample or layer field keeps
one module for every primitive class. The draw sets the class only when
the pixel shader loads the ancillary VGPR, so other draws keep the draw
cache entries they have today.
The VRS rates (bits 2-5) and the eye id (bit 7) are 0 because the driver
supports neither VRS nor multiview. SampleId is 0 because the driver
rejects MSAA and per-sample shading, and Layer is 0 while it ignores
vertex layer exports. TechnicalDebt.md records the layout sources and
these assumptions.
Tests:
- pixel_ancillary builds an export to target 0 whose two channels select
between a packed value and a phi carrying the word. It checks the word
for each primitive class, sample and layer, checks that the three
shapes the masked-select pass keeps now compile with the composed word,
and that a program whose select the pass drops reads no class.
- agc_driver_pixel_ancillary_word exports the word from a draw and reads
back 2, 3 and 1 for triangle, rectangle and line. A shader that reads
only the sample field shares one module across the classes, and the
whole-word shader gets one per class. It also checks RasterPrimitiveOf.
- agc_shader_memory covers the version 17 round trip, and
agc_shader_disk_cache the new flag.
Both ancillary tests fail with the old refusal when the composition is
disabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
APS5_TRACE_SUMMARY_EVERY=<seconds> appends one line to aps5-summary.jsonl
every <seconds> seconds, at a present, from the per-scope totals the trace
threads already keep. No event buffer is allocated unless a window switch is
set as well; then the window records its full trace on top of the intervals
and its own summary line holds only the time inside the window. Each line has
the fields of the window summary line for its interval plus interval_start_s
and interval_end_s.
tools/trace_digest.py prints a summary file with interval lines as one row
per interval with a text sparkline of the frame p99 and the GPU busy time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trace gets a gpu track: one slice per recorder batch from its timestamp
pair, and with APS5_PROFILE_GPU=1 one per command class range under the
[gputime] class names. The GPU clock is mapped onto the CPU clock from
calibrated timestamps (VK_KHR_calibrated_timestamps or the EXT version,
enabled only when a trace switch is set) taken when the window opens and
when it closes. A traced thread's wait for the GPU mutex becomes a
GpuMutex.wait.<site> slice, and the shader preparation threads are traced
with smaller buffers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zlib in the tree (libjpeg-turbo's) is built with NO_GZIP, so
deflateInit2 refuses the gzip wrapper (windowBits 31) and no trace was
written. The trace file now writes the gzip header, a raw deflate stream,
and the CRC-32 and size trailer itself. The writer's result line is flushed,
since stderr is buffered in a title process. The file writing moves to
TraceFile, and a test inflates a written file and checks its CRC-32 and
size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
APS5_TRACE_FRAMES=<start>:<count> (or APS5_TRACE_SECONDS=<start>:<length>)
records every PerformanceTimer scope and stage of the queue workers and the
presenter inside the window as complete events, named after their frame
timing keys, into a preallocated buffer per thread. Slices under 20 us are
counted per thread instead of written; a full buffer stops that thread and
sets trace_dropped. When the window closes, a ProfileOutput writer thread
writes aps5-trace-<commit>-<pid>.json.gz and appends one line to
aps5-summary.jsonl. Without either switch a PerformanceTimer costs one more
predictable branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/trace_digest.py reads an APS5_TRACE_FRAMES capture (aps5-trace-*.json.gz)
and prints a markdown report of a few KB, or the same data as JSON: the
summary line from aps5-summary.jsonl, the slowest frames with their largest
slices, the busy, wait and untraced time of each thread, the largest slices
per frame, the longest waits, and with --stacks the hottest frames of an
APS5_SAMPLE_WORKER collapsed-stack file. Python standard library only; a
unittest checks every section on a small synthetic trace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
APS5_SAMPLE_WORKER=<file> now also writes <file stem>.collapsed.txt: one
"frame;...;frame count" line per distinct stack of queue worker 0, root
first, frames as module+0xRVA, which speedscope.app and flamegraph.pl
open. Walks go up to 64 frames, and nothing allocates while the worker is
suspended.
The libraries are built with -fno-asynchronous-unwind-tables, so most of
their functions have no .pdata entry, only a DWARF record in .ehfram, and a
walk ended at the first such frame. The worker's walk now steps those
frames through their record, with the CIE/FDE decoding and the call frame
instructions of libc's unwinder, which move from Unwind.cpp into its
header unchanged. A module is indexed after the worker resumes, the first
time a walk meets it, and every register the step restores is read inside
the worker's stack. The process sampler (APS5_SAMPLE_THREADS) keeps its 12
frames through .pdata.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.github/ISSUE_TEMPLATE/performance.yml asks for the title and scene, what is
slow or what changed, the summary lines and traces of APS5_TRACE_FRAMES
captures, the trace digest and optionally the worker's collapsed stacks,
and states the capture protocol: warm caches, the same scene and settings,
no other profiling switch, the same machine and session. The pull request
template and CONTRIBUTING ask a pull request that claims a change in speed
for the summary lines of main and of the change, captured the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kernel #2081 matches stores each pattern dword with
buffer_store_format_x, so what lands in memory depends on the output
descriptor's FORMAT (bits 12-18 of user[3]). Only 32_UINT, 32_SINT and
32_FLOAT (20-22) store the dword unchanged. Other formats convert it,
and an 8- or 16-bit one writes 1 or 2 bytes of every 4-byte element, so
a buffer fill would also overwrite the bytes in between. Such a dispatch
now goes to the recompiler.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
referencedIds() took every operand word of every instruction as a
possible id, so a literal that happened to equal the id of a dead folded
constant or an unreachable block kept it: the OpTypeInt width 32 kept a
dead constant %32, LocalSize literals kept dead labels.
Visit only the <id> operands of each instruction. Literal operands are
skipped for the instructions that carry them: type widths, counts and
image parameters, constant values, storage classes, memory access and
image operand masks (their trailing ids still count), composite and
shuffle indices, OpArrayLength members, extended instruction numbers,
entry point names, execution modes, merge and function controls, branch
weights, OpSwitch case values (two words for a 64-bit selector) and
group operations. An instruction without a listed layout still counts
every word, so an unknown instruction can only keep debris, never drop
something in use.
The SpirvModule case now also folds a switch whose dead constant has id
32 and whose dead blocks' ids appear as LocalSize literals; it fails
without this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux with write watching (uffd write-protect), the first import asks the device whether a GPU
write clears the protection (ProbeImportWriteProtection): it records a vkCmdCopyBuffer and a
vkCmdFillBuffer on the import and submits them. The test's mock device records no commands, so
agc_host_import_retire failed with "missing Vulkan function: vkCmdCopyBuffer" before it reached
the retire (RADV, reported on #4515). The test now sets APS5_WRITE_WATCH_IMPORTS=unwatch, so its
imports are compared as on Windows and the probe, which a mock cannot answer, does not run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EmitBdaWrite, used for buffer stores through GPU-selected descriptors, recorded an
Unaligned fault and dropped every 32-bit store whose address is not 4-byte aligned.
EmitBdaStore and the BDA reads already take the byte path for unaligned addresses, and
raw buffer accesses use the exact bytes, as RDNA does in unaligned mode. The unaligned
case now stores the four bytes one by one like the 8- and 16-bit stores of the same
function.
Sword of the Sea hit this about once per frame from one compute store, so four bytes
were lost and a fault was reported each time.
The BDA store execution table now also runs through EmitBdaWrite; its unaligned dword
cases failed before this change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RetiredImport now owns the dropped import's map node (std::map::extract), so releaseParkedImports
takes the parked import's range from node.mapped().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agc_formatted_buffer_once required two more formatted loads from a known
descriptor to add less than a tenth of the module. The module was mostly
what specialization left of the folded format switch. Without those
leftovers it has 1,219 words and the two call sites add 225, so the
ratio failed while nothing was duplicated.
Require the two extra loads to add fewer words than the first one,
which also brings the shared function. A second copy of that function
per load still fails this check, as well as the function count checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpecializeSpirv hoists every instruction it folds into a module-level
constant and keeps that constant after its last use has folded away
too. It also leaves each block that a folded branch cuts off as
OpLabel; OpUnreachable. A formatted buffer helper's 77-case format
switch leaves about 1,800 such blocks.
Emit only the folded constants that something still refers to, and
remove OpLabel; OpUnreachable blocks that no instruction names. Merge
and continue targets of live constructs are named, so they stay.
The specialized modules lose words, not behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
refreshImports and importAllocation drop an import whose registered run changed (a title growing a
heap) while the GpuMutex hold that drops it may still hold a HostImport pointer from before: a
build's in-place region (UploadFinish flushes pending images between regions, and a flush may
reconcile the imports), an indirect argument or a vertex import. The holder went to the batch open
at the drop, or was destroyed at once with the recorder idle, and the map node the pointer names
was erased. A Submit or Sync later in the hold put the command using the old buffer or address into
a later batch, so the import could be freed before that batch ran and the GPU read an unmapped
page (VK_ERROR_DEVICE_LOST; VK_EXT_device_fault reports READ_INVALID inside the dropped import).
The dropped import's map node now moves into its holder (std::map::extract), so those pointers stay
valid, and the holder is kept by every batch the same hold opens (Recorder::KeepThroughHold), so
whatever the hold records with them completes first. Drops for pages the guest releases (the lease
waiter) keep the old lifetime, and the waiter lets go of what an ended hold kept so it does not pin
the ranges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two deterministic, GPU-free checks (mock device) of the import lifetime when refreshImports drops
an import whose registered run grew, while the GpuMutex hold that took the import may still record
with it: with the recorder idle (the import was destroyed at once) and with a Submit between the
keeping batch and the batch that records the use (the import was destroyed when the keeping batch
completed, with the later batch still in flight).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(agc): keep the driver's per-thread scratch out of thread_local teardown
With MinGW's emulated TLS, a thread_local with a destructor in a DLL is
destroyed after its storage is gone: when a std::thread ends, winpthread
runs libgcc's emutls key destructor, which frees the thread's thread_local
storage, and only then does the loader's DLL_THREAD_DETACH run the DLL's
tls_atexit list, so the destructors run on freed memory. The queue worker
touches four of them in libSceAgcDriver (the dispatch-ahead map, the
pending waits, the draw-snapshot compare vectors and the image decode
memo), and Driver::stop's join of the worker then corrupts the heap.
Under cdb's checking heap agc_driver_draw_cache_reuse stopped at the
worker's exit with heap corruption in all 127 runs, with graphics
pipeline libraries, with APS5_NO_PIPELINE_LTO=1 and with APS5_NO_GPL=1.
Without it the damage shows intermittently, as exit 0xc0000374 (2 of 143
runs with a cold NVIDIA shader cache) or as a fault in whatever allocates
next, such as NVIDIA's compiler on the optimized-link thread (#2980).
These objects now live in HostThreadLocal, fiber-local storage that owns
them, as the recorder's buffers do since e424b6b7b.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(libs): keep thread_local objects with destructors out of module teardown
The teardown order behind the driver's heap corruption holds in every
module: with MinGW's emulated TLS a thread's thread_local storage is freed
before the module's DLL_THREAD_DETACH runs its thread_local destructors,
so they run on freed objects. PreciseSleepUs's timer (used, among others,
by the video-out vblank thread) then closes a stale handle; libkernel's
directory path, socket waker and UUID random device, libSceRandom's random
device and libScePosixForWebKit's random device and host lookup free or
close stale objects. They move to HostThreadLocal. The other thread_locals
in the modules are trivially destructible or only built for Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(libs): fail when a module registers a thread_local destructor
prx_thread_local_destructors lists the symbols of every built module and,
where MinGW's thread_local destructor support is linked in, disassembles
the module for calls to __cxa_thread_atexit outside libc's registration of
guest destructors. A module that keeps a thread_local with a destructor
fails it with the function that registers one: with main's libSceRandom
it reports sceRandomGetRandomNumber, and with this branch all 122 modules
pass, libc being the only one that links the support.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(agc): keep the image memo behind its pointer
The image decode memo went into HostThreadLocal as the whole array. On
Linux, where HostThreadLocal is a plain thread_local, that put 1024 memo
entries into every thread's TLS block instead of one pointer. It keeps
its unique_ptr as before, now owned by HostThreadLocal, so Linux builds
the same thread_local as main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dean Galvin <dean.galvin@live.com>
The packed integer ops (v_pk_add/sub/mul_lo/mad/min/max/shift) read their
16-bit sources without the VOP3P negate bits. Hardware negates a 16-bit
source by flipping bit 15 for integer ops too, and Sony's compiler relies
on it: UE5's TSR history update clamps its -1 neighbour offsets with
v_pk_max_i16 v14, neg(lit 0x7fff), v14, i.e. max(-1, minY - y). Without
the modifier that became max(0x7fff, x), the x-1/y-1 taps of the 3x3
input filter read out of bounds as zero, and the history lost ~16 % of
its energy every frame (Sword of the Sea's missing glow).
The new execution test covers every packed integer op with neg_lo and
neg_hi, op_sel interplay, clamp and the TSR literal case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013fa2MbmdFLVr8YHuVjucHE
A FLAT_SHADE input without pass-through that a pixel shader reads
through v_interp_mov_f32 P10 became an explicit per-vertex input and
read vertex 1 minus vertex 0. With the last provoking vertex the
pipeline then threw ("last provoking vertex with explicit per-vertex
flat interpolation is unsupported"), which stopped all three
ps5-opengl titles in the catalog.
ps5-opengl reads every flat input through P10 when GL's last-vertex
convention is in effect, and its driver passes the GL 4.6 CTS on a PS5
(#1413). Such a read now returns the provoking vertex's value, as P0
does, and only a P20 read keeps a flat input per vertex. The lane was
not measured on the console itself, so FragmentParameter records it
and a pipeline using the first provoking vertex throws.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The third structurize pass, which routes loop exits that leave the loop
before they meet, runs only when the plain and the privatized pass
throw, and its result was used without a check. Merged with #2471, it
turns some of those budget failures into graphs that spirv-val
rejects. In this one, selection 12 of the result lies in the selection
at 7 and branches to 7's merge block 13:
{{1}, {2, 3}, {4}, {5}, {6, 7}, {}, {8, 9}, {10, 11}, {1}, {12, 4},
{5}, {13}, {8, 14}, {15, 16}, {7}, {13}, {4}}
block 12 exits the selection headed by block 12, but not via a
structured exit
hasStructuredConstructs checks the third pass's result against the
structured control-flow rules of the SPIR-V specification (2.11), with
dominance and post-dominance taken over branch, merge and continue
edges. Back edges come only from a loop's continue block. A conditional
without a merge breaks or continues its innermost loop. A header
strictly dominates its merge, and a loop header its continue block. A
construct contains the merge of every header inside it and is entered
only through its header. It is left only to its own merge or to the
innermost loop's merge or continue block, and a continue construct only
to its loop's header or merge. When the result fails, the second pass's
error is reported, as when the third pass throws. The first two passes
are unchanged.
Differential fuzz with the structured-program generator and random
reducible graphs. Oracle: exact post-dominators,
requireStructuredBranches, merges dominated by their headers, the
original's executions, and spirv-val (SPIRV-Tools v2026.4).
- Against 9fa7d06a: the same outcome and output on every graph. That
covers 100,000 random graphs at 0 words and with one block at 1000,
and 30,000 programs at 16 and at 1000 words per block, each also with
one block at 1000.
- Merged with #2471, the same 30,000 programs: the third pass had made
41, 42 and 45 of them invalid (16 words, 16 with one block at 1000,
1000 words). These end in #2471's own error again, and nothing else
changes. The 1, 1 and 18 graphs that d59a9167 merged with #2471
already structures invalidly come from the first two passes and stay
as they are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A loop whose exit paths leave the loop construct before they meet got
the nearest common post-dominator of its exits as merge, and
splitSharedMergeBlocks never separated it. An exit continues the
enclosing loop, so its path to that merge runs through the outer
header again, or an exit enters a block that a branch before the loop
also reaches. splitOneLoopMerge gave the loop a forwarding merge block
each round, the other path still bypassed it, and the post-dominator
stayed the same block until "CFG shared merge splitting exceeded
budget". These shapes from #2642 fail that way on main, on d59a9167,
with #2471 and with both:
{{1}, {2}, {3, 4}, {2, 5}, {}, {1}}
{{1}, {2, 3}, {4, 5}, {}, {3, 6}, {1}, {2}}
{{1, 3}, {2, 4}, {3, 1}, {4}, {}}
{{1, 2}, {3, 2}, {4}, {4, 1}, {}}
{{1, 2}, {4}, {3, 1}, {4, 2}, {}}
When the plain and the privatized pass both fail, Structurize now runs
a third pass. It routes such a loop's exits through one junction with
routeLoopExits, as loops whose exits have no common post-dominator are
routed already, and gives an outer loop whose only latch lies in an
inner loop its own continue block. In that pass a loop exit conditional
that is neither a direct break or continue nor has an exit tail of its
own is structured as a selection, and an exit tail that the other arm
reaches is not used as one. Without that, graphs the routing unblocks
keep a conditional without merge (the rule from #2762) or a selection
entered past its header. Graphs that the first two passes structure
are unchanged, and when the third pass fails too, the second pass's
error is reported.
Differential fuzz against main, reducible graphs with 5 to 9 blocks,
each with 0 estimated words per block and again with one block at 1000
words (the privatizeSharedReturns path). Oracle: exact post-dominators,
requireStructuredBranches, merges dominated by their headers, the same
executions as the original graph, and spirv-val (SPIRV-Tools v2026.4,
Vulkan 1.3) on a module emitted from the structured graph.
- 200,000 random graphs. main structures 178,743 (0 words) and 178,750
(1000 words); the output is identical on all of them. Of main's
14,256 and 14,311 budget failures 13,527 and 13,596 now structure,
and 2,576 and 2,565 of its refusals. No new output is invalid. The
invalid outputs are main's 530, plus at 1000 words one graph that
d59a9167 already turns from a budget failure into a back edge to a
block that is not a loop header.
- 24,000 structured programs from orishamir/anyps5-cfgfuzz's
generator (if/else, loops, break, continue, return). main structures
21,093 and 21,022; the output is identical. 619 of 2,448 and 634 of
2,429 budget failures now structure. No new output is invalid; the
invalid outputs are main's 92.
agc_structurizer has the five shapes; requireOriginalBranches now
follows route variables. agc_shader_control_flow has two RDNA programs
of the first two shapes. The non-GPU tests and the 253 agc_driver GPU
tests pass on Windows (RTX 5070 Laptop).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A selection in a loop whose arms both reach their nearest common
post-dominator within the iteration got that post-dominator as its
merge. When an arm can also continue the loop, the post-dominator
follows the back edge: the continuing path re-enters the loop header
and reaches the outer selection's merge without passing the block where
the arms meet. In the loop {1}, {2, 4}, {3, 4}, {4, 5}, {1, 6}, {1}, {}
from #2642 (latches 4 and 5) the selections at 1 and 2 both merged at 4.
splitSharedMergeBlocks gave 2 a forwarding merge block, recomputed 4 for
it because 3 continues through 5, and repeated until "CFG shared merge
splitting exceeded budget: originalBlocks=9 currentBlocks=45
splitBudget=36".
When that post-dominator is inside the loop construct, findSelectionMerge
now takes the arms' nearest common post-dominator over the paths of the
iteration that reach it, so paths that leave through the continue block
or the loop's merge no longer move the merge past the arms' join. When
no arm leaves early the result is the same block as before. The loop
structures with two forwarding blocks and no clones or route variables;
so do three latches, the selections on the false side, and an if-else
whose one arm continues.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With f16 rounding toward -infinity, v_fma_mixlo_f16 and v_fma_mixhi_f16
gave +0 for an exact cancellation such as 1.0 * 1.0 + -1.0, because the
f64 sum is computed to nearest. RDNA2 gives -0 there, as IEEE 754 does
for an exact zero sum rounded toward -infinity, measured on gfx1036 in
#2631. An exact zero sum now gives -0 in that mode unless both the
product and the addend are +0. A nonzero result that rounds to zero
keeps its sign, so a tiny positive sum still gives +0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The f16 forms of v_fma_mix threw for any FLOAT_MODE with an f16 rounding
other than to nearest even. ps5-opengl's shaders run with FLOAT_MODE
0xcc, f16 rounding toward zero with f16 denormals kept, so PPSA99005
stopped at its first draw (#2631).
The exact product-sum, already computed in f64 with round to odd, is now
rounded once in the mode's direction: scaled by the f16 step at its
exponent, truncated, floored or ceiled, and scaled back. A result beyond
65504 becomes infinity or 65504 as IEEE 754 gives for the direction. On
RDNA2 (gfx1036) -1.5 * 2^-24 + 2^-126 gives 0x8002 with round toward
-infinity, and this gives the same. Flushed f16 denormals still throw.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The half-wave scan rules for 32-lane hosts made lane 31 of a matched
scan a subgroup reduction and threw on a device without subgroup
arithmetic, where the read used to build as a shuffle of the scan. Such
a device now keeps the shuffle for lane 31; lane 63 still reads the
identity, which needs no arithmetic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wave64 vertex program with the culling scan of #2783 (lane < s3's
primitive count, row_shr bound_ctrl:1 OR scan, v_permlanex16_b32,
v_readlane_b32 of lanes 31 and 63) compares the wave OR with the ballot
of its keys and collapses its triangle when they differ. On a 32-lane
device every pixel of the grid must be covered.
Before the s3 change the program does not build on 32 lanes; with a
shuffle from lane 31 instead of the subgroup reduction no pixel is
covered, as the device leaves lanes of each vertex warp idle. Other
subgroup widths skip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A wave64 vertex program runs at one guest lane per invocation, so on a
32-lane host subgroup its lanes 32-63 have no invocation. The vertex
entry still set s3 = 64 | 64 << 8, telling the program that its wave
holds 64 vertices and 64 primitives. An NGG culling program then keys a
full-wave OR on lane < s3's primitive count, lanes 32-63 hold real cull
results, and its v_readlane_b32 of lane 63 cannot be built (#2783).
The entry now sets s3 = 32 | 32 << 8 for a wave64 program on a 32-lane
host, the wave the host runs; the console launches waves with fewer
vertices and primitives. The host subgroup width reaches the vertex
input the way it reaches the compute and mesh inputs
(BuildShaderStageInputInfo from HostSubgroupSize), and the source,
prepared-shader and disk cache keys already carry it for every stage.
Programs that do not read s3 are unchanged; wave32 programs and other
host widths keep the full wave.
The keys are then 0 in lanes 32-63, so the read is sound. On a 32-lane
host, when the existing half-wave scan rules fail, the scan may also:
- run under s_mov_b64 exec, -1 (EXEC constant true);
- use bound_ctrl:1 row_shr steps when 0, the fill, is the identity (OR,
add, unsigned max), also into another register, whose old value never
shows since such a step writes every lane;
- take keys shown to be 0 in lanes 32-63: built from 0 through selects
under a lane id or v_mbcnt of -1 below at most 32, v_cmp_ne_u32 against
0, ORs, DPP and permlanes, in stages whose entry EXEC is every lane.
Lane 63 is then the identity and lane 31 the subgroup reduction of the
host's invocations. A shuffle from lane 31 is not enough: an RTX 5070
runs the vertex warps of a non-indexed triangle list with 30 of their 32
invocations. A v_readlane_b32 outside the host subgroup of another value
that is 0 in lanes 32-63 reads 0. Only reads that used to throw change,
besides lane 31 of such scans.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Following the ImageRead case, every image operation was checked against
the two lists the masked-lane walk uses: accesses guarded by a trailing exec
operand, and lane-local values.
- ImageWrite carries its exec last like StoreBuffer, and EmitWriteOp stores
only inside an if on it (bindless tables and FMASK refuse the store). It
joins the exec-guarded list.
- A gather with an LOD of its own (gather4_l, gather4_lz) or outside a pixel
shader reads only its lane's address, like the explicit-LOD samples the
walk already follows. A pixel gather without one still keeps the select,
since on the GPU it takes its LOD from the quad.
- ImageQueryDimensions returns the size of its lane's level of the image and
is lane-local.
Left out on purpose:
- ImageQueryLod and implicit-LOD samples read their quad.
- ImageBvhIntersectRay has a trailing exec operand, but its emitter ignores
it and records faults in every lane.
- Image, buffer, shared and address atomics are guarded by their exec in the
emitter too, but the test from 60c65521 pins them as keeping the select,
and no refused shader has needed them so far.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kena: Bridge of Spirits (PPSA01802) refuses ten pixel shaders with
packed pixel ancillary input has an unsupported live use (used by SelectU32 ? ?)
at any visit limit. All ten have 30 to 33 phis on loop back edges (reported
on Linux with an RX 9070 XT).
Two rules stopped the proof at a loop, and the first one fires before the
second is ever asked:
- The walk from the select gave up at the first use inside a loop. A value
derived from the select reaches a loop phi as soon as its register is
written inside the loop, because the masked write becomes
Select(loopExec, new, phi). A loop that leaves exec alone needs nothing
more than this.
- An exec that a loop narrows and carries around its back edge is a phi
inside the loop, and such phis counted as not implying the write's exec.
The walk now follows uses into loops. A phi inside a loop implies the
write's exec when all of its incoming values do, where the phi itself counts
as implying it while its back-edge values are checked. This is sound because
each rule of the proof takes a lane bit of one value from a value computed
earlier: a phi's value on each iteration is the incoming value computed
before the edge was taken, a set lane of an And is set in both operands, a
set lane of a ballot is a lane whose predicate held. If any value in the
proof had a lane set where the write's exec is clear, the earliest such
value would take that lane from an earlier value of the proof that is set
there too, which contradicts it being the earliest. Two conditions keep this
true:
- The write's exec must be computed once, outside every loop. Otherwise a
phi can carry a ballot of it from an earlier iteration, which is not the
value the select used. Masks computed inside a loop keep the old rules.
- A result may only be kept if the assumptions it used held. When a value
that was assumed turns out not to be implied, every result recorded since
it was assumed is dropped. Results that say "not implied" are kept; they
only make the proof refuse more.
A lane that is turned back on inside the loop makes the back-edge exec wider
than the write's exec, so the phi is not implied and a read under it keeps
the select. A lane that is turned back on but never reads the derived value
does not.
New tests cover a loop that carries the write with exec saved, narrowed and
restored (read inside and after the loop), a loop that leaves exec alone,
and the helper-only ancillary select feeding such loops. The refusing tests
each fail when the matching check is removed: a revived lane that reads the
write inside the loop (ignoring back-edge values), a result proven under an
assumption that later fails (keeping it), and a write mask computed inside
an earlier loop (allowing such masks). Two existing tests expected a loop to
keep the select although every read is under the write's own exec; they now
expect it to go, next to new counterparts with a wider exec.
On the Taxi Chaos 2 requests, compute shaders with a loop inside a branch now
lose the branch's masked writes: 0x194000b400 removes 207 selects instead of
36, 0x19002bd200 111 instead of 28, and seven more gain 5 to 43. In
0x16400cf700 the 26 new removals are writes under the branch condition
tid < count; the loop's exec starts as a ballot of that condition, only loses
lanes at the latch, and every store and atomic inside the loop runs under it.
No proof on these requests had to drop a result.
Walks now go on into loops, so the pass takes longer on loop shaders, but it
stays below main. Time in MaskedSelectEliminator per prepare (median of 3,
the pass runs twice per prepare), 54 Taxi Chaos 2 requests, one run:
shader previous commit this commit main 5edae353
0x1ac0413000 334 ms 349 ms 424 ms
0x19002bd200 38 ms 103 ms 112 ms
0x19c016aa00 9 ms 74 ms 16 ms
0x194000b400 13 ms 31 ms 32 ms
all 54 547 ms 805 ms 977 ms
0x19c016aa00 removes nothing more; its walks only end later inside its
loops. The 144 requests of the NVIDIA dump go from 1283 to 1589 ms and gain
removals in the copies of the same shaders. 197 of the 198 requests prepare,
as before.
Related: #2548
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LEGO Horizon Adventures (PPSA14632) still refuses a pixel shader with
packed pixel ancillary input has an unsupported live use (used by SelectU32 ? ?)
although it has the form the previous commit handles: the only whole-word
reader is SelectU32(IEqual32(HelperInvocation, 0), BitFieldUExtract(x, 0, 11),
ancillary), the exec words are Ballot(not helper) & Ballot(true), and there
are no image ops, stores or loop phis. About 10,250 IR values derive from the
word, so the walk from that select stops at its fixed limit of 4096 visited
values and the select is kept. With the limit raised to 8192 the shader
prepares (reported on Linux with an RX 9070 XT).
A walk may now take max(4096, 2 * instructions) steps. A step is a value
visited for the first time or an implication evaluated for the first time.
The walk only visits values derived from the select, at most one per
instruction, so a walk is only cut short when the implications it evaluates
cost as much as the program itself. On the 198 Taxi Chaos 2 requests no walk
needs more than 0.79 * instructions steps. One walk stays linear in the
program, O(instructions + operands): each value is expanded once, and with
the per-walk memo each implication is evaluated once.
The walk is also cheaper, so that the larger limit costs nothing:
- visited values are marked in a table indexed by value id instead of a hash
set, and the table, the work list and the memo are reused across walks;
- blocks outside loops are looked up in a table indexed by block id instead
of a hash set. This lookup ran once per use and was most of the pass's time.
Time in MaskedSelectEliminator per prepare (median of 3, the pass runs twice
per prepare), 54 Taxi Chaos 2 requests:
shader before 40a8a5a7 this commit main 5edae353
0x1ac0413000 689 ms 285 ms 401 ms
0x19002bd200 102 ms 34 ms 100 ms
0x18c07bf000 191 ms 52 ms (refused)
all 54 1294 ms 484 ms 919 ms
0x1ac0413000 and its three copies in the NVIDIA dump now remove 659 selects
instead of 625: their walks no longer stop at 4096. No other request changes
what it removes. 197 of the 198 requests prepare before and after; the other
one fails the same way in both (v_readlane_b32 of lane 63 on a 32-lane host
subgroup).
Related: #2548
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Taxi Chaos 2 (PPSA30279) pixel shader 0x18c07bf000 is refused when it is
registered, and the title stops:
packed pixel ancillary input has an unsupported live use (used by SelectU32 ? ?)
The shader reads the ancillary VGPR (v2) once, with v_bfe_u32 v3, v2, 16, 11,
which #1904 lowers to Layer. The next write to v2, v_rcp_f32 v2, s33, runs
before any kill under the initial exec and becomes
SelectU32(IEqual32(HelperInvocation, 0), rcp, ancillary). The raw word
therefore survives only in helper lanes. The shader has no s_wqm, no DPP,
readlane or swizzle, no stores, and only *_lz samples and gathers. Every
helper-lane value that comes from v2 is dropped by a later exec select, a
compare that is ANDed with exec before its ballot, or an export whose exec
excludes helper lanes.
MaskedSelectEliminator exists to remove selects like this one, but it could
not prove the case. The kill paths save and restore exec, so the later execs
are phis at block merges, and the AND chains the kills build go deeper than
its eight-level limit.
When it checks whether a later exec implies the write's exec, it now:
- accepts a phi when the phi and each incoming value lie outside loops and
every incoming value implies that exec;
- pairs low and high exec words merged by phis in one block, predecessor by
predecessor;
- memoises results for each select and follows up to 64 levels. A cycle
counts as not implied.
Once the select is gone, nothing reads the ancillary word, so no bit of it
is produced. A select whose false lanes can still be observed is kept and
the shader is still refused. That covers a data test that leaves the word in
non-helper lanes that later export, an export under the full wave, and an
unmasked ballot.
Related: #2548
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2839 decides f32 product tininess after rounding, in all four f32
round modes, from the exponents and a UMulHi and IMul of the
significands, and builds the flushed value from them. To nearest even,
the mode of shaders without a known float mode and of the 0xc0 most
titles use, one multiply decides it: one factor times 2^64, which is
exact below 2^64, times the other factor rounds the exact product once
to 24 bits in the normal range, and 0 < |that| < 2^-62 is tininess
after rounding. A product that rounds up to 2^-126 is then not tiny and
keeps the emitted product, which is 2^-126 there, so nothing has to be
built from the significands.
The flush of v_mul_f32 v5, v6, v7 takes 12 IR instructions instead of
34, counted as the instructions the flush select needs beyond the
product. vector_constant_factor_tininess pins 12 to nearest and 36 for
the directed path of the same instruction, and checks that the
directed modes and v_mad_legacy_f32 (before rounding) keep #2839's
check. Its round-to-nearest branches are removed as unreachable.
The f32 denormals entry says which rounding the check inherits: the
multiply is a plain OpFMul like the product, with no f32 rounding mode
requested, so it relies on the driver rounding f32 to nearest even, as
the products do. F32ConstantFactorTininess names the row a check before
rounding flushes, 0x3f7ffffe * 0x00800001, which keeps the emitted
0x00800000.
Against #2839 to nearest even, a model of both checks agrees on
1,074,977,546 operand pairs (every 24-bit significand at both rounding
thresholds, random pairs, specials) with each other and with an exact
reference. On an RTX 5070 the outputs of #2839's own tininess and
mixed-source shaders are bit-identical with either lowering over
165,298 and 164,008 generated rows in all six float modes tested.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unity reuses the memory of transient render targets: in FPV Simulator
(PPSA10698) an RGBA8 1920x1080 color texture is sampled at the address
that a D32S8 surface's stencil plane occupied earlier. The lookup found
the stencil plane and threw "sampling the stencil plane ... is not
implemented", which ended the presentation.
9d65003c made depth-plane views in a format that cannot hold the plane
fall through to the ordinary texture path. A stencil-plane view whose
texels are not one byte wide cannot hold the 8-bit stencil plane
either, so it now falls through the same way.
AI-assisted: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Windows TLS rewriter only patches FS-relative instructions that
CodeInstructionCollector decodes. Code that no symbol, unwind range or
root reaches is covered by a linear sweep that starts after int3 padding
and stops at the first instruction beginning with two zero bytes, so
zero-filled bytes are not read as code.
The .NET NativeAOT homebrew titles pad between functions with zero bytes
instead of int3 and have unwind entries for only part of their code. In
Prospero Vibrate (PPSA99920) the sweep ended at the padding after the
first function. Functions reached only through switch jump tables, which
the control-flow graph does not follow, or through pointers it cannot
resolve kept 105 of 670 FS loads unpatched: 84 `mov reg, fs:[0x28]`
stack-guard reads (65 into rax, 19 into rcx) and 21 `mov rax, fs:[0]`
thread-pointer reads. The title faulted reading address 0x28 within a
second of start.
The sweep now treats a zero run that follows a jump, call, return or
trap and ends at the next 16-byte boundary as alignment padding, and
continues at that boundary. A call counts because a function can end in
a call to a noreturn function. Zero fill after straight-line code, or a
whole zero 16-byte block, still ends the sweep as before.
All 670 FS loads in PPSA99920 are now patched, as are the 102 of 685 in
Prospero Explorer (PPSA99109) and the 103 of 718 in Prospero Multi Tools
(PPSA99110). New tls_function_coverage padding cases: loads behind zero
padding after a return, a call and a jump, with 13, 2, 1 and 5 zero
bytes, are patched; a load behind zero fill after a nop is left
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0306c677784b6c4272990ce65f6daf9fb2dbc3ca)
The --deferred-undecodable mode registers shaders through a Vulkan
device but, unlike --deferred, did not check for one first. On the
Windows CI runner, which has no Vulkan driver, vkCreateInstance returns
VK_ERROR_INCOMPATIBLE_DRIVER and the test fails instead of skipping, so
the windows job on main has failed since e5b32bb4. It now returns
VulkanTestSkipped like the other modes.
A registered compute shader takes its wave size from its header's
specials (dispatch_modifier), and PrepareRegistered read them without a
null check. ps5-opengl registers compute headers without specials, since
it writes the specials pointer only for linked graphics stages, and
passes the wave size in the dispatch initiator instead. Registration
then threw "shader metadata is outside the registered header".
Such a shader is now deferred like the other shaders that can't be
prepared at registration, so it is prepared at its first dispatch with
the wave size from the initiator's CS_W32_EN.
DecodeColorTileMode rejected 2D color targets with a nonzero MIP0_DEPTH, so
an array target could only be rendered into through a view of a surface that
declares one slice. radeonsi sets CB_COLOR_ATTRIB3.MIP0_DEPTH to the last layer
of an array target (si_state.c, ac_descriptors.c), and Mesa-built homebrew
emits such targets. The view's slice already moves the target by one slice
size; it is now also checked against MIP0_DEPTH.
On Windows libc exports __cxa_init_primary_exception, std::uncaught_exception
and std::uncaught_exceptions only as _nid_postfix symbols (GCC_GLOBAL_ALIAS
is empty there), so host modules bind libstdc++-6.dll's versions:
- std::make_exception_ptr, which std::promise's destructor uses for
broken_promise, allocated the exception with libc and initialized it with
libstdc++'s __cxa_init_primary_exception, whose header layout differs.
A test that makes one and rethrows it dies with an access violation.
- std::uncaught_exceptions read libstdc++'s thread state, which libc's
throws never touch, so it returned 0 while an exception unwound. The
AGC driver's deferred-label guard relies on it to tell a failed record
from a finished one.
The three are now exported natively, like __cxa_throw and the rest.
__cxa_init_primary_exception also sets the reference count to 0, as the
Itanium ABI does: the exception_ptr created next takes the only
reference, so a made exception is destroyed once instead of leaking.
On Windows std::_Exit ends in ExitProcess, which still runs every module's
DLL_PROCESS_DETACH and TLS callbacks after the other threads are gone. A
static destructor that joins a worker then waits forever: a title that calls
_Exit while libSceVideoOut's flip queue runs (ps5-opengl's retirement
fail-stop) stays alive in FlipQueue's destructor. _Exit and libkernel's _exit
now terminate the process with TerminateProcess, as _exit ends a POSIX
process: no atexit handlers, stream flushes or module destructors.
Guest samplers with DISABLE_CUBE_WRAP (S# word 0 bit 28) are created with
VK_SAMPLER_CREATE_NON_SEAMLESS_CUBE_MAP_BIT_EXT when the device enables
VK_EXT_non_seamless_cube_map; RADV maps the flag to the same bit. The bit
only changes cube filtering, so without the extension the sampler is still
created and only a cube texture sampled through it throws.
libc implements the C++ exception runtime and already defines
exception_ptr's out-of-line members, so host modules that link libc
reference-count libc's exceptions. Code inside libstdc++-6.dll does not
see those definitions on Windows: std::__future_base::_Result_base's
destructor is one of them, and it releases the stored exception with
libstdc++'s own exception_ptr::_M_release. That decrements a count 0xa0
bytes before the object, while libc keeps it 0x80 bytes before, so the
decrement lands in the heap memory in front of libc's allocation and
the exception is never freed.
VideoOutDriver reports the presentation thread's start-up failure
through a std::promise, so video_out_flip_lifetime sometimes ended with
0xc0000374 when SDL's offscreen driver refused the window.
libc now defines the base destructor, with a _nid_postfix twin so that
the NID-patched libc keeps its name, and modules bind it there.