Skip to content

feat(agc): read frame capture deltas with the title's work in flight - #15136

Draft
thuanquang wants to merge 45 commits into
boykopovar:mainfrom
thuanquang:replay-bench/pr3-rw3
Draft

thuanquang wants to merge 45 commits into
boykopovar:mainfrom
thuanquang:replay-bench/pr3-rw3

Conversation

@thuanquang

@thuanquang thuanquang commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Commits to review, oldest first. Only these 10 commits are this PR. da47027017 and everything below it is #12568's stack as #12568's branch carries it: #11435, #11436, #12318, #12566 (merged), #12567 and #12568, with its merges of main up to 9ab6a281ea (the current heads of #11435, #11436, #12318 and #12567 are not below da47027017). The diff of the 10 commits alone: da47027017...3d1e020f44.

  1. 32d1d7aeb8 fix(agc): record a frame capture call with the serials it was made at
  2. 543a4bd550 feat(agc): attribute driver stores into guest memory to the submission that makes them
  3. cc8732bc25 feat(agc): read a frame capture delta with the title's work in flight when nothing in flight can store into it
  4. 4d0f66ee0a feat(agc): count every frame capture hold reason and time the replay's memory gates
  5. 23767f20f3 fix(agc): verify a frame capture's in-flight deltas after their work lands and hold them for waits on title memory
  6. c9ad41b4cf feat(agc): hold a frame capture delta for unwatched memory only after a driver store into it
  7. 71aac026f4 fix(agc): keep a frame replay from applying a later memory delta early to release a REWIND
  8. 73b73aaac4 feat(agc): split frame capture hold reasons by wait kind and store age
  9. 5255d64934 fix(agc): record the serial each frame capture call took and check it in the replay
  10. 3d1e020f44 fix(agc): hold a frame capture delta that a driver store reached between its hold check and its arm

da47027017 is #12568's head: f21b60440d (the previous push's base: 2b39d2ac36, a62a6958ab and a merge of main 4f41f4c375), then chitters2004's merge of main 9ab6a281ea. git range-diff f21b60440d..0191553a68 da47027017..3d1e020f44 against 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).

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 --strict replay of that capture on the same build matched its reference images exactly (pixels_identical 1.000000, max_channel_diff 0, 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, with stale_delta 0, unverified_deltas 0 and 10 unattributed_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):

    • the queue worker's, under the submission it executes; a label it defers keeps the serial of the submission that queued it, also when a later submission stores it;
    • the draw committer's and finisher's, under the serial their work was queued with;
    • recorder completions registered while a capture runs, under the serial that registered them;
    • a storage image's write-back, under the oldest serial among the pending units it stores.

    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.json counts 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.

    • A second thread has made recorded calls.
    • It has a range added since the snapshot, or shader host pages.
    • A queue is parked on a WAIT_REG_MEM, WAIT_REG_MEM_64 or REWIND wait. A MEM_SEMAPHORE wait never counts as parked.
    • A submission is still queued (so at a read in flight, every submission in flight has started).
    • An unfinished label, a pending snapshot, storage-image or shadow write, or a queue-0 draw write overlaps it. The draw finisher's held writes count from any thread (DrawFinisher::AnyPendingOverlaps): the recorder asks from the title's thread, and before this push only a thread that hands the finisher work saw them.
    • The driver stored into one of its pages since the capture started. A store reported after this check and before the delta's pages are armed for the read holds the delta too (commit 10).
    • An unwatched block in it took a reported driver store since its last record.
    • A submission in flight still has to pass a WAIT_REG_MEM, WAIT_REG_MEM_64, MEM_SEMAPHORE or 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. A MEM_SEMAPHORE counts no label, and a REWIND only another queue's label that sets its valid bit. The rule is applied when the delta is read.
    • It holds a dword that a submission in flight, or one finished since the last full delta, waits on.

    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_store tests. 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 in unverified_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.json counts 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 InFlightDeltaFlag and 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.json counts the deltas it could not check (unverified_deltas).

  • capture.json also adds in_flight_deltas, held_deltas and unattributed_stores. Its magic becomes aps5-frame-capture 2; nothing checks that magic.

  • Two driver exports are added: AgcDriverSerialDone_nid_postfix, which the replay's gate uses, and AgcDriverQueuedSerials_nid_postfix, which only the tests call.

Replay

  • A capture of the current calls format (version 2) is refused without capture.json or with a nonzero stale_delta. This includes a capture with no delta read in flight: one cut short by a kill or a crash has no capture.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 when stale_delta or unverified_deltas is missing, or unverified_deltas is nonzero. A capture of the previous format without capture.json is still accepted.
  • With pacing on, the replay refuses before any restore a capture in which a submission, flip or suspend point between the frame start and the last delta read in flight recorded no serial.
  • With pacing on, a delta read in flight waits until the replay's submissions that match the ones that had started at the read have completed. It is then restored while the queues run.
    • Before that, every submission and suspend point since the frame start must have taken the serial its recorded one maps to, counted from the frame start, and the replay's accepted serial must map to the capture's at the read.
    • A CPU flip may take 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 flip presented within its call, and counts the serial in the offset. When only the title's flip took one and it was the next serial, the map records that serial with no replay serial, the offset drops it, and the gate skips it among the submissions that had started. Each started serial maps through the serials the calls took, not by its distance from the accepted one.
    • The wait throws on a driver failure, once no queue runs and the serials have not moved for --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.
    • Submissions the capture had already completed at the read are not waited for. With work on more than one queue, one of them can still run during the restore (debt [Frame replay pacing]).
  • When the capture's frame start had accepted a submission, the first pass starts with an empty graphics command buffer, submitted from a thread of its own, so that a CPU flip takes no serial where the title's took none. This applies to every replay of such a capture, with or without pacing and in either calls format; the replay's absolute serials then start one higher.
  • In every capture of the current calls format, with or without deltas read in flight, the wait breaker no longer applies a later memory delta early to release a REWIND, since that delta may have been read in flight. So every capture this recorder writes loses fix(agc): read capture deltas and replay them while queues wait on the title #12567's early REWIND release (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 --strict the 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's fence_reuse needs.
  • The "pacing gave up" warning lists the waits the queues are blocked on again. 4ef54b694c in 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.
  • The JSON line 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 submit's pacing gate, from passes 2 to n with --repeat n.
  • main's concurrent test (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 carries InFlightDeltaFlag and 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: no capture.json needed, 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 head ca4553365f writes aps5-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 set InFlightCallsVersion to 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:

  • per packet, one load of the recorder hold;
  • per draw committer item and finisher job, a hold check and a store-serial scope;
  • per draw committer item, finisher job and deferred label, a read of the thread's store serial when it is queued, kept with it (the finisher keeps these in a deque beside its jobs, pushed and popped under the lock it already takes);
  • per submission, a scope, one node inserted into and erased from a set under the lock the worker already holds, and two relaxed loads of the wait-tracking flag;
  • per completed submission, an erase from one empty map and a lookup in another, under the driver lock it already holds;
  • per wait packet, one call with a relaxed load;
  • per title call and deferred label, one scope;
  • per completion registration, a read of the current serial, and for OnComplete and AfterRecordedWork with a nonzero serial, one load of the store observer;
  • per AfterCompletions action and per batch finish, one scope;
  • per reported driver store, one load of the store observer, which is null;
  • per storage-image mark and write-back, a loop over its units, and per write-back 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_DATA labels, 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 in read_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_delta or unverified_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_deltas 0), and the replay must accept the capture.
  • wait_after_read, wait64_after_read, rewind_after_read: a started submission still has to pass a WAIT_REG_MEM, WAIT_REG_MEM_64 or 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_title and title_wait_unpassed must be counted, and a replay with --no-release must 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 a COND_EXEC that 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 as title_wait_unpassed. wait_labelled_after replays 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: in wait_labelled_rewind the submission's label before its REWIND, in wait_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 count title_wait_unpassed, and the strict replay must copy the title's store only after the wait passes.
  • semaphore_after_read: the same with MEM_SEMAPHORE, signalled by a second thread once the submission has reached its wait. No delta may be read in flight with that submission listed and awaits_title must be counted, but not title_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 for unwatched, 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_page with and without driver_page_since_full, title_wait_unpassed, wait_dword_in_delta), along with the unwatched block counters and the replay's in_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 (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.
  • agc_driver_draw_finisher_overlap (with APS5_DRAW_PIPELINE=1): 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.

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 without capture.json is refused, also one with no delta read in flight. One with a delta read in flight is also refused without stale_delta or unverified_deltas, with a non-numeric or nonzero unverified_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 without capture.json are not refused. The mode calls the tool's restoreMemory directly, 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, as submit_depths does, not at an EOP flip reservation.
  • depth_passes: with two passes, the depth counters hold only the second pass's samples.
  • in_flight_breaker (was v2_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 before InFlightCallsVersion, 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's run() 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 (with APS5_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_start and flip_serial_neutral fail with a message when APS5_CPU_FLIP_IN_ORDER or APS5_SYNC_FLIP is set or APS5_DUMP_SUBMITS is 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 through OnComplete, AfterCompletions and AfterRecordedWork under 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's concurrent (above), and #12568's gpu_restore_cached now 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 neither APS5_CPU_FLIP_IN_ORDER nor APS5_SYNC_FLIP set and APS5_DUMP_SUBMITS unset or empty.

Open before ready for review

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) after git submodule update --init --recursive, which skips quickjs's nested test262. The focused set is ctest -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 head 3d1e020f44, the 35 new ones and the 51 of the base da47027017. The regex adds the tests of #9898 and #7978, the main changes commit 2 and 3 were restacked against. Earlier rows ran the previous regex (77 tests at 0191553a68, 44 at f21b60440d; three of those 44, shader_host_rewritten, shader_host_restored and shader_unaligned_restored, came with a62a6958ab). It runs with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1. Validation runs add VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation (a local build of the layer), VK_LOADER_LAYERS_DISABLE=~implicit~ and VK_LOADER_DEBUG=layer, so their logs show whether the layer loaded. e4dfa4b069 is 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 of flip_serial_start and flip_serial_neutral, flip_serial_neutral's third window and serial_diverged's listed-serial row). Each line names the commit it ran on.

Focused set, base and head:

Commit Run Passed Failed VUID- lines Layer
base da47027017 without the layer 49/51 reservation_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 reruns – –
base da47027017 with the layer 50/51 reservation_error, timeout (30.03 s) 0 loaded (61 such lines); no missing-layer line
head 3d1e020f44 without the layer, 3 runs 85/86 each reservation_error, timeout (30.02-30.04 s) – –
head 3d1e020f44 with the layer 85/86 reservation_error, timeout (30.02 s) 0 loaded (123 Inserted device layer "VK_LAYER_KHRONOS_validation" lines); no missing-layer line
base f21b60440d without the layer 43/44 reservation_error, timeout (30.05 s) – –
base f21b60440d with the layer 43/44 reservation_error, timeout (30.02 s) 0 loaded (56 Inserted device layer "VK_LAYER_KHRONOS_validation" lines); no missing-layer line
e4dfa4b069 without the layer, 3 runs 76/77 each reservation_error, timeout (30.02-30.03 s) – –
e4dfa4b069 with the layer 76/77 reservation_error, timeout (30.03 s) 0 loaded (112 such lines); no missing-layer line
head 0191553a68 without the layer, 3 runs 76/77 each reservation_error, timeout (30.03 s) – –
head 0191553a68 with the layer 76/77 reservation_error, timeout (30.08 s) 0 loaded (112 such lines); no missing-layer line
  • Under the layer, 49 of the 86 tests at 3d1e020f44 create a Vulkan device (25 of the 51 at the base da47027017), among them 24 of the 35 new ones: the 21 capture modes, carried_label and the 2 recorder tests. At 0191553a68, 44 of 77 did. The 10 new replay modes and agc_driver_draw_finisher_overlap create none, so the layer does not apply to them.
  • No test skipped in any of these runs.
  • Still not explained, from the previous description: in one validation run at 58d76317c6, without loader debug, agc_recorder_storage_writer timed 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 at 3366c145f6 and 4 of 4 each at e4dfa4b069 and this head.
  • fix(agc): leave GPU-stored words out of frame capture deltas #12568's rewind_release failed 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_error is 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 calls std::_Exit with the driver still running, which hangs on Windows, and this test is the first to reach that path.
  • Known: until fix(agc): shut the driver down before frame replay exits with an error #14100 lands, this PR's refusals during a replay hang on Windows the same way. They are: a delta read in flight after a second thread's first call; one whose accepted serial does not map to the replay's; one that lists a serial no call since the frame start recorded; a submission or suspend point that takes another serial than the one the capture's maps to, or none; a flip that does so other than in the two CPU-flip cases above; a CPU flip that took a serial only in the replay and then does not complete; and a gate whose submissions stop completing while no queue runs for --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 missing capture.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 are public_headers, which timed out, and its 64 public_headers_compile_* tests, which then did not run. The other 25: log_macros and log_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_poll exited 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 three tls_function_coverage_* tests). Another branch's build ran on the machine during part of the suite.

I reran public_headers and 18 of those 22 one at a time under the GPU lock with a 300 s timeout, on this head 0191553a68 and on the base f21b60440d. 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, then vkQueueSubmit result -4.

Three of the four, direct_write_completion, scratch_pool and sample_order_grid_reference, also fail on main 91ecd2d974 on this GPU, 3 of 3 runs each (reported on #14137); store_over_untracked has not been run on main. The other 15 passed on both sides. relr_conversion and the three tls_function_coverage_* tests, which timed out in the suite, were not rerun.

Every commit builds: git rebase -x builds libs, 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 head 3d1e020f44, 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-t2a instead injects the store that commit 10 handles. The patched tree is built, then the tests run with ANYPS5_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 on 7c7333675d plus their own fixups, and 58d76317c6 is the 9 patches of the push before the previous one (3366c145f6) on #12568's 78c714060e. The last column applies the same patch at e4dfa4b069 with git apply --3way. Where it no longer applied there, a re-anchored or regenerated patch ran at 0191553a68 instead, and the cell says so, as it does for d-t2a and u13-red4, which did not run at e4dfa4b069.

Red patch: what it undoes Test Earlier e4dfa4b069, or 0191553a68
title-call-closure: a completion a title call registers keeps the title-call serial agc_recorder_title_call_closure 58d76317c6: failed failed
storage-writer-d1: one writer serial per storage image instead of one per unit agc_recorder_storage_writer 3366c145f6, patch fixed by hand: failed 3 of 3 runs failed
storage-writer-title-call: a unit a title call marks keeps the title-call serial agc_recorder_storage_writer 58d76317c6: failed failed
unwatched-restored-l6: a call's serials are read after its delta, not when it enters unwatched_restored 58d76317c6: failed failed
in-flight-store-hazard: no stop on a driver store into a delta read in flight in_flight_store 58d76317c6: failed failed
late-store-end-check: no check at the end of the capture late_store 58d76317c6: failed failed
late-wait-c1: the end check waits for the driver to go idle, not for the deltas' submissions late_wait 58d76317c6: failed failed
mask-awaits-title: no awaits_title hold wait_after_read, wait64_after_read, rewind_after_read, wait_before_read, semaphore_after_read 58d76317c6: failed, 5/5 does not apply; 0191553a68: failed, 5/5
mask-queued: every delta made with work in flight is held as queued in_flight_delta, late_store, unwatched_in_flight 58d76317c6: failed, 3/3 does not apply; 0191553a68: failed, 3/3
in-flight-gate: no wait for the started submissions in_flight_gate 58d76317c6: failed does not apply; 0191553a68: failed
gate-failure-rethrow: SerialDone does not rethrow a driver failure gate_failure 58d76317c6: failed failed
gate-settled-r1: the gate does not end when no queue runs; only its time limit ends it gate_settled 58d76317c6: failed does not apply; 0191553a68: failed after 20.20 s
unverified-r3: a capture that stopped early is refused although its end check passed unverified 58d76317c6: failed does not apply; 0191553a68: failed
depth-submits-r5: depth is sampled at every paced call, not only at submits depth_submits 58d76317c6: failed failed
depth-passes-r5b: with --repeat, the first pass's depth samples are kept depth_passes 58d76317c6: failed failed
a-t2-overlap: OverlapsAny asks the finisher's producer-only query again agc_driver_draw_finisher_overlap tree of ef6a91568d: failed failed
a-t8g, 3 patches: the serial scope of the OnComplete, AfterCompletions or AfterRecordedWork wrap is removed agc_recorder_title_call_closure tree of ef6a91568d: failed, 3/3 failed, 3/3
a-t8h-capture: a deferred label does not take its submission's serial carried_label tree of ef6a91568d: failed failed
a-t8h-restore: a deferred label's store does not run under that serial carried_label tree of ef6a91568d: failed failed
b-u1-head-driver: the driver's title-wait rule back to the previous push's the 4 wait_labelled_* modes final code of 9fc6d89504, before it was committed: failed, 4/4 failed, 4/4
b-wait-labelled-after: a label behind the wait on the waiter's own queue counts wait_labelled_after same: failed failed
b-wait-labelled-before: the value check accepts every value wait_labelled_before, wait_labelled_elsewhere same: failed, 2/2 failed, 2/2
b-wait-labelled-skipped: a label under a COND_EXEC counts as fixed at submit wait_labelled_skipped same: failed failed
b-wait-labelled-elsewhere: another queue's label counts whatever its value wait_labelled_elsewhere same: failed failed
c-unlined: no accepted-serial check at the gate serial_diverged d6fa1cb409: failed does not apply; 0191553a68: failed
c-threaded: no second-thread check at the gate serial_diverged d6fa1cb409: failed does not apply; 0191553a68: failed
c-early: no refusal in run() before the restore serial_diverged d6fa1cb409: failed failed
c-suspendtraced: no serial check of a suspend point serial_diverged d6fa1cb409: failed failed
c-suspendkind: the early refusal ignores suspend points serial_diverged d6fa1cb409: failed failed
c-inflightbreaker: the breaker may apply a later delta early in a replay of the current calls format in_flight_breaker tree of abc85ff056: failed failed
c-prevbreaker: the breaker never applies a later delta early previous_breaker tree of abc85ff056: failed after 5.09 s failed after 5.09 s
c-unheadedprev: a capture of the previous format also needs capture.json unverified d6fa1cb409: failed failed
c-v1branch: the reader's previous-format branch unverified, previous_breaker d6fa1cb409: failed, 2/2 failed, 2/2
c-flipzero: the recorder writes 0 for every flip in_flight_delta d6fa1cb409: failed failed
c-suspendunaccepted: a suspend point is recorded without its accept in_flight_delta d6fa1cb409: failed failed
c-u7env, with APS5_CPU_FLIP_IN_ORDER=1: gpu_restore_cached always expects a CPU flip without a serial gpu_restore_cached tree of abc85ff056: failed not rerun
d-t2a (an injection): the first started submission reports a store into the delta's first page right after the hold check in_flight_delta (the delta must be held, not read in flight) 7c7333675d with commit 10's change: failed 0191553a68: failed, hold_masks driver_page+driver_page_since_full 1
d-t3red: the host-page loop does not update the unwatched record host_page_restored 9cac8e2692 with the fix uncommitted: failed does not apply; 0191553a68: failed
u13-red0: the replay tool back to the previous push's flip_serial_start 27408f2a32: 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_neutral and serial_flip_in_order failed not rerun
u13-red1: no empty command buffer before the first pass flip_serial_start 27408f2a32: failed does not apply; 0191553a68: failed
u13-red2: neither CPU-flip case of the serial check flip_serial_neutral, serial_flip_in_order 27408f2a32: failed, 2/2 failed, 2/2
u13-red3: the replay does not wait for a CPU flip that only it gave a serial to present serial_flip_in_order 27408f2a32: failed failed
rt-seed: executeRewindTail adds no labels ahead of a REWIND tail's waits wait_labelled_rewind, wait_labelled_rewind_chain 36551c543c (the fix on 4daf1dbd66, whose driver and tool are 0191553a68's): failed, 2/2, in 3 of 3 runs 3d1e020f44: failed, 2/2, in 3 of 3 runs
rt-record-only: a tail's waits see only the labels its submission's record kept, not the earlier tails' wait_labelled_rewind_chain (wait_labelled_rewind must pass) same: failed in 3 of 3 runs, wait_labelled_rewind passed 3d1e020f44: failed in 3 of 3 runs, wait_labelled_rewind passed
u13-red4: the gate maps a started serial by its distance from the accepted one flip_serial_neutral, serial_diverged new 0191553a68: failed, 2/2

Each 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, on 7c7333675d), in_flight_delta passes: the delta is read in flight and the capture is accepted with stale_delta 0, which is the silent case commit 10 closes. 34 of the 35 new tests have a red row; hold_diagnostics has none.

Not tested:

  • Linux. A WSL2 lavapipe build (kernel 5.15) at 58d76317c6 passed 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 base f21b60440d merged onto main 4f41f4c375 on an NVIDIA RTX 2000 Ada and on an AMD RX 6300 (RADV); both runs passed with 0 new failures and no new validation IDs against main.
  • A -DAPS5_ENABLE_TIMING_LOG=ON build of this head. By the code, in such a build flip_serial_start skips, flip_serial_neutral returns at once, serial_diverged leaves out its CPU-flip row, and gpu_restore_cached accepts 0 or the next serial for its CPU flip. Such a build of 4daf1dbd66 ran: its driver and tool are this head's, and its tests lack the assembled-head review's changes (the flip_serial_start guard, flip_serial_neutral's third window and serial_diverged's listed-serial row). There flip_serial_start failed 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, and serial_diverged, serial_flip_in_order, flip_serial_neutral, gpu_restore_cached and in_flight_delta passed 3 of 3. The same tree built without the timing log passed all six, 3 of 3. The environment side ran: gpu_restore_cached passed 3 of 3 runs with APS5_CPU_FLIP_IN_ORDER=1 on the tree of abc85ff056, and failed with its guard forced (the c-u7env row).
  • An empty APS5_DUMP_SUBMITS: Windows removes a variable set to empty, so the empty-is-unset terms rest on code reading.
  • The draw finisher's store serial (taken in DrawFinisher::Enqueue, applied around each job in DrawFinisher::run): the finisher runs only with APS5_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 to unattributed_stores and, like any reported store, holds later deltas over its page. It cannot make a replay silently wrong.
  • Branches of the title-wait rule that no test or red row pins: the predicated-packet and RELEASE_MEM timestamp terms of a label's fixed value, predicated INDIRECT_BUFFER ranges, the newest earlier submission on the waiter's own queue, the pairing of a 64-bit wait's two dwords, RewindValid on another queue's label, the MEM_SEMAPHORE early return, and the erase of a completed submission's labels.
  • A P5R capture with this head. The P5R numbers above come from 02132ba69e and b18ca951c1.
  • Linux for the two REWIND-tail modes, like the rest of this push's tests (above).
  • The cost outside a capture of the hooks listed above.
  • Cold times on llvmpipe. On the RX 6700 XT, in set 1 at e4dfa4b069, the new replay modes take 0.05-0.39 s, the new capture modes 0.36-1.39 s, carried_label 0.37 s, the two recorder tests 0.15-0.18 s and agc_driver_draw_finisher_overlap 0.05 s.

Checklist

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

Sam-mon7 and others added 30 commits October 27, 2026 14:16
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>
@github-actions github-actions Bot added docs agc libSceAgc* labels Nov 2, 2026
@thuanquang
thuanquang force-pushed the replay-bench/pr3-rw3 branch from 3366c14 to 7c73336 Compare November 3, 2026 03:55
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Nov 3, 2026
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>
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Nov 3, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chitters2004 and others added 11 commits November 4, 2026 17:40
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>
@thuanquang
thuanquang force-pushed the replay-bench/pr3-rw3 branch from 0191553 to 3d1e020 Compare November 5, 2026 07:04
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Nov 5, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Nov 5, 2026
@JJenkx

JJenkx commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

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 508ec40a2d6a933f1bc0cca56c3efa6d933a2171; the built control ec79a7b849d8 adds only an unrelated test fixture/CMake registration (patch included), with no production changes.

A two-frame capture was triggered at 180.9 s in the title menu using APS5_FRAME_CAPTURE, an initially absent APS5_FRAME_CAPTURE_TRIGGER file containing 2, and APS5_FRAME_CAPTURE_FRAMES=2. APS5_PROFILE_GPU=1 and APS5_PROFILE_GPU_DRAWS=1 were enabled; APS5_PROFILE_DRAW was unset. The capture wrote:

[frame-capture] started at frame boundary 1480: 994 ranges, 2456.8 MiB snapshot in 3.0 s
[frame-capture] stopped: the GPU work submitted before a memory delta did not finish within 10 s

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 05e3f5a5b121a379490112a3ac96a7548a58d1892e194dc54934e3f5397de159; downloaded archive and manifest verified. Raw captured memory and shader code remain local.

AI-assisted: yes.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs needs rebase Conflicts with main, rebase needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants