Repository navigation
feat(agc): read frame capture deltas with the title's work in flight - #15136
thuanquang wants to merge 45 commits into
Conversation
The first replay pass compiles every shader and pipeline the frames use, so its frame times mostly measure compilation. agc_frame_replay --repeat <n> replays the frames n times: before each later pass it waits for the driver to go idle, clears the captured ranges, restores the snapshot, registers the video outputs the snapshot had again and makes the EOP flip reservations that were pending at the snapshot again (a token still reserved from the previous pass is kept). Frame times come from passes 2 to n, each pass is listed under passes, and agc_driver_frame_replay also replays its capture with --repeat 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n minutes The drain gave up after 10 s without a completion even while the driver was still compiling or preparing the work, which reported unfinished submissions on full titles and left a process that could not exit. A blocked wait on one queue does not mean the replay is stuck either: another queue can still be preparing the work that releases it. The drain now gives up only after ten minutes without a completion and reports the waits the queues are blocked on every ten seconds and when it gives up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lready applied A replay's device has no window, so it never loaded the title's Vulkan pipeline cache file and compiled every pipeline the captured frames used again, minutes for the first Demon's Souls frames. It also replayed the pipeline journal in the background during the measured frames, which the title did at boot. APS5_HEADLESS_PIPELINE_CACHE makes a device without a window load the file, and agc_frame_replay sets it unless --cold-pipelines is given. The driver exports AgcDriverWaitForPipelineJournal, and the replay calls it after the registrations and reports the wait as journal_s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g, reservation errors --cold-pipelines now clears an inherited APS5_HEADLESS_PIPELINE_CACHE on both platforms, the replay test builds its command with boykopovar#11157's quoting so the redirect and --repeat work under sh and cmd, and a reservation that fails in a repeated pass is reported and fails the replay instead of being dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…PS5_TRACE_GPU With APS5_TRACE_GPU the driver now prints the serial it gives each submission, CPU flip and suspend point when it accepts it, with the queue, size and command address, and each completion as it is marked, including queue 0 completions deferred to the draw pipeline. A replay that stops completing can then be followed from the call that made a submission to the serial that never completed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y's serials Finding why a replay of a title stops, or which shaders a frame uses, meant running the whole capture. agc_frame_replay gains three modes: - --shaders replays nothing and maps no guest memory: it reads the snapshot, applies the captured writes in call order and prints one JSON line per shader registration with its code address, size, type, code hash and how many earlier registrations had the same code; --shader-code <dir> also writes each distinct code to a file - --submits <n> lists the first n submissions and flips of the captured frames with their queue and serials, the packets of each and the fields of its waits and releases - --trace-serials prints each submission, flip and suspend point with its call ordinal and command address as it is issued, to match the serials APS5_TRACE_GPU prints agc_driver_frame_replay_shader and agc_driver_frame_replay_shader_host check that the listing reads the code the capture holds at the registration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ayload words, output errors --submits decodes the captured ranges on Linux and macOS as well, walks packets with Pm4::PacketWords and FillerPacket so type-0 payload dwords are not taken for headers, and the listing checks open, write, flush and close of its output file, naming the path and exiting nonzero on failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… check On Linux libc prints its host CPU line to stdout, so the check that a failed write left stdout empty failed even though the tool reported the failure and exited 2. Check that no success summary was printed instead, as the blocked output case does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…apshot A capture snapshot read every committed block through the coherent path, which refuses memory under a multisampled color target, its FMASK or native multisample depth whose samples exist only on the host, so the capture stopped there. The snapshot now writes those bytes as zeros, counts them as host_only_sample_bytes in capture.json and adds a warning, and the replay starts those targets without their earlier contents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Late-added ranges and write-watched blocks are read through the same host-only-aware read as the snapshot, so a CPU store over a guarded multisample target no longer aborts a capture. The read is a helper tested for ranges in the middle, at either edge, overlapping and outside a block, and against a native target and its neighbor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry group, and write REWIND control words last A title that parks a queue on a REWIND writes the tail's command buffers and only then sets the control word valid. agc_frame_replay released such a wait by copying the recorded control word alone, so the tail ran with its INDIRECT_BUFFER still unwritten, and applying a memory group restored chunks in address order, so a control word could become valid before the buffers it chains to. A parked REWIND is now released by applying the first later memory group whose recorded control word is valid, and every group publishes valid REWIND control words, and the control words of parked REWINDs, only after the rest of the group is written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A delta read while a queue is parked held the labels, WRITE_DATA and fills the queue's earlier packets had stored, so the replay saw those values before replaying the work that writes them and released waiting queues early. The driver now reports each word a queue worker stores from a packet (labels, WRITE_DATA, COPY_DATA, DMA_DATA up to 64 KiB, semaphores, constant RAM and sample dumps, and buffer fills by their pattern), and a delta leaves out every word that still holds that value, counted as gpu_store_bytes. A block of a new range that is all zero is skipped only when no such store touched it, so a block the title cleared after a GPU fill is no longer lost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e title A memory delta was read only once every accepted submission completed, so a capture stopped after 10 s whenever a queue waited on a REWIND control word or a label a later call stores, as in the two-queue reproducer on boykopovar#11157 and the Ghost of Yotei capture on boykopovar#11197. The driver gains a settled hold: AgcDriverHoldSettled succeeds once every queue has finished or is parked on a REWIND or WAIT_REG_MEM value the title has not stored, with no queued draw, unrecorded label or pending label write, and keeps submissions and parked queues where they are until AgcDriverReleaseSettled. The recorder reads each delta under that hold through the coherent path. The replay restores each delta under the same hold. Its breaker, built on boykopovar#12318, releases a parked REWIND by applying the first later memory group that sets its control word, only under a settled probe, one wait per pass, and no longer writes a value from a later delta for any other wait, which applied a later CPU store early. Pacing counts a running queue as progress and gives up after --pace-timeout-ms only once no queue runs, or after the busy timeout. The drain also ends after 10 s without a completion once no queue runs, and the busy timeout, ten minutes, is shared by pacing, the settled hold and the drain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sample writable ranges without coverage while queues are settled. Reuse unchanged samples only when no GPU submission progressed between idle observations. Cover initial and later coverage loss, shared mappings, and restoring cached values after a GPU fill.
…s views on Windows The fixture block is one committed private allocation, and Windows can't release part of it to make room for the two section views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
boykopovar#12566 landed. Its two commits are the ones this branch carried (same patch IDs), so the conflicts are this PR's own changes to them: readGuest reads deltas synced, without the GuestRead mode, and the debt entries this PR rewrites. Kept this branch's side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in boykopovar#12567's merge of main ce9394b, which landed boykopovar#12566. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed-deltas # Conflicts: # core/libs/prx/libSceAgcDriver/CMakeLists.txt
# Conflicts: # core/libs/prx/libSceAgcDriver/CMakeLists.txt
…capture-settled-deltas # Conflicts: # core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_frame_replay_tests.cmake
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nges The shader path recorded whole 64 KiB blocks and host pages around a shader pointer even across a gap between recorded ranges, so the replay copied bytes into a page it never mapped and crashed. Trim them to the readable ranges, including ranges committed since the snapshot, and have the replay refuse a snapshot or delta chunk that reaches outside every range of the capture. Co-Authored-By: Claude <noreply@anthropic.com>
…pieces Linux and WebAssembly refuse a commit that starts inside a used range, so committing the two pieces of an allocated block threw on Linux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3366c14 to
7c73336
Compare
Shader deltas skip a host page whose hash has not changed, and full deltas skip an unwatched block whose hash and accepted serial have not changed. The two paths kept separate records of the same bytes and neither learned when the other wrote them, so a skip could leave the replay with bytes the title no longer held. Reverse: a shader delta writes B to a host page, a full delta writes C over it, the title writes B again, and the next shader delta skips the page, so the replayed registration reads C. Idle forward: a full delta records A for a chunk of a writable host range, a shader delta writes B into it, the title restores A, and the next full delta at the same accepted serial skips the chunk, so the replay keeps B. Cross-key: full-delta chunks start at the range's address, while shader deltas cut 64 KiB arena blocks and host pages at the range's edges, so a range that does not start on a 64 KiB boundary records the same bytes under two keys, and a write under one key leaves the other in place. Each block or host page a delta writes now drops every host page hash and unwatched-block hash that overlaps it before recording its own, so the other path's next delta writes those bytes again instead of trusting a hash of an older write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7c73336 to
0191553
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
recordLocked and ReserveEopFlip read the driver's serials after the call's delta step, which waits for every accepted submission, so every work call was recorded with nothing in flight. They now read them on entry, before the delta. The driver also tracks the serials that have started but not completed (StartedProgress), for the delta gate that follows. capture.json gains submit_depths: how many recorded submits the title made with 0, 1, ... 15 and 16 or more submissions in flight, from the serials read when the call entered the recorder. Co-Authored-By: Claude <noreply@anthropic.com>
…n that makes them GuestMemory gains a store serial per host thread (StoreSerialScope, CurrentStoreSerial; a HostThreadSlot like the current packet, since a plain thread_local is emulated TLS under WinLibs and every submission, item, job, label, title call and write-back touches it) and a driver-store observer that the write tracker calls with each driver store and its serial. The queue worker stores under the submission it executes, the draw committer and the draw finisher under the serial their work was queued with, and the title's driver calls under TitleCallStoreSerial. A label the queue worker queues keeps the serial of the submission that queued it, also when a later submission stores it. Stores from sites that set no serial report 0, which a caller has to treat as unknown. OnComplete and AfterRecordedWork run their actions under the serial current when they were registered while an observer is installed (DriverStoreObserved), so a normal run adds no allocation per completion; AfterCompletions, whose action already holds a copy of its bytes, always does. A completion registered by a title call keeps serial 0 instead of TitleCallStoreSerial: it runs when its batch finishes, after the call has returned, so its store is reported as unattributed rather than as a title call's own store. Each storage-image unit keeps the oldest serial among the submissions that wrote it since its last store, or 0 once a title call or a writer without a serial wrote it, and a write-back stores under the oldest serial of the pending units it stores (0 if any of them has none). Tests: agc_recorder_title_call_closure registers one action through each completion wrapper under a TitleCallStoreSerial scope, and the observer must see serial 0 for all three, then one through each under serial 141, and the observer must see 141 for all three. agc_recorder_storage_writer marks a unit pending under serial 100 (and, in a second case, 0), drops it without a write-back and marks it again under 141, and the write-back must be reported under 141; a unit marked under 141 and then under a title call must be reported as 0. Co-Authored-By: Claude <noreply@anthropic.com>
… when nothing in flight can store into it writeDelta collects the delta's blocks first and holds the queues only when a block may still be stored by accepted work: a page the driver has stored into before, an unfinished label (UnfinishedLabelOverlaps), a queued committer or draw finisher write, a recorded GPU write, a pending storage-image result, a shadowed unit, an unwatched span, a new range or a host page. It also holds when a queue is parked on a memory wait, when a submission in flight has not started, and once more than one title thread has made recorded calls. Otherwise it reads at once, records the serials as they were and the submissions that had started, and marks the Memory record InFlightDeltaFlag. The recorder observes every driver store with its serial: a store into such a delta's page by a submission accepted before the read stops the capture (stale_delta). calls.bin becomes version 2 (InFlightCallsVersion; readers accept 1), and capture.json reports in_flight_deltas, held_deltas, stale_delta, unattributed_stores and hold_reasons. DrawPipeline::OverlapsAny, which writeDelta asks from the title's thread, now sees the draw finisher's held writes on any thread (DrawFinisher::AnyPendingOverlaps) instead of only on a thread that has handed the finisher work. Its other caller, validateVariant, runs only on the queue-0 worker, the finisher's only producer, so its answer there does not change. While it records a call, the recorder holds the queue worker at its next packet and the draw committer and finisher at their next item (HoldForRecorder, ReleaseForRecorder, and ParkForRecorder, which tests the hold flag inline), so the title's work does not run on while the recorder collects, reads and compresses, and the capture keeps the depth the title had. A delta that has to wait for the queues to settle releases the hold first. The replay restores a flagged delta once the submissions that had started at its read are done, for a capture from one thread or the single-thread prefix of a multi-thread window, instead of waiting for the queues to settle. That gate ends with the driver's error on a driver failure (SerialDone rethrows it under the driver mutex, as WaitSettledFor does), throws once --pace-timeout-ms passes with no queue running and no serial moving, and gives up after 10 minutes. When any delta is flagged, the replay requires capture.json with stale_delta 0. A missing stale_delta counts as none for captures without flagged deltas. Tests: frame replay state modes gate_failure (a submission parked on an unwritten label fails under the gate, which must end with the driver's error), gate_settled (the gate must throw once no queue runs, without restoring the delta) and unverified (a capture with a flagged delta is refused without capture.json, without stale_delta or with stale_delta 1; one verified, one that stopped early but was verified and one without deltas read in flight are accepted). The concurrent test accepts a delta read with submissions unfinished when it is marked as read in flight. agc_driver_draw_finisher_overlap: a thread that hands the draw finisher no work must see, through DrawPipeline::OverlapsAny, a write the finisher still holds, and not a range beside it. BUILD.md names calls.bin version 2, and the debt entry on frame replay pacing says which submissions the gate of a delta read in flight waits for. Co-Authored-By: Claude <noreply@anthropic.com>
…s memory gates
hold_reasons counted only the first matching reason of each held delta,
so the unwatched block, which every full delta holds, hid every other
reason. Every delta read with work in flight now evaluates all eleven
reasons under the recorder hold: hold_reasons counts each reason that
applies, and hold_masks is a histogram of the reason sets ("none" for a
delta read in flight). Whether a delta is held is unchanged.
unwatched_blocks lists, for each unwatched block, those deltas and how
often its bytes had changed, and the driver had stored into it, since
its last record.
The replay JSON adds memory_held_wait_s and memory_in_flight_wait_s
(both inside pace_wait_s), restore_s, and in_flight_max and
in_flight_mean: the submissions in flight after each submission's gate,
sampled after Submit calls only, as submit_depths counts them, and only
in the passes frame_ms times. With --repeat n > 1 those are passes 2 to
n, as the usage text now says; the other counters still cover every
pass.
Tests: frame replay state modes depth_submits (a submission parked on a
label, an EOP flip reservation behind it and the delta that releases
it: one depth sample, the submission's) and depth_passes (a window
replayed twice, the first time behind a prologue submission parked on
a label: the depth counters hold only the second pass). The capture
side, mode hold_diagnostics, needs the in-flight test helpers and comes
with the other in-flight frame replay tests.
Co-Authored-By: Claude <noreply@anthropic.com>
…lands and hold them for waits on title memory finish() and every stop() that is not for a hazard now release the recorder hold, wait up to 10 s for the submissions the deltas read in flight depend on (until the newest serial accepted at such a read has completed; at once without such a delta), make the device idle, flush pending GPU writes over the pages of those deltas, and only then read the hazard. On a timeout capture.json records unverified_deltas, the number of delta groups that still own pages; the field is now always written. The store observer is installed when the capture starts and removed once capture.json is written, and a delta read after an owner's submissions completed clears that owner. finish() also reads the capture's last delta with the progress snapshot and the recorder hold of a recorded call; before, the queues ran on while it was read. The replay refuses every capture of the current calls format without capture.json, and one with a flagged delta unless its capture.json has unverified_deltas 0. The recorder read a delta in flight while a started submission still had to pass a WAIT_REG_MEM, REWIND or MEM_SEMAPHORE on memory only the title writes, or had passed one on a dword that delta restores, and the replay then parked that submission inside the delta's gate. While a capture tracks them (one load per submission), the driver now keeps the memory waits of every accepted submission, registered after the Accessible check their satisfied mark uses, plus REWIND control words (a tail's waits are added when executeRewindTail copies it), and the dword and value of each label an accepted submission writes. The worker marks each wait as it passes it; completion hands the rest over. A delta read with work in flight is held for the new reason awaits_title when an unpassed wait that was not satisfied at submit has no label that runs before it and stores a value it accepts, or when the delta's pages hold a wait dword of a submission unfinished since the previous full delta. The hold decision uses the progress snapshot taken before the recorder hold, so a submission dequeued under the hold counts as queued. Such a label is the waiting submission's last label to the dword ahead of the wait, or, without one, the last label to it of the newest unfinished submission accepted before it on its queue, or any label of an unfinished submission on another queue. For a wait in a REWIND tail, the labels ahead of it also include those its submission wrote before the REWIND and those of the earlier tails. A label behind the wait, or of a submission queued after it on its queue, never counts, and the labels of a REWIND tail count only for its own waits and those of the tails behind it. The value must be fixed at submit: a RELEASE_MEM timestamp, a predicated packet and a packet under COND_EXEC or a predicated INDIRECT_BUFFER do not count. A WAIT_REG_MEM_64 needs both of its dwords from the same submission, and on another queue from labels it writes one after the other; a REWIND counts only a label on another queue that sets its valid bit, and a MEM_SEMAPHORE counts no label. The rule is applied when the delta is read, so a submission that labelled the dword and completed before the read no longer counts. It does not see a store over such a label before the wait reads it (a later label on any queue, a title store, a COPY_DATA or DMA_DATA, a WRITE_DATA over 64 bytes, a label of a REWIND tail); a delta read in flight on such a label fails at the paced replay's gate or counts in unverified_deltas, as the new debt entry on title waits in frame capture deltas says. Tests: state mode unverified adds the refusals of a flagged delta whose capture.json lacks unverified_deltas, has one that is not a number or has 1, and of a capture with no capture.json and no flagged delta (unheaded). The capture tests of the end check and the wait registry (late_store, late_wait, wait_before_read, the *_after_read modes and the wait_labelled modes) need the in-flight test helpers and come with them. In the wait_labelled modes a label of the flag the submission waits on must not count: wait_labelled_after puts it behind the wait, wait_labelled_before ahead of it with a value the wait rejects, wait_labelled_skipped ahead of it under a COND_EXEC that skips it, and wait_labelled_elsewhere in a compute submission in flight, with a value the wait rejects. wait_labelled_after replays with the default release: the label behind the wait rewrites the flag before the next delta reads it, so no delta holds the title's store. The debt entry on frame capture deltas lists when a capture stops and what the end check counts. Co-Authored-By: Claude <noreply@anthropic.com>
… a driver store into it Every full delta holds each readable unwatched range, and a delta read with submissions in flight was held whenever it held an unwatched block, so a title with any unwatched memory never kept work in flight across a call. With work in flight, an unwatched block whose bytes equal its last record and that took no reported driver store since is now left out of the delta, one whose bytes changed with no reported store is armed and judged like a watched page, and only a block with a reported store holds the delta, so a GPU store the title overwrote with the recorded bytes is still restored. hold_reasons counts unwatched for that last case only. A shader delta that writes a host page records its bytes the same way, so a later full delta does not leave out a page the title wrote back to what an earlier full delta recorded. BUILD.md describes the capture's in-flight read and its hold reasons, and a debt entry records that a GPU store into such a block that the driver does not report, which the title then overwrites with the recorded bytes, replays with the GPU's bytes. The in-flight frame replay tests are registered in tests/cmake/agc_driver_frame_replay_tests.cmake. All but hold_diagnostics and host_page_restored keep a submission S1 of at least 20000 WRITE_DATA labels in flight and record two idle deltas first. Four of them also submit before the capture, once: in_flight_delta and in_flight_gate both of their submissions, in_flight_store S1's labels without its copy and the submission that reads the page, and late_wait S1 only. All of them skip (77) through Inconclusive when S1 got too far before the title's next call (it finished, reached its wait or passed half of its labels): - in_flight_delta and in_flight_store: a delta read with S1 in flight is flagged and replayed, and a copy queued behind S1 that stores into its page stops the capture and the replay refuses it (skipped exactly when the copy landed before the title's store); - in_flight_gate: the delta lists S1 as started and the replay must not restore the title's later store before S1 copies the old bytes; - hold_diagnostics: a delta with an unwatched block counts a parked gate and a queued submission, the block counters and the replay depth; - late_store: a copy behind S1 stores into the page of a delta read in flight after the last flip, and the capture must report that delta stale or unverified; late_wait: a later submission waits on a flag the title stores only after the last SubmitFlip returns, and the end check must still verify the delta (unverified_deltas 0); - wait_after_read, wait_before_read, wait64_after_read, semaphore_after_read and rewind_after_read: a started submission whose WAIT_REG_MEM, WAIT_REG_MEM_64, MEM_SEMAPHORE or REWIND the title has not passed keeps the delta of the title's next call from being read in flight; semaphore_after_read signals its semaphore from a helper thread once a marker label right before the wait lands and the title's next call is under way, and the capture must end without a stop; - wait_labelled_after, wait_labelled_before, wait_labelled_skipped and wait_labelled_elsewhere: the flag also gets a label that must not count, behind the wait, ahead of it with a value the wait rejects, ahead of it under a COND_EXEC that skips it, or in a compute submission in flight with a value the wait rejects, and the delta of the title's next call must still be held; wait_labelled_before waits for the submission to reach its wait, and wait_labelled_elsewhere for the compute submission's label, before the title stores the flag, and wait_labelled_after replays with the default release, since its label rewrites the flag before the next delta reads it; - unwatched_in_flight and unwatched_restored: a delta is read in flight past an unchanged unwatched block, and one a submission stored into and the title wrote back stays held; unwatched_restored fails when the call behind the busy submission is recorded with nothing in flight; - host_page_restored: while a submission waits on the title's flag, a shader delta writes a Host range page the title changed, the title writes back the bytes the full delta before it recorded, and the capture must hold those bytes at the next full delta. carried_label, registered in the same file without a capture and with APS5_PIPELINED_DRAWS=0, installs a driver-store observer of its own: a queue-0 label that a submission queued, and that the next, already queued submission stores because the first one's end flush was skipped, must be reported under the serial of the submission that queued it, and the next submission's own label under that submission's serial. Co-Authored-By: Claude <noreply@anthropic.com>
…y to release a REWIND When the replay stops making progress, its breaker releases a parked wait with an earlier recorded value, the wait's reference, or, for a REWIND, by restoring a whole later memory group early. In a capture of the current calls format (InFlightCallsVersion) that group can be a delta read with work in flight, so the breaker restored it outside its gate. The replay of such a capture now skips only that early restore: a REWIND whose control word only a later delta sets stays blocked until that delta's call, and the pacing timeout reports it with the blocked waits, as the drain timeout already did. A capture of an earlier calls format keeps the early restore. Single-dword releases stay, as fence_reuse needs them: the title stores the fence and re-arms it between two recorded calls, so no delta holds the released value. Test: frame replay state mode v2_breaker. A queue parks at a REWIND whose valid bit only a later in-flight delta holds, and the replay stalls in pacing before that delta; it must release nothing early, give up pacing once, and run the tail after the delta's own restore. Docs: BUILD.md says so and lists the replay JSON keys memory_held_wait_s, memory_in_flight_wait_s, restore_s, in_flight_max and in_flight_mean; the usage text of --pace-timeout-ms says that a memory delta read in flight fails the replay once no queue runs and the serials have not moved for that long, and that of --no-release that a capture of the current calls format never applies a later delta early; a debt entry records the REWIND limit. Co-Authored-By: Claude <noreply@anthropic.com>
hold_reasons could not show which holds a capture that reads more deltas in flight would lose: parked joined waits on a GPU label with waits on title memory, driver_page counted every page the driver had stored into since the capture started, and awaits_title joined the wait registry's two rules. Four reasons are added beside the old ones, whose names and meaning are kept: parked_on_title (a queue is parked on a dword that no unfinished submission writes as a label), driver_page_since_full (the driver stored into one of the delta's pages after the last full delta read guest memory), title_wait_unpassed (a submission in flight still has to pass a wait on title memory) and wait_dword_in_delta (the delta holds a dword that a submission in flight, or one finished since the last full delta, waits on). capture.json adds read_depths, a histogram of the submissions in flight at each delta read in flight. Whether a delta is held is unchanged. Mode hold_diagnostics first parks a compute queue on a label that a graphics submission writes behind 20000 labels. The delta read before that submission is accepted counts parked, parked_on_title and title_wait_unpassed. Once the submission has finished its first page of labels, the title stores into that page, and the delta read next counts parked without parked_on_title or title_wait_unpassed (the compute wait accepts the value of the label the submission still has to write), and driver_page_since_full. After the next full delta, a store into the same page counts driver_page without driver_page_since_full, and a store into the page of the gate the next submission waits on counts wait_dword_in_delta. in_flight_delta checks read_depths for its one delta read in flight. wait_after_read, wait64_after_read, rewind_after_read and the wait_labelled modes also need title_wait_unpassed counted, as it must be for the delta read at the title's next call, so they fail whether or not the waiting submission has parked by that read; semaphore_after_read does not, because the thread that signals its semaphore may do so before that read. wait_labelled_rewind and wait_labelled_rewind_chain wait in a REWIND tail on a flag whose newest label ahead of the wait stores a value the wait rejects: in wait_labelled_rewind the submission's label before its REWIND, and in wait_labelled_rewind_chain, whose wait is in the second tail, the label of the first tail, which follows the submission's label of a value the wait accepts. The chain mode keeps its first REWIND until the queue has parked on it, so the submission's label is stored before its tails are received. Both keep the label record of the queue's previous submission, which stored the accepted value, from before the waiting submission until the title's next call has returned, as a completion the draw pipeline has not run yet keeps it (two DriverTestAccess helpers). The delta read at that call must count title_wait_unpassed, and the strict replay must copy the title's store only after the wait passes. parked_on_title is not the driver's settle condition, and it does not follow the rule behind title_wait_unpassed: the parked slot holds the wait's address and compare but not the submission that waits, so a parked wait counts as a label wait while any unfinished submission labels its dword, including the waiting submission itself and submissions queued behind it on its queue, whatever value the label writes. parked without parked_on_title is therefore an upper bound on the waits a label can release. driver_page_since_full compares a page's last driver store with the store count taken when the last full delta read guest memory (after its settle when it was held), not with the time that page was last read. Co-Authored-By: Claude <noreply@anthropic.com>
… in the replay The replay maps the serials that a delta read in flight lists by arithmetic: the replay's accepted serial minus the capture's at that delta. Nothing checked that each call took the same serials in both runs. A CPU flip takes no serial when its thread has made no graphics submission or suspend point, the driver has accepted a submission before, no flip is pending, no submission capture runs, in-order flips are off and the build has no timing log, and a replay thread starts without the title thread's earlier submissions, so one flip can shift the map and the delta's gate then waits on another submission than the title's. The recorder now writes a Submit, Flip or Suspend after its accept and appends the serial it took (0 for a flip that took none); its ordinal is still assigned before the accept. A call that moves the driver's accepted serial by other than one (none or one for a flip) stops the capture. Up to the last delta read in flight that the replay gates, it checks that every submission and suspend point took the serial that the capture's maps to, counted from the capture's frame start and the window's first accepted serial, and that each such delta lines up the same way; otherwise, and when a call recorded no serial, it throws, even when the serials the delta lists would line up. A paced replay refuses a capture in which such a call recorded no serial before it restores memory, so captures recorded before this change that have deltas read in flight are refused. A CPU flip maps the same way, or takes a serial in only one of the two runs. When only the replay's flip took one, the replay waits until that flip presents before its next call, as the title's presented within its call, and counts it in the serial offset. When only the title's took one, and it is the next serial, the replay's flip has already presented, the map leaves that serial out and the delta's gate skips it among the submissions that had started. The gate maps each started serial through the serials the calls took, not by its distance from the accepted one. The replay's first pass starts with an empty graphics command buffer from a thread of its own when the capture's frame start had accepted a submission, since a CPU flip takes a serial while the driver has accepted none. The debt entry names what decides whether a CPU flip takes a serial and how the replay maps one that differs. New state mode serial_diverged replays these windows: a submission recorded as taking the first serial after the frame start replays; a CPU flip recorded as taking the third serial after a submission that took the first (in a build without the timing log), a submission or a suspend point recorded as taking the second serial, a submission that recorded no serial, a delta read in flight at a serial that no call before it in the window took, a delta read in flight that lists a serial no call since the frame start took, and a delta read in flight after a second thread's first call each make the replay throw. It also runs the tool's run() on two captures whose submission or suspend point before a delta read in flight recorded no serial: run() must refuse each before its restore, which would fail first on their capture.json without arena_base. New state mode flip_serial_neutral (in a build without the timing log) replays a compute submission, a CPU flip recorded as taking the next serial and a graphics submission: the replay's flip takes none, the delta read in flight behind them is restored, and with the graphics submission parked the gate skips the flip's serial and names the submission at the replay serial it took. With the compute submission before the flip parked instead, the gate waits for that submission at the replay serial it took, not at the completed serial before it that its distance from the accepted serial gives. New state mode serial_flip_in_order runs with APS5_CPU_FLIP_IN_ORDER: a CPU flip that takes a serial in the replay and none in the capture replays, and the delta read in flight behind it is restored only after that flip presented. New frame replay mode flip_serial_start (in a build without the timing log) captures two CPU flips from a thread without graphics submissions after the driver accepted submissions, so the second takes no serial, then a delta read in flight behind a busy submission; the replay must reproduce the copy and, by the driver's accepted-serial trace (APS5_TRACE_GPU), take as many serials for CPU flips as the capture's flips did. in_flight_delta and gpu_restore_cached check that each recorded submission and suspend point carries the serial after the accepted one and each flip that serial or 0. in_flight_delta now takes a suspend point after its first flip and requires its one suspend point and both of its flips to have taken a serial; gpu_restore_cached requires that its CPU flip from a thread without graphics submissions recorded 0, in a build without the timing log, with neither APS5_CPU_FLIP_IN_ORDER nor APS5_SYNC_FLIP set and with APS5_DUMP_SUBMITS unset or empty. Its new version checks (requireRecordedSerials, the mapped-ordinal scan) compare with InFlightCallsVersion. State mode v2_breaker is renamed in_flight_breaker, and new state mode previous_breaker replays its window from a capture rewritten to the calls format before InFlightCallsVersion, whose breaker must still apply the later delta early to release the REWIND before pacing gives up after 5 s; state mode unverified adds a capture of that format without capture.json, which must still be accepted. Docs: BUILD.md now names the serials each recorded call carries, the replay's gate for a delta read in flight with its timeouts, refusals and CPU flips, the empty command buffer of the first pass, which counters cover which passes with --repeat, and which unfinished labels satisfy a wait that a submission in flight still has to pass, so that the wait does not hold a delta; its replay paragraph names the current calls format instead of version 2; the debt file adds [Stores into frame capture deltas read in flight], which stores such a delta is checked against and which driver stores are not known to report their writer, and [Serials of frame replay calls], and [Unwatched blocks in frame capture deltas] points to the former. The state modes are registered in tests/cmake/agc_driver_frame_replay_state_tests.cmake, the frame replay mode in tests/cmake/agc_driver_frame_replay_tests.cmake. Co-Authored-By: Claude <noreply@anthropic.com>
…een its hold check and its arm writeDelta checked a delta's pages against the driver's stores in holdMask and only later, in arm, gave them the owner that makes a later store into them stop the capture, each under its own storeMutex block, with the label, snapshot, storage, shadow and queue checks between them. A store reported after holdMask passed its page and before arm was neither a hold reason nor a hazard. Main's boykopovar#9287 reports a deferred staged copy-back before it notes it: MarkGpuWrites (ShaderResources.cpp) calls MarkStagedWrites before NotePendingWrites. Such a report can land in that window, the read's flush misses the note that is not yet published, and the delta was read in flight without the dispatch's results. Nothing reported the range again, so the capture was accepted with stale_delta 0 and the replay's restore wrote the old bytes over the dispatch's results: a silently wrong replay. holdMask now returns the count of driver stores it checked against. arm takes that count, walks every page of the delta's blocks under storeMutex first, and refuses without recording an owner when the driver stored into one of them after it. writeDelta arms before it counts the hold masks, and a refused arm holds the delta for a driver store newer than the last full delta (driver_page and driver_page_since_full) on the unchanged held path. A store reported before the check holds the delta as before, one between the check and the arm now holds it too, and one after the arm still stops the capture (stale_delta). No committed test reaches the window, which lies between two statements of writeDelta. With a local patch that reports a store by the first started submission into the delta's first page right after the hold mask, frame replay mode in_flight_delta holds its delta (driver_page+driver_page_since_full) with this change, and without it reads the delta in flight and accepts the capture with stale_delta 0. Co-Authored-By: Claude <noreply@anthropic.com>
0191553 to
3d1e020
Compare
|
Reference failure on main for the in-flight capture work here; I have not tested this PR's stack. Linux, Ryzen 7950X, Radeon RX 6650 XT, RADV Mesa 26.2.6, The Pathless PPSA01826 / 02.000.000, fresh save/caches and no inputs. Production source is main A two-frame capture was triggered at 180.9 s in the title menu using The final manifest has 0 frames, 6 calls, 2,735 prologue calls, zero unreadable snapshot bytes and zero host-only sample bytes. The native title kept running and exited 0 at the planned ordinary close (433.2 s elapsed); the harness correctly failed capture qualification. No successful frame replay or speed claim is attached. The frozen SDK bank and protected source inputs stayed unchanged, the temporary kernel limit was restored, and no private native processes remained. Redacted manifest, exact capture log lines, settings and test-only control patch — SHA-256 AI-assisted: yes. |
Commits to review, oldest first. Only these 10 commits are this PR.
da47027017and everything below it is #12568's stack as #12568's branch carries it: #11435, #11436, #12318, #12566 (merged), #12567 and #12568, with its merges ofmainup to9ab6a281ea(the current heads of #11435, #11436, #12318 and #12567 are not belowda47027017). The diff of the 10 commits alone:da47027017...3d1e020f44.32d1d7aeb8fix(agc): record a frame capture call with the serials it was made at543a4bd550feat(agc): attribute driver stores into guest memory to the submission that makes themcc8732bc25feat(agc): read a frame capture delta with the title's work in flight when nothing in flight can store into it4d0f66ee0afeat(agc): count every frame capture hold reason and time the replay's memory gates23767f20f3fix(agc): verify a frame capture's in-flight deltas after their work lands and hold them for waits on title memoryc9ad41b4cffeat(agc): hold a frame capture delta for unwatched memory only after a driver store into it71aac026f4fix(agc): keep a frame replay from applying a later memory delta early to release a REWIND73b73aaac4feat(agc): split frame capture hold reasons by wait kind and store age5255d64934fix(agc): record the serial each frame capture call took and check it in the replay3d1e020f44fix(agc): hold a frame capture delta that a driver store reached between its hold check and its armda47027017is #12568's head:f21b60440d(the previous push's base:2b39d2ac36,a62a6958aband a merge ofmain4f41f4c375), then chitters2004's merge ofmain9ab6a281ea.git range-diff f21b60440d..0191553a68 da47027017..3d1e020f44against the previous push shows commits 1, 4, 6, 7 and 10 identical, commits 2 and 3 changed only by the restack, and commits 5, 8 and 9 changed by the items under This push. I restack whenever #12568 moves and update this list each time. The 10 commits add about 800 lines to the driver, 220 to the replay tool and 1,960 lines of tests. BUILD.md and the debt file are complete only at commit 9: commits 6, 7 and 9 also document parts of commits 2 to 5. This stays a draft until the stack and #14137 settle: #12568's head will still change, and this PR's calls format version follows #14137's (see Format version below).This push (since
0191553a68).da47027017, which mergesmain9ab6a281ea. Commit 2 is resolved against perf(agc): keep constant RAM dumps and unregistered labels on the GPU without draining #9898, which made the draw committer's queue oneenqueue(Item)shared with the stores it now queues behind pipelined draws:Itemkeeps perf(agc): keep constant RAM dumps and unregistered labels on the GPU without draining #9898'sstoreand this PR'sstoreSerial, andenqueuesetsstoreSerialfor every item. So the constant RAM dumps perf(agc): keep constant RAM dumps and unregistered labels on the GPU without draining #9898 queues, and the occlusion counter dumps perf(agc): queue occlusion counter dumps behind pipelined draws instead of draining #6501 queues, report the serial of the submission that queued them, as draws and DMA stores do. Commit 3's declaration sits next to fix(agc): raise no end-of-pipe event from flips and carry the release's interrupt context #7978's flip-work ones.mainchanges it now sits on (two reviewers, each checked by a refuter): perf(agc): keep constant RAM dumps and unregistered labels on the GPU without draining #9898 (queued stores, GPU labels in unregistered host memory, recorded store bytes, which serve shader resource captures only, DMA copy waits), perf(agc): queue occlusion counter dumps behind pipelined draws instead of draining #6501, fix(agc): raise no end-of-pipe event from flips and carry the release's interrupt context #7978 (flip work), perf(agc): reuse read-only address-based draw builds over their cached address space #13858, perf(agc): read committed guest pages directly on Windows #14799 and fix(agc): forget the blocks kept for the CPU when an upload restamps their units #15457 need no change here. Debt [Stores into frame capture deltas read in flight] (commit 9) now names feat(agc): publish a written native multisample D32S8 surface to its guest planes on a guest read #13021's native multisample depth publication, which reports the serial of the read that triggers it, not of the draw that wrote the samples, and no longer lists occlusion counters. Two items from it are under Open.executeRewindTail, commit 5). Before, such a wait did not see them and could count an accepted label of a lower unfinished serial on the same queue, while the stalled submission's own label wrote a value the wait rejects. New modeswait_labelled_rewindandwait_labelled_rewind_chain(commit 8).What
Frame captures from #12568's stack read each memory delta only after the submissions already accepted have completed, or every busy queue is parked on a wait for a title store. A paced replay therefore runs the captured frames with nothing in flight at those points. This PR lets the recorder read a delta while submissions are still in flight when none of the hold reasons below applies. Before it restores such a delta, the replay waits until the submissions that had started at the read have completed. To make this checkable, each recorded call carries the serials the driver had when the call entered it. Each submission, flip and suspend point also records the serial it took, and driver stores into guest memory carry the serial of the submission that made them. A driver store reported into such a delta's page after the read stops the capture when it comes from a submission accepted by the time of the read, or has no serial. The replay refuses a capture whose deltas read in flight were not checked.
Persona 5 Royal (PPSA05110): no change yet. A 20-frame gameplay capture made with an earlier revision of these changes (
02132ba69e) read no delta in flight. It recorded 120 submits: 100 with one submission in flight and 20 with none (submit_depths), so no submit was made with more than one submission in flight. All 120 deltas made with work in flight were held. Each was held for an unfinished label and for a dword in the delta that a submission waits on (wait_dword_in_delta), and 119 also for a page the driver had stored into. The capture also counted 56,540 driver stores with no serial (unattributed_stores). Any of them that lands in a page of a delta read in flight would stop the capture. A paced--strictreplay of that capture on the same build matched its reference images exactly (pixels_identical1.000000,max_channel_diff0, no pacing timeout). So this PR is the correctness layer that later timing work needs; it does not make replays faster or deeper on this title. A later 10-frame capture of the casino escape, made with the previous push plus nine local commits that port #14137's tracker change and other capture fixes (b18ca951c1), read 1 delta in flight and held 59, withstale_delta0,unverified_deltas0 and 10unattributed_stores. No capture has been made with this head yet.Capture
Driver stores into guest memory carry the serial of the submission that made them (
GuestMemory::StoreSerialScope):A title call's own stores carry a reserved title-call serial. A completion that a title call registered, a write-back that includes a unit a title call marked, and a store on a thread where none of these scopes is set report 0, which the recorder treats as unknown. While a capture runs, an observer passes each store and its serial to the recorder.
Each recorded submission, flip, suspend point and EOP flip reservation carries the accepted and completed serials the driver had when the call entered it, before its delta. A submission, flip or suspend point also records the serial it took (0 for a CPU flip that took none). A submission or suspend point that does not move the accepted serial by exactly one, or a flip that moves it by more than one, stops the capture.
capture.jsoncounts the submissions in flight at each recorded submit (submit_depths) and at each delta read in flight (read_depths).A delta is read with submissions in flight unless one of these holds it. A held delta is read as before, after the queues settle.
WAIT_REG_MEM,WAIT_REG_MEM_64or REWIND wait. AMEM_SEMAPHOREwait never counts as parked.DrawFinisher::AnyPendingOverlaps): the recorder asks from the title's thread, and before this push only a thread that hands the finisher work saw them.WAIT_REG_MEM,WAIT_REG_MEM_64,MEM_SEMAPHOREor REWIND wait on title memory that no unfinished label satisfies: its own last label ahead of the wait, else the last label on that dword of the newest unfinished submission before it on its queue that has one, or a label on another queue, with a value fixed at submit that the wait accepts. AMEM_SEMAPHOREcounts no label, and a REWIND only another queue's label that sets its valid bit. The rule is applied when the delta is read.These reasons do not cover every store a started submission can still make, such as a copy queued behind its labels. A reported store of that kind is caught by the stop below, as
in_flight_storetests. The label rule does not see a store over a counted label before the wait reads it (debt [Title waits in frame capture deltas]); a delta read in flight on such a wait makes the paced replay throw at its gate, or the capture counts it inunverified_deltas. Waits are registered at submit while a capture runs. With work in flight, an unwatched block whose bytes still equal its last record, and that took no reported driver store since, is left out of the delta; one whose bytes changed is judged like a watched page. A shader delta that writes a host page updates that record too.capture.jsoncounts each reason that applied (hold_reasons,hold_masks, with four keys that split parked waits, driver stores and title waits further), and per unwatched block how often it changed or took a driver store (unwatched_blocks).The record of a delta read in flight carries
InFlightDeltaFlagand lists the submissions that had started.While it records a call, the recorder holds the queue worker at its next packet and the draw committer and finisher at their next item. So the driver's threads start no new packet or item while the delta is collected, read and compressed. GPU work already submitted, a packet already executing and completions keep running. A delta that has to wait for the queues releases them first. The last delta of the capture is read the same way.
A store the driver reports into a page of a delta read in flight, from a submission accepted by the time of that read or with no serial, stops the capture (
stale_delta). The check starts when the delta's pages are armed, right before the read; a store reported between the hold check and the arm holds the delta instead (above). A title call's own stores land before the call returns and are exempt.After the last frame, and at any stop that is not such a store, the recorder waits up to 10 s for the submissions those deltas depend on to complete, waits for the GPU, flushes pending GPU writes over their pages and checks them the same way. If that wait times out,
capture.jsoncounts the deltas it could not check (unverified_deltas).capture.jsonalso addsin_flight_deltas,held_deltasandunattributed_stores. Its magic becomesaps5-frame-capture 2; nothing checks that magic.Two driver exports are added:
AgcDriverSerialDone_nid_postfix, which the replay's gate uses, andAgcDriverQueuedSerials_nid_postfix, which only the tests call.Replay
capture.jsonor with a nonzerostale_delta. This includes a capture with no delta read in flight: one cut short by a kill or a crash has nocapture.json, so it is refused where the stack's tool replays its complete records. The tool still prints its "replaying the complete records" notice first. A capture with deltas read in flight is also refused whenstale_deltaorunverified_deltasis missing, orunverified_deltasis nonzero. A capture of the previous format withoutcapture.jsonis still accepted.--pace-timeout-ms, or after 10 minutes. A serial that differs from the capture's in any other way also throws, as does such a delta after a second thread's first call, or one that lists a serial no call since the frame start recorded.97284fe586); a capture of the previous format keeps it. A REWIND whose control word only a later delta sets stays blocked until that delta's own call. The first call paced behind it waits out--pace-timeout-ms, which releases nothing, and with--strictthe replay fails. Other waits are still released with an earlier recorded value, or with a value derived from the reference, which fix(agc): read capture deltas and replay them while queues wait on the title #12567'sfence_reuseneeds.4ef54b694cin fix(agc): read capture deltas and replay them while queues wait on the title #12567 took that list out to keep the warning's earlier text. This PR puts it back because it shows which wait a stalled replay of the current format is blocked on, such as a REWIND above. The text before it is unchanged.memory_held_wait_sandmemory_in_flight_wait_s(both insidepace_wait_s),restore_s, andin_flight_maxandin_flight_mean: the submissions in flight after each submit's pacing gate, from passes 2 to n with--repeat n.main'sconcurrenttest (fix(agc): read a frame capture's memory delta once the accepted submissions completed #11197) now also accepts a memory delta flagged as read in flight: before the copier thread's first call, such a delta can be read while the copies run.Format version. Version 2 of the calls format (
aps5-frame-calls 2) changes three things. A call's accepted and completed serials are read when it enters, before its delta; version 1 read them after the delta. A submission, flip or suspend point appends the serial it took. A delta read in flight carriesInFlightDeltaFlagand lists, after its group, the serials of the submissions that had started. The reader reports the version (FrameCaptureReader::Version()). Version 1 captures keep the stack's replay rules: nocapture.jsonneeded, the early REWIND release, no serial checks. Like every capture whose frame start had accepted a submission, their first pass now starts with the empty command buffer described under Replay. The replay compares a capture's version with one constant,InFlightCallsVersion(2), at its five checks. As chitters2004 suggested on #12567, #14137 takes version 2: its headca4553365fwritesaps5-frame-calls 2, and its replay refuses calls format 1. When I restack this PR onto it, this PR will drop its own copy of the version reader and setInFlightCallsVersionto 3. Its replay rules will then apply from version 3 on, so #14137's version 2 captures keep #14137's replay rules, apart from that empty command buffer.Outside a capture these hooks stay on; their cost was not measured:
OnCompleteandAfterRecordedWorkwith a nonzero serial, one load of the store observer;AfterCompletionsaction and per batch finish, one scope;No new lock is taken.
Docs and debt. BUILD.md describes the in-flight read, its hold reasons and which unfinished labels satisfy a title wait, the stops, the replay's gate with its CPU-flip rules and the empty first command buffer, and the refusals. The debt file updates [Frame capture deltas] and [Frame replay pacing]. It adds [REWIND waits in frame replays], [Title waits in frame capture deltas], [Unwatched blocks in frame capture deltas], [Stores into frame capture deltas read in flight] and [Serials of frame replay calls]. The [Title waits ...] entry names the stores over a counted label that the rule does not see. The [Stores into ...] entry names the kinds of driver store for which it has not been checked whether they are reported with their writer. One that is not reported at all, and lands in a page of a delta read in flight after the read, is overwritten by the replay's restore. The longest new entry, [Serials ...], has 998 characters.
New tests. Each is
agc_driver_frame_replay_<mode>unless named otherwise. There are 35.Capture modes record frames through the driver, then replay them, except the two that say they run no replay. The title's work runs behind a long run of
WRITE_DATAlabels, so that it is still in flight at the title's next call.in_flight_delta: while the busy submission runs, the title stores a page and submits a copy of it. The delta of that call must be flagged as read in flight and counted at depth 1 inread_depths. The test takes a suspend point right after the flip that starts the capture; each submission and the suspend point must record the serial after the accepted one, both recorded flips must record the serial they took, and the replay must copy the stored page.in_flight_store: a copy queued behind the busy labels stores into a page that the title then writes and a delta reads in flight. The capture must stop on that page (stale_delta), and the replay must refuse it.in_flight_gate: a submission copies X to Y between two runs of labels and writes a label after the copy. The title waits for that label, overwrites X and makes a call. The delta must list the submission as started, and the replay must dump the old X from Y, so a gate that waits for the wrong submission, or for none, fails.late_store: in a one-frame capture, the last copy, behind the busy labels, lands after the last flip in a page of a delta read in flight. The end of the capture must find it (stale_deltaorunverified_deltas), and the replay must refuse the capture.late_wait: in a one-frame capture, the last submission, accepted after a delta read in flight, waits on a title dword that the title stores only after its last flip returns. The end-of-capture check must still verify that delta (unverified_deltas0), and the replay must accept the capture.wait_after_read,wait64_after_read,rewind_after_read: a started submission still has to pass aWAIT_REG_MEM,WAIT_REG_MEM_64or REWIND wait on a title flag when the title writes the copy's source and makes its next call. No delta may be read in flight with that submission listed,awaits_titleandtitle_wait_unpassedmust be counted, and a replay with--no-releasemust copy the title's store.wait_labelled_after,wait_labelled_before,wait_labelled_skipped,wait_labelled_elsewhere: the same, but the flag also gets a label that must not count: behind the wait, ahead of it with a value the wait rejects, ahead of it under aCOND_EXECthat skips it, or in a compute submission in flight with a value the wait rejects. The delta of the title's next call must still be held and counted astitle_wait_unpassed.wait_labelled_afterreplays with the default release, since its label rewrites the flag before the next delta reads it.wait_labelled_rewind,wait_labelled_rewind_chain: the wait sits in a REWIND tail, and the flag's newest label ahead of it stores a value the wait rejects: inwait_labelled_rewindthe submission's label before its REWIND, inwait_labelled_rewind_chain(wait in the second tail) the first tail's label, which follows the submission's label of an accepted value. The queue's previous submission, which stored the accepted value, keeps its label record until the title's next call has returned. The delta read at that call must counttitle_wait_unpassed, and the strict replay must copy the title's store only after the wait passes.semaphore_after_read: the same withMEM_SEMAPHORE, signalled by a second thread once the submission has reached its wait. No delta may be read in flight with that submission listed andawaits_titlemust be counted, but nottitle_wait_unpassed, since the signal may come before that read. The capture must not stop. This mode runs no replay.wait_before_read: the submission has passed its wait on the title's flag but is still running when the title makes its next call. The delta holds the flag's page, so it must be held (awaits_title), and the replay must copy the title's store.unwatched_in_flight: an unwatched block that nothing changed is left out of a delta read in flight. Its counters must show no change and no driver store, and the replay must restore the page the title wrote.unwatched_restored: the busy submission stores into an unwatched block, and the title writes the recorded bytes back. The call must be recorded with the busy submission in flight, its delta must be held forunwatched, and the replay must keep the title's bytes.host_page_restored: while a submission waits on the title's flag, a shader delta writes a host-range page that the title changed, and the title then writes back the bytes that the full delta before the shader delta recorded. The capture must hold those bytes at the next full delta, made with the submission in flight. It has no timing precondition and no inconclusive skip. This mode runs no replay; it checks the captured chunks.hold_diagnostics: deltas are read while a queue parks on a title gate, a submission queues behind it, and a compute queue parks on a label that a graphics submission writes. Each reason must be counted (parked,queued,parked_on_title,driver_pagewith and withoutdriver_page_since_full,title_wait_unpassed,wait_dword_in_delta), along with the unwatched block counters and the replay'sin_flight_max.flip_serial_start(in a build without the timing log; it skips in one with it): two CPU flips from a thread without graphics submissions after the driver accepted submissions, so the second takes no serial, then a delta read in flight behind a busy submission. The replay must reproduce the copy and, by the driver's accepted-serial trace (APS5_TRACE_GPU), take as many serials for CPU flips as the capture's flips did.Most capture modes skip as inconclusive (exit 77) when the title's work finishes, or reaches its wait, before the title's next call.
Without a capture:
carried_label(withAPS5_PIPELINED_DRAWS=0) installs a driver-store observer of its own. A queue-0 label that a submission queued, and that the next, already queued submission stores because the first one's end flush was skipped, must be reported under the serial of the submission that queued it, and the next submission's own label under that submission's serial.agc_driver_draw_finisher_overlap(withAPS5_DRAW_PIPELINE=1): a thread that hands the draw finisher no work must see, throughDrawPipeline::OverlapsAny, a write the finisher still holds, and not a range beside it.Replay modes run the replay in-process on synthetic captures:
gate_failure: a driver failure reported while a submission is parked on an unwritten label, before the gate of a delta read in flight runs, must end the gate with that error, and shutdown must rethrow it.gate_settled: the same gate with no failure must end with the "no queue runs" error before its time limit, and must not restore the delta.unverified: a capture of the current calls format withoutcapture.jsonis refused, also one with no delta read in flight. One with a delta read in flight is also refused withoutstale_deltaorunverified_deltas, with a non-numeric or nonzerounverified_deltas, or with a stale delta. A verified one (also one that stopped for low disk space), one without such deltas, and a capture of the previous calls format withoutcapture.jsonare not refused. The mode calls the tool'srestoreMemorydirectly, which checks each capture and restores the ones it accepts; it replays no calls.depth_submits: the replay samples the in-flight depth at submits only, assubmit_depthsdoes, not at an EOP flip reservation.depth_passes: with two passes, the depth counters hold only the second pass's samples.in_flight_breaker(wasv2_breaker): a queue parks at a REWIND whose valid bit only a later delta read in flight holds. The replay must release nothing, give up pacing once, and run the tail after that delta's own restore.previous_breaker: the same window from a capture rewritten to the calls format beforeInFlightCallsVersion, with the delta not flagged. The breaker must apply that later delta early to release the REWIND before pacing gives up (5 s), and the tail must run.serial_diverged: before a delta read in flight, each of these must make the replay throw: a submission or a suspend point recorded as taking the second serial after the frame start; a CPU flip recorded as taking the third serial after a submission that took the first (in a build without the timing log); a submission with no recorded serial; a delta read in flight at a serial that no call before it in the window took; one that lists a serial no call since the frame start took; and one after a second thread's first call. The tool'srun()must refuse, before its restore, a capture whose submission or suspend point before a delta read in flight recorded no serial. A window whose calls took the recorded serials replays.serial_flip_in_order(withAPS5_CPU_FLIP_IN_ORDER=1): a CPU flip that took no serial in the capture takes one in the replay. The replay must restore the delta read in flight behind it, and only after that flip presented.flip_serial_neutral(in a build without the timing log): a compute submission, a CPU flip recorded as taking the next serial and a graphics submission, where the replay's flip takes none. The delta read in flight behind them must be restored. With the graphics submission parked, the gate must skip the flip's serial and name that submission at the replay serial it took. With the compute submission before the flip parked instead, the gate must wait for it at the replay serial it took, and restore nothing.serial_diverged,flip_serial_startandflip_serial_neutralfail with a message whenAPS5_CPU_FLIP_IN_ORDERorAPS5_SYNC_FLIPis set orAPS5_DUMP_SUBMITSis not empty, since a CPU flip would then take a serial. Their CTest registrations set none of these, so only a run that sets one for every test makes them fail.Recorder tests:
agc_recorder_title_call_closure: completions registered throughOnComplete,AfterCompletionsandAfterRecordedWorkunder the title-call serial must reach the store observer with serial 0, and under submission 141 with 141.agc_recorder_storage_writer: a storage image marked pending under serial 100 (or 0), dropped without a write-back and marked again under 141 must report its write-back under 141. One marked under 141 and again under a title call must report 0.Changed:
main'sconcurrent(above), and #12568'sgpu_restore_cachednow also checks the serials each recorded call took. It expects its CPU flip to record 0 only in a build without the timing log, with neitherAPS5_CPU_FLIP_IN_ORDERnorAPS5_SYNC_FLIPset andAPS5_DUMP_SUBMITSunset or empty.Open before ready for review
ca4553365fwrites calls format 2 and whose replay refuses format 1: drop this PR's copy of the version reader and setInFlightCallsVersionto 3.previous_breakerandunverified's previous-format capture then test fix(agc): stop frame capture deltas from collecting the previous delta's pages again #14137's format 2.Depends onwill say the same.late_or_host,label,snapshot,storage,shadowandqueue0(agc_driver_draw_finisher_overlaptests the query behindqueue0, not the term), the recorder's queue hold, and commit 10's hold when the arm finds a newer driver store (no committed test reaches that window; only the local injectiond-t2ashows it). The recorder's stop when a recorded call moves the driver's accepted serial by anything but one (none or one for a flip) is an invariant check, not an untested behaviour: no input reaches it through the driver's entry points, since every submission, flip and suspend point enters the driver through the recorder, the recorder holds its mutex across each call's accept, and a submission or suspend point moves the accepted serial by exactly one or throws.NativeMultisampleDepth; until then debt [Stores into frame capture deltas read in flight] names it. A miss needs the HTILE plane to share a 4 KiB page with title data written between the draw's submission and the delta read; the replay then fails at the publication's same-value check rather than replaying wrong bytes. From code reading; not seen on P5R.main: with frame capture on, a command-buffer flip waits in fix(agc): raise no end-of-pipe event from flips and carry the release's interrupt context #7978's flip hold while the recorder holds its lock across the call (fix(agc): leave GPU-stored words out of frame capture deltas #12568's design, which commit 9's accepted-serial check also relies on). If the previous flip submission waits after its flip on a queue-0MEM_SEMAPHOREthat another thread signals with a later submission, that thread blocks on the lock and the run hangs. The root is that the flip hold does not count aMEM_SEMAPHOREwait as a wait on the title, as it doesWAIT_REG_MEM; without the recorder the same hang needs the signal to come after the title's next flip. From code reading, not reproduced; P5R's captures record calls from one thread.stale_deltaandunverified_deltashave not been measured at this head (the latest capture,b18ca951c1above, carries the previous push).mainor a dispatch, not on a pull request. There,tools/check_skips.pyfails any skip that does not report a device limitation, and the capture modes' inconclusive skips do not.Tested
Windows 11 26200, AMD Radeon RX 6700 XT (gfx1031), driver 32.0.21045.16010, WinLibs GCC 15.2.0 r7, Release. Each arm is a full build (
cmake --build build) aftergit submodule update --init --recursive, which skips quickjs's nested test262. The focused set isctest -R "frame_(replay|capture)|agc_recorder_(title_call_closure|storage_writer|completion_labels)|draw_finisher_overlap|queued_constant_ram_dump|dumped_constant_capture|draw_pipeline_retired_items|agc_recorder_stored_bytes|agc_driver_(present_mode|flip_compute|eop_interrupts)" -V --timeout 120: 86 tests at the head3d1e020f44, the 35 new ones and the 51 of the baseda47027017. The regex adds the tests of #9898 and #7978, themainchanges commit 2 and 3 were restacked against. Earlier rows ran the previous regex (77 tests at0191553a68, 44 atf21b60440d; three of those 44,shader_host_rewritten,shader_host_restoredandshader_unaligned_restored, came witha62a6958ab). It runs withANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1. Validation runs addVK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation(a local build of the layer),VK_LOADER_LAYERS_DISABLE=~implicit~andVK_LOADER_DEBUG=layer, so their logs show whether the layer loaded.e4dfa4b069is this series before the assembled-head review: its commits 1 to 8 have the same trees as this head's, and its commits 9 and 10 differ only in tests (the guards offlip_serial_startandflip_serial_neutral,flip_serial_neutral's third window andserial_diverged's listed-serial row). Each line names the commit it ran on.Focused set, base and head:
VUID-linesda47027017reservation_error, timeout (30.03 s);reservations(#11435's), "replace guest placeholder with private memory: Attempt to access invalid address" after 0.05 s, then passed in the run below and in 5 of 5 rerunsda47027017reservation_error, timeout (30.03 s)3d1e020f44reservation_error, timeout (30.02-30.04 s)3d1e020f44reservation_error, timeout (30.02 s)Inserted device layer "VK_LAYER_KHRONOS_validation"lines); no missing-layer linef21b60440dreservation_error, timeout (30.05 s)f21b60440dreservation_error, timeout (30.02 s)Inserted device layer "VK_LAYER_KHRONOS_validation"lines); no missing-layer linee4dfa4b069reservation_error, timeout (30.02-30.03 s)e4dfa4b069reservation_error, timeout (30.03 s)0191553a68reservation_error, timeout (30.03 s)0191553a68reservation_error, timeout (30.08 s)3d1e020f44create a Vulkan device (25 of the 51 at the baseda47027017), among them 24 of the 35 new ones: the 21 capture modes,carried_labeland the 2 recorder tests. At0191553a68, 44 of 77 did. The 10 new replay modes andagc_driver_draw_finisher_overlapcreate none, so the layer does not apply to them.58d76317c6, without loader debug,agc_recorder_storage_writertimed out once at 30.36 s, its log stopping after the write-watch probe line. It passed in every other run, among them 20 of 20 at3366c145f6and 4 of 4 each ate4dfa4b069and this head.rewind_releasefailed 2 of 26 runs in this push's fix rounds, both with "the parked memory delta does not hold the CPU store" (after 29.20 s and 8.66 s): once on the previous push plus that round's fixups, once on the previous push with another local branch on top. It passed in every run in the table above.agc_driver_frame_replay_reservation_erroris feat(agc): replay full titles with repeated passes and the title's pipeline cache #11435's test. It times out at 30 s in all our Windows runs of this branch and of its base. fix(agc): shut the driver down before frame replay exits with an error #14100 gives the cause: on a replay error the tool callsstd::_Exitwith the driver still running, which hangs on Windows, and this test is the first to reach that path.--pace-timeout-ms, or do not complete within 10 minutes. With the replay tool of the previous push,flip_serial_start's refused replay ran into this hang and timed out at 120 s. This PR's refusals of a whole capture (stale_delta,unverified_deltas, a missingcapture.json, missing serials) come before the driver starts and exit normally.Full suite (not rerun for this restack; from the push before the previous one) at
e4dfa4b069(ctest --timeout 120 --parallel 4,ANYPS5_REQUIRE_VULKAN=1, no layer): 90 of 3,980 tests failed and 18 skipped. Of the 90, 65 arepublic_headers, which timed out, and its 64public_headers_compile_*tests, which then did not run. The other 25:log_macrosandlog_macros_timestamps, which fail locally because our ccache setting makes__FILE__relative;reservation_error(above); and 22 outside the focused set. Of those, 6 failed (agc_driver_depth_fast_clear_submit,agc_driver_direct_write_completion,agc_driver_scratch_pool,agc_driver_store_over_untracked,agc_driver_sample_order_grid_reference,none_relocations),guest_pollexited with 0xc0000409, and 15 timed out (agc_driver_native_multisample_depth,guest_pthread_start,guest_raise_exiting_thread,masked_store_lowering,mulx_lowering, seven relocation and section-symbol tests, and threetls_function_coverage_*tests). Another branch's build ran on the machine during part of the suite.I reran
public_headersand 18 of those 22 one at a time under the GPU lock with a 300 s timeout, on this head0191553a68and on the basef21b60440d. Four failed on both sides with the same message, so this PR does not cause them:agc_driver_direct_write_completion: "the completion of a buffer write dropped image results recorded after it";agc_driver_scratch_pool: "scratch pool isolation wave64: round 0 lane 0 dword 0 read 0xdeadbeef, expected 0x00000000";agc_driver_store_over_untracked: "store over untracked: the first result was not stored";agc_driver_sample_order_grid_reference: a GPU fault, thenvkQueueSubmitresult -4.Three of the four,
direct_write_completion,scratch_poolandsample_order_grid_reference, also fail onmain91ecd2d974on this GPU, 3 of 3 runs each (reported on #14137);store_over_untrackedhas not been run onmain. The other 15 passed on both sides.relr_conversionand the threetls_function_coverage_*tests, which timed out in the suite, were not rerun.Every commit builds:
git rebase -xbuildslibs, the replay tool and the frame capture, replay, replay-state, recorder and draw-pipeline test targets at each commit. All 10 commits built at this head3d1e020f44, after the restack.Red checks. Each patch undoes one piece of this PR and must turn the named test red; the other tests its regex matches must pass.
d-t2ainstead injects the store that commit 10 handles. The patched tree is built, then the tests run withANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1. The "Earlier" column is the run on the branch where the fix was made, labelled with its commit; the fix rounds ran on7c7333675dplus their own fixups, and58d76317c6is the 9 patches of the push before the previous one (3366c145f6) on #12568's78c714060e. The last column applies the same patch ate4dfa4b069withgit apply --3way. Where it no longer applied there, a re-anchored or regenerated patch ran at0191553a68instead, and the cell says so, as it does ford-t2aand u13-red4, which did not run ate4dfa4b069.e4dfa4b069, or0191553a68agc_recorder_title_call_closure58d76317c6: failedagc_recorder_storage_writer3366c145f6, patch fixed by hand: failed 3 of 3 runsagc_recorder_storage_writer58d76317c6: failedunwatched_restored58d76317c6: failedin_flight_store58d76317c6: failedlate_store58d76317c6: failedlate_wait58d76317c6: failedawaits_titleholdwait_after_read,wait64_after_read,rewind_after_read,wait_before_read,semaphore_after_read58d76317c6: failed, 5/50191553a68: failed, 5/5queuedin_flight_delta,late_store,unwatched_in_flight58d76317c6: failed, 3/30191553a68: failed, 3/3in_flight_gate58d76317c6: failed0191553a68: failedSerialDonedoes not rethrow a driver failuregate_failure58d76317c6: failedgate_settled58d76317c6: failed0191553a68: failed after 20.20 sunverified58d76317c6: failed0191553a68: faileddepth_submits58d76317c6: failed--repeat, the first pass's depth samples are keptdepth_passes58d76317c6: failedOverlapsAnyasks the finisher's producer-only query againagc_driver_draw_finisher_overlapef6a91568d: failedOnComplete,AfterCompletionsorAfterRecordedWorkwrap is removedagc_recorder_title_call_closureef6a91568d: failed, 3/3carried_labelef6a91568d: failedcarried_labelef6a91568d: failedwait_labelled_*modes9fc6d89504, before it was committed: failed, 4/4wait_labelled_afterwait_labelled_before,wait_labelled_elsewhereCOND_EXECcounts as fixed at submitwait_labelled_skippedwait_labelled_elsewhereserial_divergedd6fa1cb409: failed0191553a68: failedserial_divergedd6fa1cb409: failed0191553a68: failedrun()before the restoreserial_divergedd6fa1cb409: failedserial_divergedd6fa1cb409: failedserial_divergedd6fa1cb409: failedin_flight_breakerabc85ff056: failedprevious_breakerabc85ff056: failed after 5.09 scapture.jsonunverifiedd6fa1cb409: failedunverified,previous_breakerd6fa1cb409: failed, 2/2in_flight_deltad6fa1cb409: failedin_flight_deltad6fa1cb409: failedAPS5_CPU_FLIP_IN_ORDER=1:gpu_restore_cachedalways expects a CPU flip without a serialgpu_restore_cachedabc85ff056: failedin_flight_delta(the delta must be held, not read in flight)7c7333675dwith commit 10's change: failed0191553a68: failed,hold_masksdriver_page+driver_page_since_full1host_page_restored9cac8e2692with the fix uncommitted: failed0191553a68: failedflip_serial_start27408f2a32: the replay refused the capture ("call 5 (flip) took replay serial 1 where the capture's call took none") and the test timed out at 120 s (Known, above);flip_serial_neutralandserial_flip_in_orderfailedflip_serial_start27408f2a32: failed0191553a68: failedflip_serial_neutral,serial_flip_in_order27408f2a32: failed, 2/2serial_flip_in_order27408f2a32: failedexecuteRewindTailadds no labels ahead of a REWIND tail's waitswait_labelled_rewind,wait_labelled_rewind_chain36551c543c(the fix on4daf1dbd66, whose driver and tool are0191553a68's): failed, 2/2, in 3 of 3 runs3d1e020f44: failed, 2/2, in 3 of 3 runswait_labelled_rewind_chain(wait_labelled_rewindmust pass)wait_labelled_rewindpassed3d1e020f44: failed in 3 of 3 runs,wait_labelled_rewindpassedflip_serial_neutral,serial_diverged0191553a68: failed, 2/2Each failure in the table printed the message its test gives for that defect, except u13-red0's
flip_serial_start(the tool's refusal, then the exit hang) and c-v1branch (the reader's "is not a frame capture file"). With commit 10 reverted as well as injected (t2b, on7c7333675d),in_flight_deltapasses: the delta is read in flight and the capture is accepted withstale_delta0, which is the silent case commit 10 closes. 34 of the 35 new tests have a red row;hold_diagnosticshas none.Not tested:
58d76317c6passed 26 of 63 tests. The 37 frame-capture tests stop at "frame capture: the guest write watch is unavailable", because asynchronous userfaultfd write protection fails there. That is not the CI path, and the pull request build does not run the Linux job. None of this push's new or changed tests has run on Linux. Third-party Linux runs so far cover only the stack, without this PR's code: TechLevelZero ran this PR's basef21b60440dmerged ontomain4f41f4c375on an NVIDIA RTX 2000 Ada and on an AMD RX 6300 (RADV); both runs passed with 0 new failures and no new validation IDs againstmain.-DAPS5_ENABLE_TIMING_LOG=ONbuild of this head. By the code, in such a buildflip_serial_startskips,flip_serial_neutralreturns at once,serial_divergedleaves out its CPU-flip row, andgpu_restore_cachedaccepts 0 or the next serial for its CPU flip. Such a build of4daf1dbd66ran: its driver and tool are this head's, and its tests lack the assembled-head review's changes (theflip_serial_startguard,flip_serial_neutral's third window andserial_diverged's listed-serial row). Thereflip_serial_startfailed 3 of 3 runs ("the first CPU flip after the frame start, from a thread without graphics submissions, took a serial in the capture"), the case its guard now skips, andserial_diverged,serial_flip_in_order,flip_serial_neutral,gpu_restore_cachedandin_flight_deltapassed 3 of 3. The same tree built without the timing log passed all six, 3 of 3. The environment side ran:gpu_restore_cachedpassed 3 of 3 runs withAPS5_CPU_FLIP_IN_ORDER=1on the tree ofabc85ff056, and failed with its guard forced (the c-u7env row).APS5_DUMP_SUBMITS: Windows removes a variable set to empty, so the empty-is-unset terms rest on code reading.DrawFinisher::Enqueue, applied around each job inDrawFinisher::run): the finisher runs only withAPS5_DRAW_PIPELINE, and none of the tests that install a driver-store observer sets it. Without that scope a finisher store reports serial 0. Into a page of a delta read in flight, that stops the capture (stale_delta), so a good capture is refused, loudly. Anywhere else it only adds tounattributed_storesand, like any reported store, holds later deltas over its page. It cannot make a replay silently wrong.RELEASE_MEMtimestamp terms of a label's fixed value, predicatedINDIRECT_BUFFERranges, the newest earlier submission on the waiter's own queue, the pairing of a 64-bit wait's two dwords,RewindValidon another queue's label, theMEM_SEMAPHOREearly return, and the erase of a completed submission's labels.02132ba69eandb18ca951c1.e4dfa4b069, the new replay modes take 0.05-0.39 s, the new capture modes 0.36-1.39 s,carried_label0.37 s, the two recorder tests 0.15-0.18 s andagc_driver_draw_finisher_overlap0.05 s.Checklist
main; no other open PR implements the same functions (not currentmain: these commits sit on fix(agc): leave GPU-stored words out of frame capture deltas #12568'sda47027017, whosemainis9ab6a281ea; the trivialgit merge-treeof the head intomain1e24993fa3is clean; fix(agc): stop frame capture deltas from collecting the previous delta's pages again #14137 also adds the calls version and its reader, see Format version)NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)da47027017, fix(agc): stop frame capture deltas from collecting the previous delta's pages again #14137 (merge order: this PR last; the order of fix(agc): leave GPU-stored words out of frame capture deltas #12568's stack and fix(agc): stop frame capture deltas from collecting the previous delta's pages again #14137 is not settled yet, see Open)Best merged after #14100; it does not depend on it. Without #14100, the refusals listed as Known under Tested hang the tool on Windows instead of exiting with their message.
🤖 Generated with Claude Code