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>
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>
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>
lavapipe and MoltenVK leave the vertex stage out of subgroupSupportedStages,
so the driver's shader validation refuses every vertex program that declares
GroupNonUniform ("subgroup capability 61 is unsupported for shader stage 1").
Most do, since the entry EXEC is a ballot.
The device target now records whether the vertex stage has subgroups. When
it does not, vertex and local programs get a host subgroup width of 1: s3
reports one vertex and one primitive, the lane id is 0, a ballot is the
invocation's own bit, a shuffle returns the invocation's value, an elect is
true and a half-wave reduction read is the key itself. Reads of other lanes
keep the existing narrow-host rules and throw where they did. Devices with
vertex subgroups emit the same SPIR-V as before.
The memo stamp held TrackerGeneration, which every collect and driver store
advances, so it almost never matched. Driver stores into uncovered ranges
record their generation per 64 KiB block, and NewestStamp of the surface
replaces TrackerGeneration in the stamp.
describePages stored the whole VMA that PROCMAP_QUERY or /proc/self/maps returned. Linux merges adjacent
anonymous mappings with equal flags, so an arena VMA can span GiBs, and the first table miss inside it after
an invalidation rewrote every page of it. Only the pages of the request, rounded out to whole pages, are
stored now, as #1791 describes.
When the guest arena is available, RunUnmappedGapTests checks that a check stores no pages outside its
range, and that protecting and unmapping through the registry clears the stored pages. The registry checks
follow the test on the perf/linux-page-cache branch linked from #1791.
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>
A frame presented from guest memory with APS5_DUMP_FRAMES is read back
at the size of the display buffer, and that whole copy went into the
dump writer's queue with the scale beside it (31.6 MiB for a 3840x2160
buffer). The queue holds 64 MiB, so the third frame the writer had not
finished ended the run with "frame dump queue exceeded 64 MiB". A frame
blitted from the resident image is already reduced on the GPU and queued
at the reduced size.
The readback is now reduced by the dump scale when the slot retires and
queued at that size, the same pixels the writer kept before. With
APS5_DUMP_SCALE=1 nothing is reduced and the limit is reached as before.
video_out_frame_dump_queue presents four 3840x2160 frames from guest
memory while the writer is held on the first file and compares every
dump with the decoded display buffer.
* 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>
Capture the request once before rethrowing from the shared handler. Standard exceptions keep the shader address, stage, optional dump filename and full message; non-standard exceptions remain unchanged after capture.
AI-assisted: yes.
PrepareShaderWithDiagnostics dumped the request but rethrew the raw error, so a failure never said which shader it came from. Wrap it with the shader address and stage, keeping the dump. AI-assisted: yes.
On Windows agc_write_tracking has failed since #3594 with
write tracking test failed: an unwatched range reads as not stored over
from the check in CheckSharedBlock:
if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over");
The host stack array lies in a host hole of the guest arena. #3594 made
Watched, collectWrites, UnchangedSince, UnchangedSinceAll and storeOwn
reject ranges overlapping such holes, but StoredOver, MarkWritten,
WrittenSince, ChangedBlocks and ImportWatched still accepted them through
WriteTracker::covers(), so StoredOver proved a range not stored over while
Watched called it unwatched (#4087).
The exclusion now lives in WriteTracker::covers() on Windows, which every
query already asks, and the per-function checks are removed. Host holes
are padded to the allocation granularity and the tracker blocks are 64 KiB
from the arena base, so no block is shared between a hole and arena
memory, and ranges outside the holes get the same answers as before.
A new case enumerates the arena for a host hole and checks every query
against it; on the previous code StoredOver, MarkWritten, StoreOwnBytes,
WrittenSince and ChangedBlocks fail it.
Fixes#4087
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Keep the tracker initialization fix without an explanatory code comment or technical-debt entry, as requested in review.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
An image whose results stay on the GPU is never written back, so
APS5_DUMP_STORAGE saw nothing; APS5_STORE_AT_FLIP=1 stores every pending
image and ran a 4 GB device out of memory at 3840x2160. The selected
images alone are now stored at each flip.