Skip to content

fix(agc): read capture deltas and replay them while queues wait on the title - #12567

Open
chitters2004 wants to merge 16 commits into
boykopovar:mainfrom
chitters2004:fix/agc-capture-settled-deltas
Open

chitters2004 wants to merge 16 commits into
boykopovar:mainfrom
chitters2004:fix/agc-capture-settled-deltas

Conversation

@chitters2004

@chitters2004 chitters2004 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What

Review db4ab05bc1 (+574 −52) and @JJenkx's 76ce637975 (+10 −3). The others belong to PRs that merge first in the agreed order and drop out of this diff as they do: #11435 and #11436 (Sam-mon7) and #12318 (Tehshortbus). #12566 has merged, and 79d7617af2 merges main (ce9394b844) into this branch.

76ce637975 is JJenkx's fixture shutdown fix (his 42ccdf0b79, first carried in #12568), kept as his commit. The replay fixture calls AgcDriverShutdown before static teardown; on Linux the driver's static destructor otherwise crashes the tests this PR adds after they pass (his GDB trace). It moves here because this PR adds those tests.

A memory delta is read only once every accepted submission has completed, so a capture stops after 10 s whenever a queue waits on a REWIND control word or a label that a later call stores: @JJenkx's two-queue reproducer on #11157 (comment), and the Ghost of Yōtei capture on #11197 (comment).

Driver:

  • AgcDriverHoldSettled(ms) succeeds once every queue has finished or is parked on a REWIND or WAIT_REG_MEM value the title has not stored yet (parkedOnTitle: no uncommitted queue 0 work, queued draw, unrecorded label or pending label write, and the awaited value still fails its compare). On success it holds: Submit and SuspendPoint wait at admission, and a worker leaving a wait waits in noteWaitBlocked, until AgcDriverReleaseSettled. AgcDriverSettled is the same check without the hold. Pm4::WaitCompares leaves Pm4.cpp's anonymous namespace for that compare.

Recorder:

Replay (built on #11436 and #12318, whose commits are included until they merge):

  • A memory delta is applied under the same hold, after the driver's writes to its pages are flushed (AgcDriverPrepareCommandWrite). It waits for the hold for up to the busy timeout, or not at all once pacing has already given up at the current serial.
  • The breaker releases a wait only when the driver is settled, no delta is being paced or applied and nothing changed during the quiet interval, and releases one wait per pass. A parked REWIND is released with fix(agc): release a parked REWIND in frame replay with its whole memory group, and write REWIND control words last #12318's whole-group restore under a settled probe (AgcDriverHoldSettled(0)). Any other wait gets the last earlier recorded value or a synthesized one; the breaker no longer writes a value from a later delta, which applied a later CPU store early.
  • Pacing gives up after --pace-timeout-ms (new option, default 10000) only once the driver is settled and progress did not change, or after the busy timeout regardless, so a queue still executing (compiling pipelines on a cold cache) counts as progress. The busy timeout is feat(agc): replay full titles with repeated passes and the title's pipeline cache #11435's ten-minute drain limit, renamed busyTimeout and shared by pacing, the settled hold and the drain, replacing a separate limit of this PR. A memory delta waits for the hold instead of a submission depth.
  • The drain keeps feat(agc): replay full titles with repeated passes and the title's pipeline cache #11435's loop and its reports every 10 s, and also ends after 10 s without a completion once the driver is settled, when nothing can complete any more.

Overlap: this follows the order agreed on #11435 (comment): #11435, #11436, #12318, then this PR, which is stacked on all three and adds its test modes to their argument check in tests/FrameReplay.cpp. #7978 changes QueueWorker.cpp L72-74 next to the worker's wait hook, #11455 rewrites BUILD.md L257, and #10760, #11447, #12348 and #12452 edit the frame capture debt entries; whichever merges second rebases those lines.

BUILD.md is updated. The frame capture debt entry loses its delta stop; two entries are added for what is still open: a capture still stops when a busy queue neither completes nor parks within 10 s, a submission another captured thread made before a delta can still reach the driver after it, and a queue that neither finishes nor parks holds the replay for the busy timeout.

Tested

Windows 11 Pro 26220, WinLibs GCC, Ninja Release, NVIDIA GeForce RTX 3080 (driver 621.01).

New tests. "Before", measured before this was restacked onto #11436, is this branch with FrameRecorder.cpp and tools/FrameReplay.cpp at their base (#12318), keeping only the --pace-timeout-ms parsing, and the admission wait removed from Submit and SuspendPoint:

  • agc_driver_frame_capture_eop now also takes a settled hold on an idle driver and checks that another thread's SuspendPoint is not admitted until the release. Before: the driver admitted a submission while a settled hold was taken on an idle driver.
  • agc_driver_frame_replay_rewind_store, _rewind_copy, _rewind_compute, _rewind_release and _busy_queue read a delta while a queue is parked on a REWIND or a fence and check the capture and the replayed bytes (see BUILD.md). Before, each capture stops after 10 s: the GPU work submitted before a memory delta did not finish within 10 s.
  • agc_driver_frame_replay_wait_label is JJenkx's reproducer. Before: the producer submission waited 10016 ms for the capture.
  • agc_driver_frame_replay_rewind_fill, _rewind_split and _fence_reuse pass before and after; they cover delta restoration and fence reuse under the hold.

All pass with this PR. _busy_queue also covers the replay's pacing and breaker rules: it runs the replay with --pace-timeout-ms 1 while a queue copies 64 MiB, and with the tool's AgcDriverSettled checks removed it fails 3 of 3 (pacing gave up at call 9 (flip): serials 0/2); with them it passes 3 of 3 in about 1 s. On GTA V (PPSA04263, North Yankton prologue, 3 frames, 13 deltas, all four PRs of this series applied), with an earlier revision of this tool that had neither the pacing rule nor the one-wait-per-pass limit, each of 6 replays gave up pacing once (pacing gave up at call 131479 (suspend): serials 0/3, the capture had 0 in flight, a queue still compiling) and released 2 to 3 waits with synthesized values. With this PR's tool before the restack, which changed only the busy limit (never reached in these runs) and the drain's reports: 1 cold-cache run with --no-synthesize (90 s) and 5 normal runs, all with 0 pace timeouts, 0 released waits, 0 synthesized and no unfinished submissions. With the restacked tool at 5ed0a3f0bc, a cold run with --no-synthesize and a warm run give the same. This is not a pixel-fidelity claim; replayed frames still differ from the title's.

Paired run on main and this branch, same machine and GPU, ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ctest --test-dir build -R "frame_(capture|replay)|sample_multisample_target" -V: main at 1235db19dc: 12 tests, all passed, 0 VUID- lines; #11436 at 210bd86b58: 19 tests, all passed, 0 VUID- lines; this branch at db4ab05bc1: 29 tests, all passed, 0 VUID- lines (the full set ran on the previous revision, which differed only in one check in tests/FrameCapture.cpp; agc_frame_capture was rerun at this head and passes). Neither log contains was not found but was requested by env var VK_INSTANCE_LAYERS.

After 4ef54b694c (the pacing warning back to its text before this PR): ctest -R "frame_(replay|capture)" -V with the same variables, full builds with submodules at their pinned commits. main bb04a4f1ea: 11 tests, all passed. This head merged into it (79e77c5da3): 28 tests, all passed, including agc_driver_frame_replay_busy_queue. 0 VUID- lines on both, and neither log has the missing-layer message.

After 76ce637975, on main 4c8cff1916 (after #12264), the same command and variables, full builds: main: 11 tests, all passed. This head merged into it (13240d9aca): 28 tests, all passed, including the nine replay modes the Linux CI saw crash. 0 VUID- lines on both, and neither log has the missing-layer message. Windows 11 26220, RTX 3080, driver 621.01.

After 79d7617af2 (main with #12566 merged in), the same command, variables and full builds: main ce9394b844: 11 tests, all passed. This head merged into it (ba86d7be1f): 28 tests, all passed. 0 VUID- lines on both, and neither log has the missing-layer message.

Checklist

🤖 Generated with Claude Code

…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>

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — last commit only

Per the PR description, this review covers commit 7431fc799665d5a853e54cbc2892f4dd81d0e1cf and its settled-driver hold. It does not independently approve the preceding stacked work in this PR or its prerequisites.

What I checked

  • Reviewed the settled predicate, wait-state capture, worker admission gates, label-pending check, delta RAII release, replay pacing/breaker call sites, and the new hold and replay tests.
  • Verified the predicate only accepts a completed queue or queues parked on still-unsatisfied title waits, and that worker resumption and new submissions are gated until the capture/replay releases the hold.
  • git diff --check upstream/main...refs/review/pr-12567-current passed.
  • git merge-tree --write-tree upstream/main refs/review/pr-12567-current was clean (8f8f26546a275d3b913eb7c95cbd09457018ce2b).
  • Hosted Windows job passed. Eligibility, conventions, intake, labels and report checks passed; Linux product tests were skipped.

Findings

Blocking

None found in the reviewed commit.

Limits

I did not build or run the AGC/Vulkan tests locally or reproduce the paired Windows/Linux and GTA V reports. Those results are author-provided; the hosted Windows job passed. This review is scoped to the requested commit and makes no console-equivalence or pixel-fidelity claim.

@oneandonlydean oneandonlydean self-assigned this Oct 28, 2026
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 12452 docs/dev/debt/libSceAgcDriver.md L144

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1835 docs/dev/BUILD.md
PR 4040 core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp
PR 5021 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
docs/dev/BUILD.md
PR 5609 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 5925 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6186 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6188 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
PR 6194 docs/dev/debt/libSceAgcDriver.md
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6280 docs/dev/debt/libSceAgcDriver.md
PR 6412 docs/dev/debt/libSceAgcDriver.md
PR 6481 docs/dev/debt/libSceAgcDriver.md
PR 6549 core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6804 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6808 docs/dev/debt/libSceAgcDriver.md
PR 7085 docs/dev/debt/libSceAgcDriver.md
PR 7275 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7604 docs/dev/debt/libSceAgcDriver.md
PR 7642 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 8592 docs/dev/debt/libSceAgcDriver.md
PR 9289 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 9356 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9387 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9592 docs/dev/debt/libSceAgcDriver.md
PR 9748 docs/dev/debt/libSceAgcDriver.md
PR 9893 core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9915 docs/dev/debt/libSceAgcDriver.md
PR 9950 docs/dev/debt/libSceAgcDriver.md
PR 10193 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 10233 docs/dev/debt/libSceAgcDriver.md
PR 10249 core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 10359 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
docs/dev/debt/libSceAgcDriver.md
PR 10442 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 10678 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
PR 11444 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 11825 docs/dev/debt/libSceAgcDriver.md
PR 12904 docs/dev/debt/libSceAgcDriver.md
PR 13486 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 13599 docs/dev/debt/libSceAgcDriver.md
PR 13707 docs/dev/debt/libSceAgcDriver.md
PR 13793 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 13809 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 13863 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 13920 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 13928 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 13929 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 14137 core/libs/prx/libSceAgcDriver/Execution/include/FrameRecorder.hpp
core/libs/prx/libSceAgcDriver/Execution/src/FrameRecorder.cpp
core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp
docs/dev/BUILD.md
docs/dev/debt/libSceAgcDriver.md
PR 14212 docs/dev/debt/libSceAgcDriver.md
PR 14344 docs/dev/debt/libSceAgcDriver.md
PR 14944 docs/dev/debt/libSceAgcDriver.md
PR 15169 docs/dev/debt/libSceAgcDriver.md
PR 15529 docs/dev/debt/libSceAgcDriver.md
PR 15548 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 15570 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 16059 docs/dev/debt/libSceAgcDriver.md

chitters2004 and others added 3 commits October 28, 2026 21:58
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>
…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>
@chitters2004
chitters2004 force-pushed the fix/agc-capture-settled-deltas branch from 7431fc7 to db4ab05 Compare October 28, 2026 22:00
@chitters2004

Copy link
Copy Markdown
Contributor Author

Restacked onto #12566's review fix (fa2b143), so the series merges in order without conflicts. The last commit is now db4ab05. Its only change from 7431fc7 is that readGuest no longer takes a read function: with this PR every capture read is coherent, so it always uses GuestMemory::ReadCommitted. Paired Vulkan validation on the new head: 29 tests, all passed, 0 VUID- lines.

@MarcZX10, could you re-approve db4ab05? Your review covered the rest of it unchanged.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — looks good

This approval is scoped to commit db4ab05bc12abeadf6a58184d2b7901c958c01bc and its ReadAroundRanges extraction; it does not independently approve the preceding stacked PRs.

What I ran

  • Compared the exact head with previously reviewed commit 7431fc799665d5a853e54cbc2892f4dd81d0e1cf.
  • Compiled the exact-head FrameCapture.cpp and tests/FrameCapture.cpp together with the exact-head header using GCC, C++20, and system Zstd; ran the resulting executable.
  • Ran git diff --check on the follow-up.

Results

Check Result
Standalone frame-capture test
Raw outputframe capture tests passed
Whitespace check
Raw outputgit diff --check 7431fc799665d5a853e54cbc2892f4dd81d0e1cf...refs/review/pr-12567 — no output; exit status 0
Hosted checks
Raw statuseswindows pass; conventions pass; labels pass; report pass; linux, register-context, relinker, relinker-asan, write-watch skipped
Vulkan validation
Author-reported exact-head resultRTX 3080, Windows 11 Pro 26220: 29 selected tests passed, 0 VUID- lines, and no missing-layer warning. The author reports the same focused command on main and intermediate stack heads with 12 and 19 tests respectively.

Negative control

The PR report includes main and intermediate-stack comparison runs, but no mutation control for this extraction. I did not run a Vulkan main-versus-change control locally.

Findings

Blocking

None found in this scoped follow-up.

Non-blocking

ReadAroundRanges walks skipped ranges in order. Its production producer, HostOnlySampleRanges, sorts the ranges before returning them; the new tests cover overlapping ranges and ranges outside the captured block. The standalone test passed.

Sources

  • Reviewed commit db4ab05bc12abeadf6a58184d2b7901c958c01bc.
  • Previous reviewed commit 7431fc799665d5a853e54cbc2892f4dd81d0e1cf.
  • Vulkan test report in the PR description at the reviewed head.

Not verified

I did not run the Vulkan suite or test a console. The Vulkan results above are author-reported; my independent run covered the CPU frame-capture target only.

To get this to a good state

No further change is requested for this commit. Keep the PR's declared dependency order so the other stacked commits are reviewed and merged separately.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@chitters2004 thanks, the settle/hold logic reads correctly (RAII release, no worker self-wait, the hold costs one atomic load when capture is off), and the tests are registered. One request about agc_frame_replay's default output, which we keep byte-identical for existing users of the tool:

  1. Default JSON and stderr. With no new option the tool now prints "passes":[...], "kept_reservations" and "journal_s" in the stdout summary (FrameReplay.cpp:1185), and appends , blocked on <...> to the progress, pacing and drain lines on stderr (:368, :411, :718, :722). Please print the new JSON fields only when --repeat (or another new option) is given and keep the previous stderr text unless a new option asks for the blocked-on detail; or tell us this tool's output is not covered by the rule and we will record it. The pipeline-cache default (APS5_HEADLESS_PIPELINE_CACHE set unless --cold-pipelines) changes timings rather than bytes; please mention it in the PR text so people comparing old runs know.
  2. Dependencies. Commits 1-8 are feat(agc): list a capture's shaders and submissions and trace replay serials #11436's (Sam) and the REWIND commit is Tehshortbus's. Please list the PRs you carry under Depends on so the second to land can drop its copies.

@chitters2004

Copy link
Copy Markdown
Contributor Author

@oneandonlydean, thanks for reading the settle/hold logic. On the two points:

  1. Most of the new default output comes from feat(agc): replay full titles with repeated passes and the title's pipeline cache #11435's commits, which this PR carries rather than adds. "passes" and "kept_reservations" come from d64657c5b0. "journal_s" and the APS5_HEADLESS_PIPELINE_CACHE default come from f691bc9f41. The blocked on suffix on the progress and drain lines comes from 8a17ba39da. The one line this PR changes is the "pacing gave up" warning: it adds the busy-timeout note and blocked on. That warning prints only when pacing gives up. If feat(agc): replay full titles with repeated passes and the title's pipeline cache #11435 moves its fields and suffixes behind an option, I'll put this line behind the same one so the tool stays consistent. Otherwise I'll revert the line to its old text here, whichever you prefer. The pipeline-cache note belongs in feat(agc): replay full titles with repeated passes and the title's pipeline cache #11435's text; @Sam-mon7, could you add it there?
  2. Depends on already lists feat(agc): list a capture's shaders and submissions and trace replay serials #11436, which carries feat(agc): replay full titles with repeated passes and the title's pipeline cache #11435's commits, and fix(agc): release a parked REWIND in frame replay with its whole memory group, and write REWIND control words last #12318 (Tehshortbus's REWIND fix), along with fix(agc): write host-only multisample memory as zeros in a capture snapshot #12566. I'll add feat(agc): replay full titles with repeated passes and the title's pipeline cache #11435 by name so it's explicit, and drop the carried commits once those PRs land.

AI-assisted: yes (Claude Code)

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@chitters2004 Thanks, that explains where the new default output comes from. Please revert the "pacing gave up" warning to its old text here, so this PR adds nothing to the tool's default output itself. The passes, kept_reservations and journal_s fields and the blocked on suffixes come from #11435's commits, so we'll take the same point there rather than here. Naming #11435 in Depends on is good.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chitters2004

Copy link
Copy Markdown
Contributor Author

@oneandonlydean done: 4ef54b694c puts the "pacing gave up" warning back to its text before this PR, byte for byte. The busy-timeout note and the blocked on detail are gone from it, so this PR adds nothing to the tool's default output. No test checked that text. The description no longer says pacing's message names what the queues wait on.

Paired run on Windows 11, RTX 3080 (NVIDIA 621.01), full builds, validation layers on, ctest -R "frame_(replay|capture)" -V: main bb04a4f1ea 11/11, this head merged into it (79e77c5da3) 28/28, including agc_driver_frame_replay_busy_queue. 0 VUID- on both. The pushed head fast-forwards from db4ab05bc1.

AI-assisted: yes (Claude Code)

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked — new head, looks good

The latest commit restores the prior pacing-timeout warning text, addressing the requested default-output change; the author also reports the refreshed paired Windows run passing.

What I ran

  • Exact PR head: 4ef54b694c2c5cd4900ff418eb1923bcf29f516f.
  • Current base inspected: main 420b6a064eadaa7c5e50b080c7fad59c41150b4b; merge preview is clean (5d547a7a98bb27bed8ad9161bc56fe4090314dce).
  • Environment: Linux 6.1.0-54-amd64 x86_64, Git 2.39.5, Python 3.11.2.
  • Commands:
git merge-tree --write-tree upstream/main refs/remotes/review/pr-12567
git diff --check upstream/main...refs/remotes/review/pr-12567
python3 tools/check_conventions.py --base upstream/main --head refs/remotes/review/pr-12567
git diff --unified=4 refs/remotes/review/pr-12567^ refs/remotes/review/pr-12567 -- core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp

Results

Check Result
Current-main merge preview Clean; tree 5d547a7a98bb27bed8ad9161bc56fe4090314dce.
git diff --check Passed; no output.
Conventions Passed.
Latest commit diff Only restores the prior pacing gave up stderr format; the busy-timeout and blocked-queue suffixes are removed.
Author-reported paired run On Windows 11 Pro, RTX 3080, driver 621.01: full builds; `frame_(replay
Hosted checks Windows, eligibility, conventions, labels and report pass; Linux and specialized product jobs are skipped by workflow policy.
Unedited local output
$ git merge-tree --write-tree upstream/main refs/remotes/review/pr-12567
5d547a7a98bb27bed8ad9161bc56fe4090314dce
$ python3 tools/check_conventions.py --base upstream/main --head refs/remotes/review/pr-12567
All conventions checks passed.
$ git diff --stat refs/remotes/review/pr-12567^ refs/remotes/review/pr-12567 -- core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp
core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)

Negative control

No mutation control was run for the stderr-format restoration. The author reports a paired main/PR run, but says no test specifically asserted the restored warning text.

Findings

Blocking

None found in the latest commit.

Non-blocking

The author-reported test run was against bb04a4f1ea, not the newer 420b6a064eadaa7c5e50b080c7fad59c41150b4b; my merge preview against current main is clean, but I did not rerun the product suite locally. This approval is scoped to the previously approved follow-up and the latest warning-text restoration; the PR still declares its dependency stack in the body.

Sources

Not verified

I did not build or run CTest locally, test on a console, or independently rerun the author-reported Windows/GPU matrix. No test specifically asserts the warning text.

To get this to a good state

No further change is requested for this scoped follow-up. Preserve the declared dependency order; I will recheck any changed head and rerun the relevant commands then.

@MarcZX10

Copy link
Copy Markdown
Contributor

Correction — author test report link

The Windows paired-run results cited above are in the author’s 19:46 UTC comment; the issue-comment link in the review’s Sources section was incorrect.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 29, 2026
@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ AMD GPU test: passed

PR head 1ed9922 merged onto main a4eca2a · tested 2026-11-03 02:13 UTC · 42 min

GPU AMD Radeon RX 6300 (RADV NAVI24), driver Mesa 25.2.8-0ubuntu0.24.04.4, Vulkan 1.4.318
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-42-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,689 passed · 12 failed (0 new, 11 also fail on main, 1 unstable on main) · 54 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,755 of 3,591 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (993 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 120.3 s, ⚠️ agc_driver_graphics 101.0 s, ⚠️ public_headers 65.7 s, ⚠️ unwrap_self 38.7 s, ⚠️ agc_driver_lazy_resolved_abi_switch_set 35.8 s
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 4 of the last 6 main runs
Failing on main too (11, not counted against this PR)
agc_driver_guest_memory_lifetime_direct
agc_driver_image_table
agc_driver_native_depth_bda_clear
agc_driver_native_depth_buffer_clear
agc_driver_native_depth_clears
agc_driver_native_multisample_depth
agc_idle_publish_wait
agc_submit_progress
agc_submit_progress_no_timeline
guest_mmap_executable
guest_socket_options_multicast
Validated but not compared (8, not in the main baseline's validation pass)
agc_driver_frame_replay_busy_queue
agc_driver_frame_replay_fence_reuse
agc_driver_frame_replay_rewind_compute
agc_driver_frame_replay_rewind_copy
agc_driver_frame_replay_rewind_fill
agc_driver_frame_replay_rewind_release
agc_driver_frame_replay_rewind_split
agc_driver_frame_replay_wait_label
Validation IDs already present on main (4 IDs in 516 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (516 tests): agc_driver, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, agc_driver_bda_span_reads, and 508 more
VUID-vkCmdCopyImage-srcImage-01548 (6 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-vkCmdCopyImage-srcImage-01551 (6 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (1 test): agc_driver_prepared_exports
Skipped (54)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_copied_writer_label_order: skipped, the device reports a minStorageBufferOffsetAlignment of 4, so the dispatch binds its record at +0x1004 in place and nothing is written back; the test needs an alignment above 4
agc_driver_float16_unused_nan_payloads: skipped, the device has no native f16 conversions with RTE rounding and kept f16 denormals
agc_driver_image_table_vouched_lookups: skipped, the device reports 4 of 4 host-imported pages written after a GPU read, so a watched imported table cannot be vouched for
agc_driver_ngg_vertex_wave_reduction: skipped, s3 describes a 32-lane wave only on 32-lane subgroups, and the device has 64 lanes
agc_driver_unit_shadow: skipped, the device does not import watched host memory for unit shadows
agc_driver_unit_shadow_budget: skipped, the device does not import watched host memory for unit shadows
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_shadowed_key_stores: skipped, the device imports no watched host memory under a unit shadow
agc_small_tiled_shadow_label: skipped, host imports are unavailable: the test block is not imported and watched
agc_storage_clear_hand_over: skipped, the device imports no watched host memory for the clear-key hand-over
agc_unit_shadow: skipped, the device does not import watched host memory for unit shadows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip_in_order: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_dcc: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_deferr
Reproduce
git fetch origin 1ed9922bb52a25ecbc797ceeef03de234b4af435 a4eca2a056bf3bbbec98ab42e9d28163e4ea9cb8
git checkout a4eca2a056bf3bbbec98ab42e9d28163e4ea9cb8 && git merge --no-ff 1ed9922bb52a25ecbc797ceeef03de234b4af435
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 12 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 16 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's AMD machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@embersas

embersas commented Nov 3, 2026

Copy link
Copy Markdown

Diagnostic follow-up: buffered image observations (no game-FPS claim)

Sharing this here alongside our earlier replay findings so others investigating intermittent image differences do not spend time rediscovering the same observer interference. This is separate from the reader optimisation already discussed above and is not a request to expand this capture-fix PR.

The improvement is to our experimental diagnostic logger, not a newly discovered fault in upstream's general logging. Upstream already has asynchronous logging from #6371. Our image observer used direct fprintf(stderr, ...) calls; the buffered mode below avoids that synchronous I/O at the observation sites. Reusing the existing logging infrastructure should be considered before any upstream port.

Observation and change

Two nearby selected-pixel observer calls, with no explicit GPU work between them, were consistently about 24–26 ms apart in our preceding diagnostic runs. We added opt-in APS5_IMAGE_OBSERVE_BUFFERED=1: 4,096 preallocated slots per module, unique reservations, details copied by value, release/acquire readiness, and explicit incomplete/overflow/truncation/late-record diagnostics. Normal recording has no explicit per-record allocation or stream I/O. Formatting and sampling still cost time.

Driver and replay buffers are flushed separately after final image acquisition and existing shutdown work. The change adds no GPU command, wait or resource retention and does not change publication stores or ordering. Flush seals reservations, skips unreadied payloads with an explicit incomplete count, and never waits for writers. Our external harness rejects missing module summaries or any incomplete/overflow/truncated/late record. The diagnostic failure marker alone does not change replay's return code; consumers must enforce that completeness gate. Abnormal shutdown can lose the driver's unflushed records.

Bounded qualification, not a workload benchmark

One predeclared synchronous run followed by one buffered run, the same frozen executable and libraries with only the buffered flag changed. Windows, Ryzen 9 3900X, 64 GiB RAM, RTX 3080 Ti 12 GiB, NVIDIA 610.88, MinGW GCC 15.2 Release. Profiling off; hidden/muted; --strict --no-synthesize --repeat 3; separate byte-identical copies of the same warm application cache (13,821 files). Driver caches were untouched and uncontrolled. No build, tests, hashing or image analysis overlapped replay. No replacement runs.

Check Synchronous Buffered
Final images exact against unchanged replay references 3/3 3/3
Changed pixels 0 0
Submissions / restored MiB 45 / 13,268.2 45 / 13,268.2
Real / synthesised wait releases 0 / 0 0 / 0
Observer records / selected publication observations 134 / 6 134 / 6
Coordinate-to-decoded observer timestamp gap 23.7862 ms 0.0013 ms

Buffered output retained 50 driver + 84 replay records, all complete, with no overflow, truncation or late records. Both modes had identical stage counts and matching observed byte chains and conversion settings. These are three final buffer images after repeat3, not independent images of every temporal frame.

The timestamp gap includes formatting/helpers and scheduling; it is not isolated fprintf CPU cost. One ordered pair establishes no confidence interval, average per-event cost, repeatable whole-replay improvement or live FPS gain. Do not multiply the gap by all records or treat it as a frame-budget saving. We have not shown that synchronous observation was suppressing the intermittent failure.

Both runs had zero failed/stale registrations, unfinished submissions, skipped MiB, pacing timeouts, reservation errors, pending-image destruction warnings, contention flags or reported validation diagnostics. Existing warnings remain: packet-wait warnings 2/1, one prebuild snapshot disagreement and eight pending-image CPU-change warnings per run. No warning or correctness check was suppressed.

Focused checks and limits

  • Synthetic recorder fixture: eight writers, 128 unique records retained exactly once; explicit overflow, unfinished reservation, truncation, sealing/duplicate flush and late-record rejection; existing parser/boundary/read-only sampling cases retained. Expected negative-test late record after flush FAILED messages occur in this fixture and are forbidden in qualified replay logs.
  • Seven focused CPU CTests and two GPU CTests passed with each of the preserved diagnostic and new buffered runtime library sets. The header-only fixture is the same new executable in both arms, not an independently implemented baseline recorder. GPU checks: agc_driver_storage_flush_ordering and agc_driver_storage_read_during_idle_store, serial, required Vulkan, shader cache disabled, Khronos validation 1.4.363 explicitly loaded (loader diagnostics confirmed), no VUIDs/validation warnings/errors, missing layers or skips. Replay itself did not newly force validation.
  • Full isolated build, source conventions and whitespace checks passed. Six compiler warnings were retained, in glslang, libkernel and PipelineLibrary/STL comparisons; none originated in the new recorder. Original 473 artifact checksums still pass. The previously documented Windows agc_idle_publish_wait principal-assertion coverage gap remains open.
  • Neither historical image failure recurred. The first divergent stage remains unresolved; performance testing stays stopped. No performance optimisation is qualified by these passing runs. The raw observations bracket the whole decode and do not identify the exact scalar word consumed at the selected pixel; staging also does not identify the original GPU producer.
  • Tested on our archived local diagnostic base 8c4b2c9f8568c0fe93efc8830feb1e0ebcc198b0 plus the earlier conversion observer, not current main, other GPUs/OSes, a live scene or the byte-identical historical failing control. This is experimental source for reuse, not a merge-ready upstream patch. The next useful diagnostic is a by-value observation of the actual selected conversion input/result without rereading or changing its semantics.

Source for reuse

The incremental patch below is against our local diagnostic observer layer, which is not present in upstream main. It ignores end-of-line whitespace changes and excludes the earlier conversion observer; it is not a complete reconstruction of the private investigation build and should not be applied directly to this PR. Complete recorder header and synthetic fixture source are also included so the implementation can be reviewed/reused without that local layer. Porting must place the flush sites after the relevant producers are quiescent and final images are acquired, preserve separate module buffers, enforce the completeness gate and add any needed include/test registration. No current-main qualification is claimed.

A standalone synthetic fixture can be built with a C++20 MinGW toolchain after placing these two files at the shown paths:

g++ -std=c++20 -O2 -I core/libs core/libs/prx/libSceAgcDriver/tests/ImageObservation.cpp -o image_observation_tests.exe
$env:APS5_IMAGE_OBSERVE='1000,17,19'
$env:APS5_IMAGE_OBSERVE_BUFFERED='1'
./image_observation_tests.exe

The address above belongs only to synthetic test data. No private capture, image, shader, guest-memory sample, game code, personal path or binary is included in this comment.

AI-assisted: yes (Codex).

Incremental source-only review patch against the local observer layer
diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt
index 0633583..4e55f3e 100644
--- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt
+++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt
@@ -505,6 +505,8 @@ if(BUILD_TESTING)
     add_test(NAME agc_image_observation_disabled COMMAND agc_image_observation_tests)
     add_test(NAME agc_image_observation_enabled COMMAND agc_image_observation_tests)
     set_tests_properties(agc_image_observation_enabled PROPERTIES ENVIRONMENT "APS5_IMAGE_OBSERVE=1000,17,19")
+    add_test(NAME agc_image_observation_buffered COMMAND agc_image_observation_tests)
+    set_tests_properties(agc_image_observation_buffered PROPERTIES ENVIRONMENT "APS5_IMAGE_OBSERVE=1000,17,19;APS5_IMAGE_OBSERVE_BUFFERED=1")
 
     add_test_executable(agc_frame_dump_io_tests tests/FrameDumpIo.cpp)
     target_include_directories(agc_frame_dump_io_tests PRIVATE ${LIBS_INCLUDE_DIR})
diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ImageObservation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ImageObservation.hpp
index 318dd06..591f796 100644
--- a/core/libs/prx/libSceAgcDriver/Execution/include/ImageObservation.hpp
+++ b/core/libs/prx/libSceAgcDriver/Execution/include/ImageObservation.hpp
@@ -58,9 +58,101 @@ inline bool Selects(std::uint64_t address, std::size_t bytes) {
     return Selected().address != 0 && Contains(address, bytes, Selected().address);
 }
 
+template<std::size_t TCapacity>
+class RecordBuffer {
+public:
+    struct Record {
+        std::atomic<bool> ready{false};
+        long long ns = 0;
+        std::size_t thread = 0;
+        std::array<char, 64> stage{};
+        std::array<char, 768> detail{};
+        bool truncated = false;
+    };
+
+    Record* Reserve() {
+        auto value = state.load(std::memory_order_relaxed);
+        for (;;) {
+            if ((value & sealed) != 0) {
+                late.fetch_add(1, std::memory_order_relaxed);
+                std::fprintf(stderr, "[image-observe-buffer] late record after flush FAILED\n");
+                return nullptr;
+            }
+            if (value == TCapacity) {
+                overflow.fetch_add(1, std::memory_order_relaxed);
+                return nullptr;
+            }
+            if (state.compare_exchange_weak(value, value + 1, std::memory_order_relaxed)) return &records[value];
+        }
+    }
+
+    template<typename... TArgs>
+    void Write(const char* stage, const char* format, TArgs... args) {
+        auto* record = Reserve();
+        if (record == nullptr) return;
+        record->ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
+        record->thread = std::hash<std::thread::id>{}(std::this_thread::get_id());
+        const auto stageSize = std::snprintf(record->stage.data(), record->stage.size(), "%s", stage);
+        const auto detailSize = std::snprintf(record->detail.data(), record->detail.size(), format, args...);
+        record->truncated = stageSize < 0 || static_cast<std::size_t>(stageSize) >= record->stage.size() || detailSize < 0 || static_cast<std::size_t>(detailSize) >= record->detail.size();
+        record->ready.store(true, std::memory_order_release);
+    }
+
+    bool Flush(std::FILE* stream, const char* module) {
+        const auto value = state.fetch_or(sealed, std::memory_order_acq_rel);
+        if ((value & sealed) != 0) return false;
+        std::size_t complete = 0;
+        std::size_t truncated = 0;
+        bool output = true;
+        for (std::size_t i = 0; i < value; ++i) {
+            const auto& record = records[i];
+            if (!record.ready.load(std::memory_order_acquire)) continue;
+            ++complete;
+            truncated += record.truncated;
+            output &= std::fprintf(stream, "[image-observe] ns=%lld thread=%zu stage=%s %s\n", record.ns, record.thread, record.stage.data(), record.detail.data()) >= 0;
+        }
+        const auto dropped = overflow.load(std::memory_order_relaxed);
+        const auto after = late.load(std::memory_order_relaxed);
+        output &= std::fprintf(stream, "[image-observe-buffer] module=%s reserved=%llu complete=%zu incomplete=%llu overflow=%llu truncated=%zu late=%llu\n", module, static_cast<unsigned long long>(value), complete, static_cast<unsigned long long>(value - complete), static_cast<unsigned long long>(dropped), truncated, static_cast<unsigned long long>(after)) >= 0;
+        output &= std::fflush(stream) == 0;
+        return output && complete == value && dropped == 0 && truncated == 0 && after == 0;
+    }
+
+private:
+    static_assert(TCapacity > 0 && TCapacity < (std::uint64_t{1} << 63));
+    static constexpr std::uint64_t sealed = std::uint64_t{1} << 63;
+    std::array<Record, TCapacity> records{};
+    std::atomic<std::uint64_t> state{0};
+    std::atomic<std::uint64_t> overflow{0};
+    std::atomic<std::uint64_t> late{0};
+};
+
+inline bool Buffered() {
+    static const bool enabled = [] {
+        const auto* value = std::getenv("APS5_IMAGE_OBSERVE_BUFFERED");
+        if (value == nullptr || std::string_view(value) == "0") return false;
+        if (std::string_view(value) != "1") throw std::runtime_error("image observation buffered mode requires 0 or 1");
+        return true;
+    }();
+    return enabled;
+}
+
+inline RecordBuffer<4096>& Buffer() {
+    static constinit RecordBuffer<4096> buffer;
+    return buffer;
+}
+
+inline void Flush(const char* module) {
+    if (Selected().address != 0 && Buffered() && !Buffer().Flush(stderr, module)) std::fprintf(stderr, "[image-observe-buffer] module=%s FAILED\n", module);
+}
+
 template<typename... TArgs>
 void Log(const char* stage, const char* format, TArgs... args) {
     if (Selected().address == 0) return;
+    if (Buffered()) {
+        Buffer().Write(stage, format, args...);
+        return;
+    }
     static std::atomic<unsigned> count{0};
     const auto event = count.fetch_add(1, std::memory_order_relaxed);
     if (event > 4096) return;
diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
index 3811e82..f503fac 100644
--- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
+++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp
@@ -190,6 +190,7 @@ void Driver::stop() {
     FlushAsyncLogAtEnd_nid_no_patch();
     StopProfileOutput_nid_no_patch();
     stopped = true;
+    ImageObservation::Flush("driver");
 }
 
 Driver::Driver() {
diff --git a/core/libs/prx/libSceAgcDriver/tests/ImageObservation.cpp b/core/libs/prx/libSceAgcDriver/tests/ImageObservation.cpp
index d0129c3..e31a959 100644
--- a/core/libs/prx/libSceAgcDriver/tests/ImageObservation.cpp
+++ b/core/libs/prx/libSceAgcDriver/tests/ImageObservation.cpp
@@ -1,6 +1,8 @@
 #include "prx/libSceAgcDriver/Execution/include/ImageObservation.hpp"
 #include <array>
 #include <limits>
+#include <memory>
+#include <string>
 #include <thread>
 
 int main() {
@@ -39,5 +41,51 @@ int main() {
     firstThread.join();
     secondThread.join();
     require(bytes == saved);
+    const auto capture = [&](auto& buffer, const char* module, bool expected) {
+        const std::unique_ptr<std::FILE, decltype(&std::fclose)> file(std::tmpfile(), &std::fclose);
+        require(file != nullptr);
+        require(buffer.Flush(file.get(), module) == expected);
+        std::rewind(file.get());
+        std::string result;
+        std::array<char, 1024> chunk{};
+        while (const auto size = std::fread(chunk.data(), 1, chunk.size(), file.get())) result.append(chunk.data(), size);
+        require(!std::ferror(file.get()));
+        return result;
+    };
+    RecordBuffer<128> concurrent;
+    std::array<std::thread, 8> writers;
+    for (std::size_t i = 0; i < writers.size(); ++i) writers[i] = std::thread([&, i] {
+        for (unsigned j = 0; j < 16; ++j) concurrent.Write("concurrent", "writer=%zu item=%u!", i, j);
+    });
+    for (auto& writer : writers) writer.join();
+    const auto records = capture(concurrent, "concurrent", true);
+    require(records.find("reserved=128 complete=128 incomplete=0 overflow=0 truncated=0 late=0") != records.npos);
+    for (unsigned i = 0; i < 8; ++i) for (unsigned j = 0; j < 16; ++j) {
+        const auto key = "writer=" + std::to_string(i) + " item=" + std::to_string(j) + "!";
+        const auto index = records.find(key);
+        require(index != records.npos && records.find(key, index + 1) == records.npos);
+    }
+    require(concurrent.Reserve() == nullptr);
+    RecordBuffer<1> overflow;
+    overflow.Write("first", "%s", "retained");
+    overflow.Write("second", "%s", "not retained");
+    const auto overflowText = capture(overflow, "overflow", false);
+    require(overflowText.find("overflow=1") != overflowText.npos && overflowText.find("not retained") == overflowText.npos);
+    RecordBuffer<1> incomplete;
+    auto* reserved = incomplete.Reserve();
+    require(reserved != nullptr);
+    const auto incompleteText = capture(incomplete, "incomplete", false);
+    require(incompleteText.find("complete=0 incomplete=1") != incompleteText.npos);
+    reserved->ready.store(true, std::memory_order_release);
+    RecordBuffer<1> truncated;
+    const std::string longText(900, 'x');
+    truncated.Write(longText.c_str(), "%s", longText.c_str());
+    const auto truncatedText = capture(truncated, "truncated", false);
+    require(truncatedText.find("truncated=1") != truncatedText.npos);
+    RecordBuffer<1> late;
+    capture(late, "empty", true);
+    late.Write("late", "%s", "not retained");
+    capture(late, "duplicate", false);
+    Flush("fixture");
     std::puts("image observation parser, boundaries, aliases and concurrent read-only sampling passed");
 }
diff --git a/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp
index 8c25735..f991eb2 100644
--- a/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp
+++ b/core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp
@@ -1351,12 +1351,14 @@ int main(int argc, char** argv) {
             AgcDriver::ImageObservation::Log("replay-shutdown-begin", "%s", "");
             AgcDriverShutdown_nid_postfix();
             AgcDriver::ImageObservation::Log("replay-shutdown-end", "%s", "");
+            AgcDriver::ImageObservation::Flush("replay");
             return 0;
         }
     } catch (const std::exception& error) {
         std::fprintf(stderr, "agc_frame_replay: %s\n", error.what());
     }
     std::fflush(stdout);
+    AgcDriver::ImageObservation::Flush("replay-error");
     std::fflush(stderr);
     std::_Exit(code);
 }
Complete recorder header: core/libs/prx/libSceAgcDriver/Execution/include/ImageObservation.hpp
#pragma once

#include <algorithm>
#include <array>
#include <atomic>
#include <charconv>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <span>
#include <stdexcept>
#include <string_view>
#include <thread>

namespace AgcDriver::ImageObservation {

struct Selection {
    std::uint64_t address = 0;
    std::uint32_t x = 0;
    std::uint32_t y = 0;
};

inline Selection Parse(std::string_view value) {
    if (value.empty()) return {};
    const auto first = value.find(',');
    const auto second = first == value.npos ? value.npos : value.find(',', first + 1);
    if (first == value.npos || second == value.npos) throw std::runtime_error("image observation requires hex-address,x,y");
    auto address = value.substr(0, first);
    if (address.starts_with("0x") || address.starts_with("0X")) address.remove_prefix(2);
    const auto number = [](std::string_view text, auto& result, int base) {
        const auto parsed = std::from_chars(text.data(), text.data() + text.size(), result, base);
        if (text.empty() || parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size()) throw std::runtime_error("invalid image observation number");
    };
    Selection selected;
    number(address, selected.address, 16);
    number(value.substr(first + 1, second - first - 1), selected.x, 10);
    number(value.substr(second + 1), selected.y, 10);
    if (selected.address == 0) throw std::runtime_error("image observation address is zero");
    return selected;
}

inline const Selection& Selected() {
    static const Selection selected = [] {
        const auto* value = std::getenv("APS5_IMAGE_OBSERVE");
        return Parse(value == nullptr ? std::string_view{} : std::string_view(value));
    }();
    return selected;
}

inline bool Contains(std::uint64_t address, std::size_t bytes, std::uint64_t selected) {
    return selected >= address && selected - address < bytes;
}

inline bool Selects(std::uint64_t address, std::size_t bytes) {
    return Selected().address != 0 && Contains(address, bytes, Selected().address);
}

template<std::size_t TCapacity>
class RecordBuffer {
public:
    struct Record {
        std::atomic<bool> ready{false};
        long long ns = 0;
        std::size_t thread = 0;
        std::array<char, 64> stage{};
        std::array<char, 768> detail{};
        bool truncated = false;
    };

    Record* Reserve() {
        auto value = state.load(std::memory_order_relaxed);
        for (;;) {
            if ((value & sealed) != 0) {
                late.fetch_add(1, std::memory_order_relaxed);
                std::fprintf(stderr, "[image-observe-buffer] late record after flush FAILED\n");
                return nullptr;
            }
            if (value == TCapacity) {
                overflow.fetch_add(1, std::memory_order_relaxed);
                return nullptr;
            }
            if (state.compare_exchange_weak(value, value + 1, std::memory_order_relaxed)) return &records[value];
        }
    }

    template<typename... TArgs>
    void Write(const char* stage, const char* format, TArgs... args) {
        auto* record = Reserve();
        if (record == nullptr) return;
        record->ns = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
        record->thread = std::hash<std::thread::id>{}(std::this_thread::get_id());
        const auto stageSize = std::snprintf(record->stage.data(), record->stage.size(), "%s", stage);
        const auto detailSize = std::snprintf(record->detail.data(), record->detail.size(), format, args...);
        record->truncated = stageSize < 0 || static_cast<std::size_t>(stageSize) >= record->stage.size() || detailSize < 0 || static_cast<std::size_t>(detailSize) >= record->detail.size();
        record->ready.store(true, std::memory_order_release);
    }

    bool Flush(std::FILE* stream, const char* module) {
        const auto value = state.fetch_or(sealed, std::memory_order_acq_rel);
        if ((value & sealed) != 0) return false;
        std::size_t complete = 0;
        std::size_t truncated = 0;
        bool output = true;
        for (std::size_t i = 0; i < value; ++i) {
            const auto& record = records[i];
            if (!record.ready.load(std::memory_order_acquire)) continue;
            ++complete;
            truncated += record.truncated;
            output &= std::fprintf(stream, "[image-observe] ns=%lld thread=%zu stage=%s %s\n", record.ns, record.thread, record.stage.data(), record.detail.data()) >= 0;
        }
        const auto dropped = overflow.load(std::memory_order_relaxed);
        const auto after = late.load(std::memory_order_relaxed);
        output &= std::fprintf(stream, "[image-observe-buffer] module=%s reserved=%llu complete=%zu incomplete=%llu overflow=%llu truncated=%zu late=%llu\n", module, static_cast<unsigned long long>(value), complete, static_cast<unsigned long long>(value - complete), static_cast<unsigned long long>(dropped), truncated, static_cast<unsigned long long>(after)) >= 0;
        output &= std::fflush(stream) == 0;
        return output && complete == value && dropped == 0 && truncated == 0 && after == 0;
    }

private:
    static_assert(TCapacity > 0 && TCapacity < (std::uint64_t{1} << 63));
    static constexpr std::uint64_t sealed = std::uint64_t{1} << 63;
    std::array<Record, TCapacity> records{};
    std::atomic<std::uint64_t> state{0};
    std::atomic<std::uint64_t> overflow{0};
    std::atomic<std::uint64_t> late{0};
};

inline bool Buffered() {
    static const bool enabled = [] {
        const auto* value = std::getenv("APS5_IMAGE_OBSERVE_BUFFERED");
        if (value == nullptr || std::string_view(value) == "0") return false;
        if (std::string_view(value) != "1") throw std::runtime_error("image observation buffered mode requires 0 or 1");
        return true;
    }();
    return enabled;
}

inline RecordBuffer<4096>& Buffer() {
    static constinit RecordBuffer<4096> buffer;
    return buffer;
}

inline void Flush(const char* module) {
    if (Selected().address != 0 && Buffered() && !Buffer().Flush(stderr, module)) std::fprintf(stderr, "[image-observe-buffer] module=%s FAILED\n", module);
}

template<typename... TArgs>
void Log(const char* stage, const char* format, TArgs... args) {
    if (Selected().address == 0) return;
    if (Buffered()) {
        Buffer().Write(stage, format, args...);
        return;
    }
    static std::atomic<unsigned> count{0};
    const auto event = count.fetch_add(1, std::memory_order_relaxed);
    if (event > 4096) return;
    const auto now = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
    if (event == 4096) {
        std::fprintf(stderr, "[image-observe] ns=%lld stage=overflow\n", static_cast<long long>(now));
        return;
    }
    std::array<char, 768> detail{};
    std::snprintf(detail.data(), detail.size(), format, args...);
    std::fprintf(stderr, "[image-observe] ns=%lld thread=%zu stage=%s %s\n", static_cast<long long>(now), std::hash<std::thread::id>{}(std::this_thread::get_id()), stage, detail.data());
}

struct Window {
    std::size_t offset = 0;
    std::size_t bytes = 0;
};

inline Window SelectWindow(std::uint64_t address, std::size_t bytes, std::uint64_t selected) {
    if (!Contains(address, bytes, selected)) return {};
    const auto offset = static_cast<std::size_t>(selected - address);
    const auto first = offset > 16 ? offset - 16 : 0;
    return {first, std::min(bytes - first, offset - first + 20)};
}

inline void Bytes(const char* stage, std::uint64_t address, std::span<const std::byte> source, const void* owner = nullptr, std::uint64_t generation = 0) {
    if (!Selects(address, source.size())) return;
    const auto window = SelectWindow(address, source.size(), Selected().address);
    std::array<char, 81> hex{};
    constexpr char digits[] = "0123456789abcdef";
    for (std::size_t i = 0; i < window.bytes; ++i) {
        const auto byte = std::to_integer<unsigned>(source[window.offset + i]);
        hex[2 * i] = digits[byte >> 4];
        hex[2 * i + 1] = digits[byte & 15];
    }
    Log(stage, "owner=%p generation=%llu range=%llx+%zu selected=%llx sample=%llx bytes=%zu hex=%s", owner, static_cast<unsigned long long>(generation), static_cast<unsigned long long>(address), source.size(), static_cast<unsigned long long>(Selected().address), static_cast<unsigned long long>(address + window.offset), window.bytes, hex.data());
}

}
Complete synthetic fixture: core/libs/prx/libSceAgcDriver/tests/ImageObservation.cpp
#include "prx/libSceAgcDriver/Execution/include/ImageObservation.hpp"
#include <array>
#include <limits>
#include <memory>
#include <string>
#include <thread>

int main() {
    using namespace AgcDriver::ImageObservation;
    const auto require = [](bool condition) {
        if (!condition) throw std::runtime_error("image observation regression failed");
    };
    const auto selected = Parse("0x1000,17,19");
    require(selected.address == 4096 && selected.x == 17 && selected.y == 19);
    require(Parse("").address == 0);
    for (const auto* value : {"0,1,2", "1000", "1000,1", "1000,1,2,3", "1000,-1,2", "1000,1,4294967296", "10000000000000000,1,2", "g,1,2"}) {
        bool rejected = false;
        try { static_cast<void>(Parse(value)); } catch (const std::runtime_error&) { rejected = true; }
        require(rejected);
    }
    require(!Contains(100, 0, 100) && !Contains(100, 4, 99) && !Contains(100, 4, 104));
    require(Contains(100, 4, 100) && Contains(100, 4, 103));
    constexpr auto maximum = std::numeric_limits<std::uint64_t>::max();
    require(Contains(maximum - 3, 4, maximum));
    const auto full = SelectWindow(100, 100, 150);
    require(full.offset == 34 && full.bytes == 36);
    const auto first = SelectWindow(100, 4, 100);
    const auto last = SelectWindow(100, 100, 199);
    require(first.offset == 0 && first.bytes == 4);
    require(last.offset == 83 && last.bytes == 17);
    require(SelectWindow(100, 4, 104).bytes == 0);
    std::array<std::byte, 64> bytes{};
    for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast<std::byte>(i);
    const auto saved = bytes;
    const auto observe = [&] {
        Bytes("test-whole", 4096, bytes);
        Bytes("test-alias", 4096, std::span<const std::byte>(bytes).subspan(4, 8));
        Bytes("test-partial", 4096, std::span<const std::byte>(bytes).first(1));
    };
    std::thread firstThread(observe), secondThread(observe);
    firstThread.join();
    secondThread.join();
    require(bytes == saved);
    const auto capture = [&](auto& buffer, const char* module, bool expected) {
        const std::unique_ptr<std::FILE, decltype(&std::fclose)> file(std::tmpfile(), &std::fclose);
        require(file != nullptr);
        require(buffer.Flush(file.get(), module) == expected);
        std::rewind(file.get());
        std::string result;
        std::array<char, 1024> chunk{};
        while (const auto size = std::fread(chunk.data(), 1, chunk.size(), file.get())) result.append(chunk.data(), size);
        require(!std::ferror(file.get()));
        return result;
    };
    RecordBuffer<128> concurrent;
    std::array<std::thread, 8> writers;
    for (std::size_t i = 0; i < writers.size(); ++i) writers[i] = std::thread([&, i] {
        for (unsigned j = 0; j < 16; ++j) concurrent.Write("concurrent", "writer=%zu item=%u!", i, j);
    });
    for (auto& writer : writers) writer.join();
    const auto records = capture(concurrent, "concurrent", true);
    require(records.find("reserved=128 complete=128 incomplete=0 overflow=0 truncated=0 late=0") != records.npos);
    for (unsigned i = 0; i < 8; ++i) for (unsigned j = 0; j < 16; ++j) {
        const auto key = "writer=" + std::to_string(i) + " item=" + std::to_string(j) + "!";
        const auto index = records.find(key);
        require(index != records.npos && records.find(key, index + 1) == records.npos);
    }
    require(concurrent.Reserve() == nullptr);
    RecordBuffer<1> overflow;
    overflow.Write("first", "%s", "retained");
    overflow.Write("second", "%s", "not retained");
    const auto overflowText = capture(overflow, "overflow", false);
    require(overflowText.find("overflow=1") != overflowText.npos && overflowText.find("not retained") == overflowText.npos);
    RecordBuffer<1> incomplete;
    auto* reserved = incomplete.Reserve();
    require(reserved != nullptr);
    const auto incompleteText = capture(incomplete, "incomplete", false);
    require(incompleteText.find("complete=0 incomplete=1") != incompleteText.npos);
    reserved->ready.store(true, std::memory_order_release);
    RecordBuffer<1> truncated;
    const std::string longText(900, 'x');
    truncated.Write(longText.c_str(), "%s", longText.c_str());
    const auto truncatedText = capture(truncated, "truncated", false);
    require(truncatedText.find("truncated=1") != truncatedText.npos);
    RecordBuffer<1> late;
    capture(late, "empty", true);
    late.Write("late", "%s", "not retained");
    capture(late, "duplicate", false);
    Flush("fixture");
    std::puts("image observation parser, boundaries, aliases and concurrent read-only sampling passed");
}

@thuanquang

Copy link
Copy Markdown
Contributor

@embersas On whether synchronous observation suppressed your intermittent failure: in our replays, a 22 µs busy-wait per delta chunk restore suppressed a restore-timing failure. That is one mechanism worth ruling out; we don't know it is yours.

Our reader pools memory.bin streams like yours, each with its own zstd context and a grow-only compressed buffer. On the PPSA05110 capture from our 11-01 comment, it matches a separate ZSTD_decompress on all 39,635 chunks, and reads + decompresses the 33,217 steady-state delta chunks in 864/878 ms (main's reader: 1,590/1,580 ms). Replays with it:

  • Through main's agc_frame_replay, which restores deltas without AgcDriverPrepareCommandWrite, on that capture: 5/6 did not match the reference frames (two of them under memory pressure). With that 22 µs busy-wait (the two readers' per-chunk difference): 6/6 matched.
  • Through a build whose agc_frame_replay has the same code as fix(agc): stop frame capture deltas from collecting the previous delta's pages again #14137's 2b9f0754d6 (pushed today), which, like restoreGroup here, makes that call before each delta copy, on a capture its recorder made (fewer delta bytes): 5/6 matched (main's reader: 2/2); the sixth hit pacing gave up at call 3891527 (submit): serials 3/4, as main's tool sometimes does on the first capture. With only that call removed, one replay failed with [gpu] driver failure: null or misaligned index base (we stopped there), consistent with chitters2004's finding that coherent captures need it.

Your patch keeps that call, so this may not apply. But the call acts only when pending recorded GPU work writes the range, and we have not identified the hazard (e.g. a pending GPU store landing after the copy, or a copy while GPU work still reads the range). What would tell: did the failing runs restore faster than the old reader, and did their tool make that call before each delta copy? If faster, interleave plain runs with runs that busy-wait each chunk restore up to the old reader's cost until a plain run fails; if those fail too, it is unlikely to be this.

@chitters2004 For your port of embersas's reader, two additions from ours if useful: a zstd context per pooled stream (decoding those chunks: 769–813 ms, against 816–834 ms with a fresh context per chunk, in 6 bench runs) and a grow-only compressed buffer instead of an allocation per read. We can push a branch, or leave it if you'd rather port his as is.

Windows 11 26200, Ryzen 5 5600X, RX 6700 XT (gfx1031), AMD driver 32.0.21045.16010, WinLibs GCC 15.2.0 r7, Release (reader bench: -O2); shared GPU; replay arms ran in blocks. Not tested: embersas's patch, other GPUs, live play.

AI-assisted: yes (Claude Code)

thuanquang added a commit to thuanquang/AnyPS5 that referenced this pull request Nov 3, 2026
…refuse calls format 1

FrameRecorder::readGuest reads range and written-block deltas through
GuestMemory::ReadCommitted, which first lands the GPU writes the driver
still has pending over the range, so such a delta holds the bytes after
those writes. The replay restored a delta without landing them, so a
write still pending over the range in the replay landed after the
restore and replaced the delta's bytes. The replay now reads each delta
chunk, calls AgcDriverPrepareCommandWrite_nid_no_patch over its range
and then copies it, the order chitters2004's boykopovar#12567 uses in
restoreGroup.

A calls format 1 capture does not record whether GPU writes had landed
before each memory delta was read: main's recorder read its deltas
without landing them. calls.bin is now format 2 ("aps5-frame-calls 2").
FrameCaptureReader still reads format 1 and reports the format
(Version()), and agc_frame_replay refuses a format 1 capture before it
restores any memory, with a message that says to record it again.

Co-authored-by: chitters2004 <55252868+chitters2004@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@chitters2004

Copy link
Copy Markdown
Contributor Author

@thuanquang Yes please, push the branch. A zstd context per pooled stream and a grow-only compressed buffer are what I'd want in the port anyway, and your bench numbers make the case. I'll build the port on your branch with embersas's reader once this stack has merged, credit you both in it, and take your bench on the PPSA05110 capture as the reference to reproduce before claiming anything.

cc @embersas

AI-assisted: yes (Claude Code)

@thuanquang

Copy link
Copy Markdown
Contributor

@chitters2004 Pushed: thuanquang:replay/reader-stream-pool, one commit (c72645700a) on main f2d874c1fd. It touches only FrameCapture.hpp/.cpp, tests/FrameCapture.cpp and its cmake file: a pool of memory.bin streams, each keeping its own ZSTD_DCtx and a grow-only compressed buffer, with error text built only when a check fails. agc_frame_capture_reuse checks buffer growth both ways, four threads reading with an overlap check, and recovery after a failed read and after a failed decompression. A trial merge with #14137's head 702492e63e and with current main 1c8c0efc8c shows no conflict.

The capture behind the bench numbers comes from my own copy of the game, so it stays local; the bench program is ours, and I can push it as well if it helps you reproduce on your own capture.

AI-assisted: yes (Claude Code)

@embersas

embersas commented Nov 3, 2026

Copy link
Copy Markdown

Thanks @thuanquang. We checked our failing stack: AgcDriverPrepareCommandWrite is retained immediately before each delta copy.

We’ve now measured the original sequential reader on the same capture with unchanged graphics libraries, then run a calibrated comparison. Warm delta-restoration times were:

Mode Seconds per pass
Original sequential reader 17.65
Improved reader, normal 4.20
Improved reader, slowed 18.03

Per-group targets came from four warm original-reader measurements, totalling 17.565 seconds per pass. Every warm slowed group reached or exceeded its target. Both comparison modes used the same executable, --strict --no-synthesize --repeat 3, and independent identical starting caches.

We ran normal/slowed, slowed/normal, then normal/slowed. All six processes matched the three preserved final replay-output buffers exactly. An extra real restore excluded one calibration process and the final comparison pair from matched timing. No replacement runs. The table’s normal/slowed means use the two eligible pairs.

Neither historical failure recurred, so we can’t demonstrate suppression or rule out your timing hypothesis. This approximates original group costs rather than recreating the exact sequential read/copy cadence.

I’ve also prepared anyps5-restore-harness-v1.zip for review and reuse. It contains the measured archival patches, timing helper, portable Python runner, GPLv2 licence/attribution and synthetic tests. Recipients supply their own compatible capture and references. The portable runner passed synthetic checks; the measurements above used our original local launcher. The archival patches require porting to newer revisions.

Windows, Ryzen 3900X, RTX 3080 Ti, NVIDIA 610.88; archived integrated stack, not current main or #14137. No live-FPS claim.

AI-assisted: yes (Codex).

@chitters2004

Copy link
Copy Markdown
Contributor Author

@thuanquang Thanks. Yes please, push the bench program too: since the capture stays with you, I'll run your bench and the sequential reader on my own GTA V (PPSA04263) capture on this machine, and put both numbers in the port before claiming a gain. The port waits for this stack to merge, as planned.

@embersas Thanks for the calibrated run and the harness. I read it the same way you do: the slowed reader brings back the sequential reader's per-group cost and the failure still doesn't recur, so the reader's speed isn't shown to be what exposed it. I'll keep that open in the port's notes rather than claim the faster reader fixes it.

AI-assisted: yes (Claude Code)

…s its REWIND control words

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chitters2004

Copy link
Copy Markdown
Contributor Author

@thuanquang On the hazard you couldn't name: one candidate we haven't run, from reading the restore path on #14137's head ca4553365f.

AgcDriverPrepareCommandWrite (EntryPoints.cpp:248-251) flushes only pending GPU writes that overlap the chunk. Nothing waits for submitted GPU work that still reads it. So the memcpy at tools/FrameReplay.cpp:437 can overwrite bytes that an earlier draw hasn't read yet, such as an index or vertex buffer the title rewrites between frames. That would fit both results you saw: a 22 µs busy-wait per chunk hides it, and with the call removed you got null or misaligned index base.

To check, wait for submitted work to finish before each copy instead of only flushing. Plain AgcDriverWaitIdle can deadlock when a queue is parked on a label this group releases. A bounded Driver::WaitIdleFor behind a diagnostic export avoids that, and its timeouts tell you which groups had parked queues. If plain runs then match 6/6, it is a write-after-read hazard on the replay side, and the fix is to wait for the readers of the range, not a sleep. If they still fail, the cause is elsewhere, perhaps on the recorder side: deltas are read with ReadCommittedUnsynced after WaitIdleFor, so a CPU write-back that hasn't landed yet could be missed.

One ordering point on our side, in #12567 only: restoreGroup reads the REWIND control words from the target at FrameReplay.cpp:529 (from #12318) before our flush at :531. A pending GPU write to one of those words would be lost. #14137 has no such read, so this doesn't touch your results. 12ca5b811b moves our flush above that loop; the line still goes once #14137 lands. On it, frame_(replay|capture) with the validation layer passes 31 of 31 with 0 VUID- lines (RTX 3080, 621.01), as on 1ed9922bb5. Your merge 2bca7a2583 for #12568 has the old order (tools/FrameReplay.cpp:552 before :554), so it needs the same move.

@embersas The same check applies to your stack, which keeps the flush call before each copy.

The read-hazard check is not run here; we can't reproduce the failure on our captures. I'll answer your #12568 design question there separately.

AI-assisted: yes (Claude Code)

@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ NVIDIA GPU test: passed

PR head 12ca5b8 merged onto main 1c8c0ef · tested 2026-11-04 03:10 UTC · 28 min

GPU NVIDIA RTX 2000 Ada Generation, driver 595.99.02, Vulkan 1.4.329
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,811 passed · 6 failed (0 new, 3 also fail on main, 3 unstable on main) · 50 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,867 of 3,842 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (968 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 120.1 s
Unstable on main (3, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 4 of the last 10 main runs
agc_submit_progress: failed in 5 of the last 10 main runs
agc_submit_progress_no_timeline: failed in 4 of the last 10 main runs
Failing on main too (3, not counted against this PR)
agc_driver_image_table
guest_mmap_executable
guest_socket_options_multicast
Validation IDs already present on main (8 IDs in 545 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (545 tests): agc_driver, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, agc_driver_bda_span_reads, and 537 more
VUID-vkCmdCopyImage-srcImage-01548 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-vkCmdCopyImage-srcImage-01551 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (1 test): agc_driver_srgb8_color_target
VUID-VkSamplerCreateInfo-addressModeU-01079 (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeU-parameter (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeV-parameter (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeW-parameter (1 test): agc_driver_image_gather_volume
Skipped (50)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_gcn_display_present: skipped, no display: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
agc_driver_sample_order_grid_reference: skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in 0 of 3005 sloped samples, so no path can be compared with it bit for bit
agc_driver_scattered_runs: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_sparse_dma_buf_import: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_stencil_ref_export: skipped, the device lacks VK_EXT_shader_stencil_export
agc_driver_vertex_elected_atomic_max: skipped, the device runs vertex programs in subgroups, so they keep their cross-lane reductions
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip_in_order: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_dcc: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_deferred: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
vide
Reproduce
git fetch origin 12ca5b811b6e1f7cfb354421043b5dc8a1bf1b24 1c8c0efc8c41314402e6966da5bef6ea3d639d10
git checkout 1c8c0efc8c41314402e6966da5bef6ea3d639d10 && git merge --no-ff 12ca5b811b6e1f7cfb354421043b5dc8a1bf1b24
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 32 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 4 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ AMD GPU test: passed

PR head 12ca5b8 merged onto main 1c8c0ef · tested 2026-11-04 06:08 UTC · 34 min

GPU AMD Radeon RX 6300 (RADV NAVI24), driver Mesa 25.2.8-0ubuntu0.24.04.4, Vulkan 1.4.318
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-42-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,801 passed · 9 failed (0 new, 8 also fail on main, 1 unstable on main) · 57 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,867 of 3,842 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (1023 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 120.1 s, ⚠️ public_headers 55.0 s, ⚠️ agc_driver_lazy_resolved_abi_switch_set 34.9 s, ⚠️ check_skips 33.8 s
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 8 of the last 10 main runs
Failing on main too (8, not counted against this PR)
agc_driver_image_table
agc_driver_job_manager_compute_capacity
agc_driver_native_depth_buffer_clear
agc_driver_native_depth_clears
agc_submit_progress
agc_submit_progress_no_timeline
guest_mmap_executable
guest_socket_options_multicast
Validated but not compared (55, not in the main baseline's validation pass)
public_headers
public_headers_compile_0
public_headers_compile_10
public_headers_compile_12
public_headers_compile_14
public_headers_compile_16
public_headers_compile_18
public_headers_compile_2
public_headers_compile_20
public_headers_compile_21
public_headers_compile_22
public_headers_compile_23
public_headers_compile_24
public_headers_compile_25
public_headers_compile_26
public_headers_compile_27
public_headers_compile_28
public_headers_compile_29
public_headers_compile_30
public_headers_compile_31
public_headers_compile_32
public_headers_compile_33
public_headers_compile_34
public_headers_compile_35
public_headers_compile_36
public_headers_compile_37
public_headers_compile_38
public_headers_compile_39
public_headers_compile_4
public_headers_compile_40
public_headers_compile_41
public_headers_compile_42
public_headers_compile_43
public_headers_compile_44
public_headers_compile_45
public_headers_compile_46
public_headers_compile_47
public_headers_compile_48
public_headers_compile_49
public_headers_compile_50
public_headers_compile_51
public_headers_compile_52
public_headers_compile_53
public_headers_compile_54
public_headers_compile_55
public_headers_compile_56
public_headers_compile_57
public_headers_compile_58
public_headers_compile_59
public_headers_compile_6
public_headers_compile_60
public_headers_compile_61
public_headers_compile_62
public_headers_compile_63
public_headers_compile_8
Validation IDs already present on main (8 IDs in 532 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (532 tests): agc_driver, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, agc_driver_bda_span_reads, and 524 more
VUID-vkCmdCopyImage-srcImage-01548 (4 tests): agc_driver_depth_clear_hle, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-vkCmdCopyImage-srcImage-01551 (4 tests): agc_driver_depth_clear_hle, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (1 test): agc_driver_prepared_exports
VUID-VkSamplerCreateInfo-addressModeU-01079 (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeU-parameter (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeV-parameter (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeW-parameter (1 test): agc_driver_image_gather_volume
Skipped (57)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_copied_writer_label_order: skipped, the device reports a minStorageBufferOffsetAlignment of 4, so the dispatch binds its record at +0x1004 in place and nothing is written back; the test needs an alignment above 4
agc_driver_float16_unused_nan_payloads: skipped, the device has no native f16 conversions with RTE rounding and kept f16 denormals
agc_driver_gcn_display_present: skipped, no display: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
agc_driver_image_table_vouched_lookups: skipped, the device reports 4 of 4 host-imported pages written after a GPU read, so a watched imported table cannot be vouched for
agc_driver_linear_target_direct_write_completion: skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked
agc_driver_unit_shadow: skipped, the device does not import watched host memory for unit shadows
agc_driver_unit_shadow_budget: skipped, the device does not import watched host memory for unit shadows
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_idle_publish_wait: skipped, the device reports 4 of 4 host-imported pages written after a GPU read, so a forced watch of the import reads its submissions as CPU stores
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_shadowed_key_stores: skipped, the device imports no watched host memory under a unit shadow
agc_small_tiled_shadow_label: skipped, host imports are unavailable: the test block is not imported and watched
agc_storage_clear_hand_over: skipped, the device imports no watched host memory for the clear-key hand-over
agc_storage_pending_key_refresh: skipped, refreshes over a pending key store are not testable here
agc_unit_shadow: skipped, the device does not import watched host memory for unit shadows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not av
Reproduce
git fetch origin 12ca5b811b6e1f7cfb354421043b5dc8a1bf1b24 1c8c0efc8c41314402e6966da5bef6ea3d639d10
git checkout 1c8c0efc8c41314402e6966da5bef6ea3d639d10 && git merge --no-ff 12ca5b811b6e1f7cfb354421043b5dc8a1bf1b24
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 12 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 16 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's AMD machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@thuanquang

Copy link
Copy Markdown
Contributor

@chitters2004 Thanks. We ran a version of your check: with a bounded wait for the recorded GPU work before each delta group's restore, 6 of 6 replays matched the capture's 2 reference frames with the pre-copy flush, and 6 of 6 without it. Our only earlier replay without the flush (11-03, no wait) failed in the driver.

Why not WaitIdleFor. On #14137's head it waits only for completed >= accepted (Lifecycle.cpp:276), and a queue worker marks a submission completed once it has executed it on the CPU, not once the GPU has run its work. The replay's pacing already waits for those serials before each Memory call (FrameReplay.cpp:344-363; the recorder took each delta after the same wait), so it would not wait for GPU work still reading the range. We waited on the GPU timeline instead. That wait never waits for queues, so a queue parked on a label doesn't hold it up, and its timeouts say nothing about parked queues.

What we ran. The code from the second item of our 11-03 12:10Z comment: #14137's round 3 (the same code as 2b9f0754d6 apart from tests; the tool is unchanged on the head ca4553365f) with our pooled reader (the same code as c72645700a), plus a local diagnostic, not a proposed change: one AgcDriverWaitRecordedOnGpuFor_nid_postfix(10000) in the Memory call, before the copies of a delta group. That export (Driver::WaitRecordedOnGpuFor, thuanquang/AnyPS5@ace2179f0d) submits the open recorded batch and waits on the timeline until every batch submitted so far has run on the GPU, for up to 10 s (the bound doesn't cover taking the driver's GPU lock first). It runs no batch completions and no flush. A timeout would print a line, and the copy would go ahead. Two arms, alternating, 6 runs each: wait + flush (AgcDriverPrepareCommandWrite before each copy, as before) and wait only (that call removed). The same PPSA05110 capture as there (20 frames, recorded by that round-3 build), --strict:

Tool Runs Matched the 2 reference frames Wait timeouts Pacing give-ups Driver failures
r3 + reader + wait + flush 6 6/6 (1.000000, max channel diff 0) 0 0 0
r3 + reader + wait, no flush 6 6/6 (1.000000, max channel diff 0) 0 0 0
r3 + reader, flush (11-03, no wait) 6 5/6; the sixth stopped at its give-up – 1 (call 3891527) 0
r3 + reader, no flush (11-03, no wait) 1 – – – 1 (null or misaligned index base)

The 11-03 rows are our earlier runs of the same tool and reader without the wait (another build and day, not interleaved with these); we didn't repeat the no-flush failure, since such replays can leave the AMD driver hung until a reboot. Pacing give-ups count submit, flip and suspend calls; a Memory call's pacing timeout returns silently (FrameReplay.cpp:363) and isn't counted.

What this does and doesn't tell. The wait-only matches fit GPU work still running at the copy behind the 11-03 no-flush failure, but they don't place that work over the restored range: the wait covers every recorded batch, and it also delays each group's copy (a delay alone, the 22 µs busy-wait, hid a failure before). The wait lets reads of the range finish and also lands the stores those batches make straight into imported guest memory, so these runs don't separate your write-after-read case from such a store landing after the copy; a wait for only the batches that read the group's ranges would. It lands no deferred result (CPU write-backs made when a batch is reaped, storage-image results and shadows kept on the GPU until a flush): those land whenever a driver thread next reaps or flushes, so in the wait-only arm their order against the copy isn't fixed, while in the wait + flush arm the flush still lands them for chunks with a recorded write. 12 runs are too few to say whether the wait affects the pacing give-up, which main's tool also sometimes hits on the first capture.

Windows 11 26200, Ryzen 5 5600X, RX 6700 XT (gfx1031), AMD driver 32.0.21045.16010, WinLibs GCC 15.2.0 r7, Release; replays on a shared GPU (two of the 12 next to another GPU job); the lowest available physical memory in a run was 0.0-2.9 GB. Not tested: main's tool with our reader on the first capture (where 5 of 6 mismatched) with the wait, #14137's head ca4553365f itself (its main merge changes the driver under the tool), other GPUs, other captures, embersas's patch.

AI-assisted: yes (Claude Code)

@chitters2004

Copy link
Copy Markdown
Contributor Author

@thuanquang You're right about WaitIdleFor, and my suggestion was wrong there. Unlike WaitIdle (Lifecycle.cpp:257-268 on ca4553365f), it never calls device->WaitIdle(). It only waits for completed >= accepted (:276), so the wait ends once the CPU has run the work, not the GPU. Waiting on the GPU timeline was the right test. Thanks for running both arms.

This doesn't change #12567. It keeps the flush before each copy, and that arm matched 6 of 6. As you say, a wait for only the batches that read a group's ranges would tell the write-after-read case apart from a late store. I'll leave that with you.

AI-assisted: yes (Claude Code)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Nov 4, 2026
@thuanquang

Copy link
Copy Markdown
Contributor

@chitters2004 Pushed the bench program: thuanquang:replay/reader-bench, one commit (1fdc12d0b3) on c72645700a that adds tools/bench/frame-capture-reader/ with the two programs behind our numbers. Neither is part of the build or meant for merge.

  • ReaderBench.cpp times FrameCaptureReader::Read on a capture's steady-state delta chunks. It skips the delta groups of 512 MiB or more and reports their size as large_mib. Next to the reader times it prints two baselines: the same chunks decoded from memory, once with one reused ZSTD_DCtx and once with ZSTD_decompress, and the copy into a 64 MiB region, with and without MEM_WRITE_WATCH (Windows only). It prints one JSON line.
  • ReaderVerify.cpp decodes every delta and snapshot chunk independently with ZSTD_decompress and compares a hash of each with what the reader returns. It does this once serially, then from hardware_concurrency() threads that alternate the span and vector Read overloads.

The README has the build lines. For the comparison with main's reader we built ReaderBench.cpp twice with the same zstd, once against this branch's FrameCapture.cpp and once against main's, and ran the two binaries alternately on the same capture with a warm file cache. That is how the 864/878 ms against 1,590/1,580 ms numbers above were made.

Checked tonight: both programs build on this branch, and ReaderBench.cpp also builds against current main's reader (b11c7bf84f). On the same capture, ReaderVerify found 0 mismatches in 39,635 chunks, both serially and from 12 threads. One ReaderBench run each gave read_span_ms 853.8 with this branch's reader and 1,595.4 with main's. Windows 11 26200, Ryzen 5 5600X, WinLibs GCC 15.2.0 r7, -O2. Not tested: other compilers, Linux.

AI-assisted: yes (Claude Code)

@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ NVIDIA GPU test: passed

PR head d625d2b merged onto main 9ab6a28 · tested 2026-11-04 21:37 UTC · 40 min

GPU NVIDIA RTX 2000 Ada Generation, driver 595.99.02, Vulkan 1.4.329
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,876 passed · 6 failed (0 new, 3 also fail on main, 3 unstable on main) · 49 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,931 of 3,909 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (996 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 120.2 s
Unstable on main (3, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 3 of the last 10 main runs
agc_submit_progress: failed in 4 of the last 10 main runs
agc_submit_progress_no_timeline: failed in 3 of the last 10 main runs
Failing on main too (3, not counted against this PR)
agc_driver_image_table
guest_mmap_executable
guest_socket_options_multicast
Validation IDs already present on main (13 IDs in 557 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (557 tests): agc_driver, agc_driver_address_draw_space_reuse, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, and 549 more
VUID-VkShaderModuleCreateInfo-pCode-08737 (8 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_float16_fma_rounding, agc_driver_float16_unused_nan_payloads, agc_driver_packed_alu, agc_driver_packed_f16_denormal_modes, agc_driver_packed_float16_native
VUID-VkShaderModuleCreateInfo-pCode-08739 (8 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_float16_fma_rounding, agc_driver_float16_unused_nan_payloads, agc_driver_packed_alu, agc_driver_packed_f16_denormal_modes, agc_driver_packed_float16_native
VUID-VkShaderModuleCreateInfo-pCode-08741 (8 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_float16_fma_rounding, agc_driver_float16_unused_nan_payloads, agc_driver_packed_alu, agc_driver_packed_f16_denormal_modes, agc_driver_packed_float16_native
VUID-vkCmdCopyImage-srcImage-01548 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-vkCmdCopyImage-srcImage-01551 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (2 tests): agc_driver_prepared_exports, agc_driver_srgb8_color_target
VUID-VkPipelineShaderStageCreateInfo-module-parameter (1 test): agc_driver_float16_unused_nan_payloads
VUID-VkPipelineShaderStageCreateInfo-pSpecializationInfo-06849 (1 test): agc_driver_float16_unused_nan_payloads
VUID-VkSamplerCreateInfo-addressModeU-01079 (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeU-parameter (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeV-parameter (1 test): agc_driver_image_gather_volume
VUID-VkSamplerCreateInfo-addressModeW-parameter (1 test): agc_driver_image_gather_volume
Skipped (49)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_gcn_display_present: skipped, no display: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
agc_driver_sample_order_grid_reference: skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in 0 of 3005 sloped samples, so no path can be compared with it bit for bit
agc_driver_scattered_runs: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_sparse_dma_buf_import: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_stencil_ref_export: skipped, the device lacks VK_EXT_shader_stencil_export
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip_in_order: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_dcc: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_deferred: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_eop_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not av
Reproduce
git fetch origin d625d2bad6fe36f0dfc5a223943d81d86cba72e1 9ab6a281ea61c9e62a7eb07eb4cef8f19ede1c5c
git checkout 9ab6a281ea61c9e62a7eb07eb4cef8f19ede1c5c && git merge --no-ff d625d2bad6fe36f0dfc5a223943d81d86cba72e1
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 32 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 4 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@chitters2004

Copy link
Copy Markdown
Contributor Author

@thuanquang Thanks for the bench. We ran both programs on a GTA V capture (PPSA04263, 10 frames, 7.1 GB memory.bin), built as in your README against c72645700a's reader and current main's (b611e634f7), same zstd and flags.

  • ReaderVerify: 0 mismatches in 240,501 chunks (about 16 GiB decoded), serially and from 16 threads.
  • ReaderBench as written times very little on this capture: 40 chunks, 1.9 MiB, because 12,119.5 MiB of its deltas are in groups of 512 MiB or more. There, read_span_ms was 1.0 against main's 6.2 (5 alternating runs each).
  • So we added one optional argument to select those large groups instead (236,565 chunks), timing logic unchanged. Five alternating runs each, file cache warm, CPU under 8% before every run:
this branch main main / branch
read_span median (min–max) 10.5 s (10.3–14.7) 27.0 s (26.8–33.9) 2.6×
decode (no reader), median 9.3 s 9.5 s 1.0×

The decode baselines agree, so the runs are comparable. Your reader costs about 1.2 s over a bare decode of the same bytes here, and main's about 17.5 s. So the gain holds, and is larger, on the large groups a GTA replay is mostly made of.

RTX 3080 machine, Windows 11 26220, WinLibs GCC 15.2.0 r7, -O2. Not tested: Linux, other compilers.

AI-assisted: yes (Claude Code)

@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ AMD GPU test: passed

PR head d625d2b merged onto main b611e63 · tested 2026-11-05 02:20 UTC · 47 min

GPU AMD Radeon RX 6300 (RADV NAVI24), driver Mesa 25.2.8-0ubuntu0.24.04.4, Vulkan 1.4.318
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-42-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,894 passed · 10 failed (0 new, 9 also fail on main, 1 unstable on main) · 60 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 2,964 of 3,946 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (1003 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 121.2 s, ⚠️ agc_driver_stream_copy 47.6 s, ⚠️ public_headers 47.1 s, ⚠️ agc_driver_image_sample_unnormalized 46.8 s, ⚠️ agc_driver_sample_multisample_target 44.2 s
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 9 of the last 10 main runs
Failing on main too (9, not counted against this PR)
agc_driver_image_gather_volume
agc_driver_image_table
agc_driver_native_depth_bda_clear
agc_driver_native_multisample_depth
agc_driver_submit_progress
agc_submit_progress
agc_submit_progress_no_timeline
guest_mmap_executable
guest_socket_options_multicast
Passed on retry (1, failed in the parallel run, passed when re-run with a 360 s timeout; not counted against this PR)
agc_driver_recorder: Timeout in the parallel run; also passed only on retry in 25 other runs in the last 7 days
Validation IDs already present on main (4 IDs in 568 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (568 tests): agc_driver, agc_driver_address_draw_space_reuse, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, and 560 more
VUID-vkCmdCopyImage-srcImage-01548 (4 tests): agc_driver_depth_clear_hle, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-vkCmdCopyImage-srcImage-01551 (4 tests): agc_driver_depth_clear_hle, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (2 tests): agc_driver_prepared_exports, agc_driver_srgb8_color_target
Skipped (60)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_copied_writer_label_order: skipped, the device reports a minStorageBufferOffsetAlignment of 4, so the dispatch binds its record at +0x1004 in place and nothing is written back; the test needs an alignment above 4
agc_driver_float16_unused_nan_payloads: skipped, the device has no native f16 conversions with RTE rounding and kept f16 denormals
agc_driver_gcn_display_present: skipped, no display: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
agc_driver_image_table_vouched_lookups: skipped, the device reports 4 of 4 host-imported pages written after a GPU read, so a watched imported table cannot be vouched for
agc_driver_linear_target_direct_write_completion: skipped, the device does not keep host imports write-watched, so no completion stamp over the target is tracked
agc_driver_ngg_vertex_wave_reduction: skipped, s3 describes a 32-lane wave only on 32-lane subgroups, and the device has 64 lanes
agc_driver_scattered_runs: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_sparse_dma_buf_import: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_unit_shadow: skipped, the device does not import watched host memory for unit shadows
agc_driver_unit_shadow_budget: skipped, the device does not import watched host memory for unit shadows
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_idle_publish_wait: skipped, the device reports 4 of 4 host-imported pages written after a GPU read, so a forced watch of the import reads its submissions as CPU stores
agc_import_settle_initial_dcc: skipped, the device has no deferred settle of host imports: imports under 1 MiB wait for one only on Windows
agc_shadowed_key_stores: skipped, the device imports no watched host memory under a unit shadow
agc_small_tiled_shadow_label: skipped, host imports are unavailable: the test block is not imported and watched
agc_storage_clear_hand_over: skipped, the device imports no watched host memory for the clear-key hand-over
agc_storage_pending_key_refresh: skipped, refreshes over a pending key store are not testable here
agc_unit_shadow: skipped, the device does not import watched host memory for unit shadows
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platf
Reproduce
git fetch origin d625d2bad6fe36f0dfc5a223943d81d86cba72e1 b611e634f7fc024f0c13e342d96de744448ef1d9
git checkout b611e634f7fc024f0c13e342d96de744448ef1d9 && git merge --no-ff d625d2bad6fe36f0dfc5a223943d81d86cba72e1
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 12 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 16 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's AMD machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Nov 5, 2026
…ed-deltas

# Conflicts:
#	core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp
#	core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_frame_replay_tests.cmake
#	docs/dev/BUILD.md
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Nov 6, 2026
@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ NVIDIA GPU test: passed

PR head 30d7714 merged onto main f8167f3 · tested 2026-11-06 05:21 UTC · 35 min

GPU NVIDIA RTX 2000 Ada Generation, driver 595.99.02, Vulkan 1.4.329
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 2,998 passed · 4 failed (0 new, 3 also fail on main, 1 unstable on main) · 56 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 3,058 of 4,158 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (996 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 120.1 s
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 1 of the last 10 main runs
Failing on main too (3, not counted against this PR)
agc_driver_image_table
guest_mmap_executable
guest_socket_options_multicast
Validation IDs already present on main (9 IDs in 598 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (598 tests): agc_driver, agc_driver_address_draw_space_reuse, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, agc_driver_bda_anchor_reads, and 590 more
VUID-VkShaderModuleCreateInfo-pCode-08737 (14 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_f16_fma_denormal_modes, agc_driver_f16_fma_invalid_nan, agc_driver_float16_fma_rounding, agc_driver_float16_modifiers, agc_driver_float16_native, and 6 more
VUID-VkShaderModuleCreateInfo-pCode-08739 (14 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_f16_fma_denormal_modes, agc_driver_f16_fma_invalid_nan, agc_driver_float16_fma_rounding, agc_driver_float16_modifiers, agc_driver_float16_native, and 6 more
VUID-VkShaderModuleCreateInfo-pCode-08741 (14 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_f16_fma_denormal_modes, agc_driver_f16_fma_invalid_nan, agc_driver_float16_fma_rounding, agc_driver_float16_modifiers, agc_driver_float16_native, and 6 more
VUID-vkCmdCopyImage-srcImage-01548 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-vkCmdCopyImage-srcImage-01551 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (2 tests): agc_driver_prepared_exports, agc_driver_srgb8_color_target
VUID-VkPipelineShaderStageCreateInfo-module-parameter (1 test): agc_driver_float16_unused_nan_payloads
VUID-VkPipelineShaderStageCreateInfo-pSpecializationInfo-06849 (1 test): agc_driver_float16_unused_nan_payloads
Skipped (56)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_gcn_display_present: skipped, no display: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
agc_driver_sample_order_grid_reference: skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in 0 of 3005 sloped samples, so no path can be compared with it bit for bit
agc_driver_sparse_dma_buf_import: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_stencil_ref_export: skipped, the device lacks VK_EXT_shader_stencil_export
agc_driver_vertex_elected_atomic_max: skipped, the device runs vertex programs in subgroups, so they keep their cross-lane reductions
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_net_ctl_disconnected: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_device_capability: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_disabled: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_four_k: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_height_only: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_limit: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_malformed: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_width_only: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan de
Reproduce
git fetch origin 30d77143305b5e868c6bac7683ec2dec3801edcf f8167f32cd3795a776b955aa10e8284d4de67aef
git checkout f8167f32cd3795a776b955aa10e8284d4de67aef && git merge --no-ff 30d77143305b5e868c6bac7683ec2dec3801edcf
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 32 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 4 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

…ed-deltas

# Conflicts:
#	core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp
#	core/libs/prx/libSceAgcDriver/tests/cmake/agc_driver_frame_replay_tests.cmake
#	core/libs/prx/libSceAgcDriver/tools/FrameReplay.cpp
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Nov 6, 2026
@TechLevelZero

Copy link
Copy Markdown
Contributor

✅ NVIDIA GPU test: passed

PR head c4afb1e merged onto main 8fa2f2f · tested 2026-11-06 18:31 UTC · 35 min

GPU NVIDIA RTX 2000 Ada Generation, driver 595.99.02, Vulkan 1.4.329
Host Ubuntu 26.04.1 LTS, kernel 7.0.0-38-generic, container ubuntu:24.04
Logs build and test logs for this run
Build ✅ passed (Release, -DANYPS5_ENABLE_SPIRV_TOOLS=ON)
Tests ✅ 3,033 passed · 4 failed (0 new, 3 also fail on main, 1 unstable on main) · 55 skipped (ANYPS5_REQUIRE_VULKAN=1)
Test setup CPU-only tests without a GPU; GPU tests (and any CPU-only test that failed or skipped without one) once on the GPU with VK_LAYER_KHRONOS_validation and ANYPS5_NO_SHADER_CACHE=1; main ran the same way
Test selection 3,092 of 4,235 tests run: the ones this change can reach, chosen by tools/select_tests.py from main's build graph
Vulkan validation ✅ no new validation IDs vs main (1006 tests run on the GPU)
Slow tests (>30 s, GPU tests timed with validation layers) ⚠️ agc_driver_pipeline_journal 120.1 s, ⚠️ public_headers 31.3 s
Unstable on main (1, these also failed in recent main runs on this machine; not counted against this PR)
agc_driver_pipeline_journal: failed in 1 of the last 10 main runs
Failing on main too (3, not counted against this PR)
agc_driver_image_table
guest_mmap_executable
guest_socket_options_multicast
Validation IDs already present on main (9 IDs in 602 tests)
VUID-VkDeviceCreateInfo-pNext-pNext (602 tests): agc_driver, agc_driver_address_draw_space_reuse, agc_driver_address_draw_space_reuse_verify, agc_driver_address_passes, agc_driver_alias_copy_label, agc_driver_alpha_to_coverage, agc_driver_array_layer_target, agc_driver_bc_block_copy, and 594 more
VUID-VkShaderModuleCreateInfo-pCode-08737 (14 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_f16_fma_denormal_modes, agc_driver_f16_fma_invalid_nan, agc_driver_float16_fma_rounding, agc_driver_float16_modifiers, agc_driver_float16_native, and 6 more
VUID-VkShaderModuleCreateInfo-pCode-08739 (14 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_f16_fma_denormal_modes, agc_driver_f16_fma_invalid_nan, agc_driver_float16_fma_rounding, agc_driver_float16_modifiers, agc_driver_float16_native, and 6 more
VUID-VkShaderModuleCreateInfo-pCode-08741 (14 tests): agc_driver_dpp16_operands, agc_driver_dpp8_pk_fmac, agc_driver_dx10_clamp, agc_driver_f16_fma_denormal_modes, agc_driver_f16_fma_invalid_nan, agc_driver_float16_fma_rounding, agc_driver_float16_modifiers, agc_driver_float16_native, and 6 more
VUID-vkCmdCopyImage-srcImage-01548 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-vkCmdCopyImage-srcImage-01551 (7 tests): agc_driver_depth_clear_hle, agc_driver_depth_plane_buffer_memory, agc_driver_depth_plane_partial_store, agc_driver_depth_plane_sampling, agc_driver_depth_storage_dispatch, agc_driver_depth_surface_from_storage, agc_driver_dispatch_move_depth
VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 (1 test): agc_driver_prepared_exports
VUID-VkPipelineShaderStageCreateInfo-module-parameter (1 test): agc_driver_float16_unused_nan_payloads
VUID-VkPipelineShaderStageCreateInfo-pSpecializationInfo-06849 (1 test): agc_driver_float16_unused_nan_payloads
Skipped (55)
agc_driver_bda_reads_shadowed_results: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_bda_reads_shadowed_results_per_build: skipped, the device reports watched host memory written when it imports or reads it
agc_driver_gcn_display_present: skipped, no display: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
agc_driver_sample_order_grid_reference: skipped, the device does not keep depth drawn at custom sample locations at those locations, even after a transition that names them: the composite of uniform renders holds the guest's depth in 0 of 3005 sloped samples, so no path can be compared with it bit for bit
agc_driver_scattered_runs: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_driver_vertex_elected_atomic_max: skipped, the device runs vertex programs in subgroups, so they keep their cross-lane reductions
agc_host_import_sparse: skipped, host imports are unavailable: /dev/udmabuf is not accessible
agc_unit_shadow_completion_store: skipped, the device reports watched host memory written when it imports or reads it
guest_http_uri_locale: 
guest_net_ctl: skipped, the host has no IPv4 address
guest_net_ctl_disconnected: skipped, the host has no IPv4 address
guest_video_out_color_space: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_device_capability: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_hdr: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_high_frame_rate: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_disabled: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_four_k: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_height_only: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_limit: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_malformed: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_host_resolution_width_only: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_output_mode: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_resolution_status: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
guest_video_out_wqhd_detection: skipped, no display or Vulkan device: SDL_InitSubSystem(VIDEO | GAMECONTROLLER) failed: wayland,x11 not available
video_out_flip_aftergpu: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_backtoback: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_controls: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in current SDL video driver (offscreen) or platform
video_out_flip_cpuflip: skipped, no display or Vulkan device: DisplayWindow: Vulkan support is either not configured in SDL or not available in curre
Reproduce
git fetch origin c4afb1ead6de8a54936cd2c52f0ac9c10cd0bd60 8fa2f2f8881b537b24062bd377badebae8d97abf
git checkout 8fa2f2f8881b537b24062bd377badebae8d97abf && git merge --no-ff c4afb1ead6de8a54936cd2c52f0ac9c10cd0bd60
git submodule update --init --recursive
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON -DANYPS5_ENABLE_SPIRV_TOOLS=ON
cmake --build build && cmake --build build --target libs
VK_DRIVER_FILES=/nonexistent/icd.json ctest --test-dir build -E '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 32 --output-on-failure
ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation \
  ctest --test-dir build -R '^(agc|shader|recompiler|videoout|video_out|guest_video_out|vulkan)|gpu' --timeout 120 --parallel 4 --output-on-failure
# failures are then re-run alone: --timeout 360 --parallel 2; new validation IDs: the test alone, twice, --parallel 1

Automated build and test on a contributor's NVIDIA machine; it does not replace upstream CI. A new comment is posted for each new push. Full logs.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants