Skip to content

feat(agc): capture and replay frames on Linux - #11157

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
janzezno:feat/agc-frame-capture-linux
Oct 27, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
janzezno:feat/agc-frame-capture-linux

Conversation

@janzezno

@janzezno janzezno commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

What

APS5_FRAME_CAPTURE and agc_frame_replay only work on Windows. On Linux a capture throws at its first frame boundary with frame capture: guest memory capture is implemented on Windows only, so a title that misrenders or hangs on Linux can't be captured and replayed offline. Everything else the recorder uses already runs on Linux (the write watch, ReadCommitted, the guest arena and the allocation registry). The missing parts were the VirtualQuery walk in FrameRecorder::committedRanges and the VirtualAlloc calls in the replay tool. This change adds Linux versions of those and runs the capture tests on Linux.

Recorder (committedRanges, Linux branch). It walks each readable registry allocation's VMAs with a new HostMapping::Walk (PROCMAP_QUERY when the kernel has it, else one /proc/self/maps pass). HostMapping::Mapping gains the r/w/x/shared bits and the inode, filled on both paths. Classification follows the Windows branch:

  • An allocation outside the arena is Host.
  • A PROT_NONE VMA is the Linux form of reserved and uncommitted memory (the arena reserves and resets with PROT_NONE), so it is left out.
  • Inside the arena, a range the write watch covers is Watched, other private anonymous memory is Unwatched, and the rest (shared and file-backed) is Section. One difference from Windows: on Linux libkernel registers direct memory views with the write watch (AddMapping → WatchMapping). Those views are watched, so CPU stores to direct memory during the frames are recorded, which Windows section views can't do.
  • protection stores the Windows PAGE_* value of the VMA bits. That is the protection vocabulary the arena API already uses on Linux, so the field means the same thing in captures from either host.
  • A file mapping can extend past the end of its file, and reading those pages raises SIGBUS. ccleste's bundled Mesa maps its radv-shader-cache/.../index file that way, and the first Linux capture died in ReadCommitted. /proc/self/map_files needs CAP_SYS_ADMIN, so the file size can't be read from there. Instead the recorder probes pages with process_vm_readv on its own process (a page past the end gives EFAULT, not a signal), binary-searches the boundary and records the pages past it as an unreadable range. Any other probe error (for example EPERM under a seccomp or ptrace policy) fails the capture instead of dropping memory.
  • A capture now throws when it starts if the guest write watch is unavailable (APS5_NO_WRITE_WATCH, or a host without one), on both platforms. Without the watch a capture used to start and silently miss every CPU store during the frames.

Replay tool.

  • committedWritable: Linux version through HostMapping::Walk.
  • mapHostRanges: maps page-aligned envelopes with MAP_FIXED_NOREPLACE. Envelopes merge only where two ranges share a page, so an occupied CPU range doesn't block a free GPU range next to it. Windows merges touching 64 KiB envelopes because of its allocation granularity; Linux doesn't need to.
  • Arena ranges: a registry allocation need not start on a page (ccleste has a 32-byte one at 0x200538020). On Linux the arena commit is a MAP_FIXED mmap, which failed with EINVAL. An unaligned range now has its pages made writable with GuestArenaSetProtection; the exact bytes are still marked used. Windows keeps GuestArenaCommit.
  • Restore now decides which ranges the replaying process already maps writable before it commits any range. Before, a second range in a page the first range had just committed counted as the process's own memory, and was skipped (CPU) or zeroed (GPU). The new agc_driver_frame_replay_shared_page test fails with each half of this reverted: guest arena commit failed: Invalid argument, and a range sharing a page with an earlier one was skipped.

Tests. agc_driver_frame_capture_eop and the zero, shader and shader_host replay-state modes are registered on Linux, plus new modes: unwatched (Windows and Linux; a capture with APS5_NO_WRITE_WATCH=1 must throw), and the Linux-only shared_page and host_neighbors (an occupied CPU host range next to a free GPU host range must skip only the CPU range). Each new mode fails with its fix reverted: a capture started without the write watch, frame replay: cannot map the GPU-visible host range at .... On Linux the write watch covers only ranges someone registers (libkernel does for title memory), while Windows watches arena commits automatically. So the two tests that commit arena memory directly register it with GuestWriteWatchRegister on Linux. tests/FrameReplay.cpp wrapped its std::system commands (the capture-and-replay case and the concurrent case #11197 added) in an extra pair of quotes for cmd.exe, which sh reads as one word; that pair is now added on Windows only.

#10711. #11197 fixed the recorder race and lifted the Windows quarantine, so agc_driver_frame_replay and agc_driver_frame_replay_concurrent are now registered on Linux next to Windows, with no quarantine option. Before #11197, the Linux build of agc_driver_frame_replay failed 24 of 50 runs pinned with taskset -c 0,1 (the replay did not reproduce the second copy); on this rebase both tests pass 50 of 50 pinned to two cores, to one core and unpinned.

Debt and BUILD.md updated: Linux support, watched direct memory views, the no-access and past-end-of-file cases. macOS still throws.

Overlap: pr_overlap reports #10760, #11435 and #11436 (below). Of the 82 others it could not compare, most share only the libSceAgcDriver CMakeLists.txt or debt file. One changes the same code: #7201 (macOS port) puts HostMapping::Find under #if defined(__APPLE__) and keeps the Linux return Mapping{...} line in its #else, which this change turns into return FromQuery(query, name). That is a one-line conflict for whichever merges second, and its macOS Mapping{begin, end, name} still compiles with the new defaulted fields. #10540 adds an extern char** environ; next to the <unistd.h> include of FrameRecorder.cpp, a line away from the include added here.

The reported overlaps, and the recorder fix this change waited for:

Tested

Arch Linux 7.2.11, AMD Radeon RX 7900 GRE (RADV NAVI31, Mesa 26.2.5), Ryzen 5 7500F, Release, on main 7903201bb4.

Focused tests with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, ctest -R 'agc_frame_capture|agc_driver_frame_(capture_eop|replay)' -V:

  • main: 4/4 pass (agc_frame_capture and the replay modes order, tokens and blocking; the others are not registered on Linux), 0 VUID-, no missing-layer line.
  • this PR: 13/13 pass (agc_frame_capture, agc_driver_frame_capture_eop, replay modes order, tokens, blocking, zero, shader, shader_host, unwatched, shared_page, host_neighbors, agc_driver_frame_replay, agc_driver_frame_replay_concurrent), 0 VUID-, no missing-layer line. The replay tests copy with DMA and don't depend on the Vulkan device.

agc_driver_frame_replay and agc_driver_frame_replay_concurrent, 50 runs each under taskset -c 0,1, taskset -c 0 and unpinned: 0 failures in 300.

ctest -R 'guest|sysmodule|module|unwind|frame' -j6 (819 tests, covering the HostMapping users GuestMemory, sceKernelGetModuleInfoForUnwind and sceSysmoduleGetModuleInfoForUnwind): all pass, 2 skipped (video_out_frame_dump_queue, video_out_frame_dump_wait).

On the pre-rebase head 9aad99ba3c (main ae7d59258e; the rebase leaves the recorder and replay tool code unchanged):

ccleste PPSA59322 1.00 (homebrew, MIT, sha256 96861b81…), relinked with default options, title screen, no input, APS5_FRAME_CAPTURE_AT_FLIP=300 APS5_FRAME_CAPTURE_FRAMES=10:

  • main: aborts at frame boundary 300 with terminate called after throwing an instance of 'std::runtime_error' / what(): frame capture: guest memory capture is implemented on Windows only.
  • this PR: [frame-capture] started at frame boundary 300: 111 ranges, 163.0 MiB snapshot in 0.2 s / 10 frames, 32 calls, 33 memory deltas (6.6 MiB), 264.5 MiB raw, 28.0 MiB stored, unreadable_bytes 24576 (the shader cache index pages past the end of its file), no warnings. agc_frame_replay capture --frames out:
{"frames":10,"replay_s":0.133,"fps":75.412,"frame_ms":{"p50":2.782,"p90":7.751,"max":7.751,"each":[0,7,7,0,2,3,2,2,7]},"submits":11,"delta_mib":6.6,"pace_timeouts":0,"pace_wait_s":0.333,"stale_registrations":0,"failed_registrations":0,"skipped_mib":0.0,"threads":3,"released_waits":0,"of_them_from_earlier_writes":0,"of_them_synthesized":0,"unfinished":0,"frames_compared":3,"pixels_identical":1.000000,"max_channel_diff":0,"load_s":0.032}

All three display buffers match their reference images pixel for pixel; the replayed title screen is attached.

Not tested: Windows. The Windows recorder walk and the Windows commit path are unchanged; the write-watch check at capture start and the unwatched test also run there. The ownership decision before commits also applies on Windows and changes behaviour only for two ranges in one page. tests/FrameReplay.cpp keeps its outer quotes on Windows.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: AMD Radeon RX 7900 GRE, RADV NAVI31, Mesa 26.2.5, Arch Linux 7.2.11
  • AI-assisted: yes

@janzezno

Copy link
Copy Markdown
Contributor Author
ccleste-replay-11157

@github-actions

github-actions Bot commented Oct 25, 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 10760 docs/dev/debt/libSceAgcDriver.md L79-80
PR 11435 core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp L187-192
PR 11436 core/libs/prx/libSceAgcDriver/tests/FrameReplay.cpp L187-192

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 674 docs/dev/BUILD.md
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1835 docs/dev/BUILD.md
PR 1896 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 2434 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 4448 docs/dev/debt/libSceAgcDriver.md
PR 4449 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/BUILD.md
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5372 docs/dev/debt/libSceAgcDriver.md
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6187 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6194 docs/dev/debt/libSceAgcDriver.md
PR 6209 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6273 docs/dev/debt/libSceAgcDriver.md
PR 6280 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6300 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6412 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6481 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6548 docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6743 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6804 docs/dev/debt/libSceAgcDriver.md
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6926 docs/dev/debt/libSceAgcDriver.md
PR 6959 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6977 docs/dev/debt/libSceAgcDriver.md
PR 7048 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7256 docs/dev/debt/libSceAgcDriver.md
PR 7275 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7457 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7514 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7604 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 7642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 8223 docs/dev/debt/libSceAgcDriver.md
PR 8319 docs/dev/debt/libSceAgcDriver.md
PR 8395 docs/dev/debt/libSceAgcDriver.md
PR 8484 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8542 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 8592 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 8939 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9020 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 9248 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9335 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 9356 docs/dev/debt/libSceAgcDriver.md
PR 9387 docs/dev/debt/libSceAgcDriver.md
PR 9431 docs/dev/debt/libSceAgcDriver.md
PR 9432 docs/dev/debt/libSceAgcDriver.md
PR 9457 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9508 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 9533 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 9592 docs/dev/debt/libSceAgcDriver.md
PR 9633 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9672 docs/dev/debt/libSceAgcDriver.md
PR 9673 docs/dev/debt/libSceAgcDriver.md
PR 9674 docs/dev/debt/libSceAgcDriver.md
PR 9675 docs/dev/debt/libSceAgcDriver.md
PR 9676 docs/dev/debt/libSceAgcDriver.md
PR 9717 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 9742 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 9748 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 9749 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9790 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9842 docs/dev/debt/libSceAgcDriver.md
PR 9852 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9891 docs/dev/debt/libSceAgcDriver.md
PR 9892 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 9893 docs/dev/debt/libSceAgcDriver.md
PR 9895 docs/dev/debt/libSceAgcDriver.md
PR 9897 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9950 docs/dev/debt/libSceAgcDriver.md
PR 9969 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10000 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10043 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10049 docs/dev/debt/libSceAgcDriver.md
PR 10064 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 10193 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 10233 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 10333 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 10348 docs/dev/debt/libSceAgcDriver.md
PR 10359 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 10435 docs/dev/debt/libSceAgcDriver.md
PR 10442 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10489 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10757 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10846 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10867 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 11351 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/BUILD.md
PR 11366 docs/dev/BUILD.md
PR 11557 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md

@MarcZX10

Copy link
Copy Markdown
Contributor

I checked the overlap bot warning against the current heads of #11157 and #10760. A three-way merge is clean in the production/source files but conflicts in docs/dev/debt/libSceAgcDriver.md: both branches insert a Functional debt entry at the same point. The entries describe separate behavior and do not appear to imply a code dependency. Please coordinate which documentation edit lands first, then resolve the remaining entry on the other branch; add Depends on: #N only if you require that order.

@janzezno

Copy link
Copy Markdown
Contributor Author

@MarcZX10 thanks for checking. I proposed the order on #10760: either order, whichever merges second keeps both debt entries, no Depends on. I'll update the Overlap line once lgqdaxia answers.

There is a second overlap that pr_overlap can't see yet: the recorder fix for #10711 (Sumaiter, not opened yet) lifts the agc_driver_frame_replay quarantine and removes its debt entry, and this PR extends both to Linux. I've asked about the order on #10711.

lgqdaxia added a commit to lgqdaxia/AnyPS5 that referenced this pull request Oct 25, 2026
Remove the extra blank line between capture trace output and frame capture. The independent changes in boykopovar#11157 may merge in either order; the second branch keeps both entries when rebasing.

AI-assisted: Codex
@ponhei9502

Copy link
Copy Markdown

Thanks for this. We have been running Demon's Souls captures through agc_frame_replay on Linux (RTX 3080, NVIDIA) as a benchmark
for a couple of days, and ran into a few problems on the way. These fixes may be useful here. Happy to rebase them onto your branch or send them as a follow-up PR after yours lands, whichever you prefer.

  1. Early wait release corrupts the picture. The replay releases a GPU wait on its own after 3 ms without a completion
    (--release-after-ms, default 3, still the default on main). On our capture that released 54 waits, broke cross-queue ordering
    (async-compute writes were consumed too late), and the replayed frames matched the recorded ones at 0.000. With the release off,
    four runs matched at 0.943-0.953. The fix makes the release opt-in:
    ponhei9502@f070d4c
    (this one applies to your PR as is: it only touches tools/FrameReplay.cpp).

  2. Section (memfd) memory on Linux. Guest direct memory on Linux is a memfd, so the same pages can sit at several addresses. Two
    commits: the recorder copies a written block to every alias view, and the replay maps recorded Section ranges back as direct
    memory, so the driver imports them as it does in the game (one replay maps 3269 such ranges, 8932 MiB). Your PR already parses
    /proc/self/maps with inode and a Section kind, so these may overlap; worth a look:
    ponhei9502@ad9e917
    ponhei9502@0409274

  3. Timeouts and stall reports. --pace-timeout-ms / --drain-timeout-ms (a cold pipeline journal made one submit take about 90 s
    to retire, past the fixed timeout), and the replay names the awaited waits when it stalls (address, compare function, reference,
    current value). The stall report is how we found one wait that sometimes never completes in our capture:
    ponhei9502@d89a226
    ponhei9502@5aed01f

  4. Capture checks. --check-submits counts zero-filled or malformed PM4 submissions in the JSON line, and --dry-check runs the same
    check over the capture without a device:
    ponhei9502@32b4a54
    ponhei9502@976b482

  5. Bisect helpers. --cut-call / --cut-command stop the replay at a given call or command so a wrong pixel can be traced to the
    draw that wrote it, plus per-submit target stats:
    ponhei9502@493033d
    ponhei9502@80a9f1c

  6. Timing stability. --settle-s waits for the background pipeline-journal replay before the timed window. With it and the
    release off, three-run medians of replay_s repeat within about 8.7 % on our box (pooled over 18 runs), and frame matches stay
    at 0.915-0.958:
    ponhei9502@243119f

None of these change the driver's behaviour outside the replay tool, except the recorder's alias mirroring (2) and some
diagnostics that stay off unless their environment switch is set (APS5_PROBE_DISPATCH, APS5_PROBE_TARGETS,
APS5_REPLAY_PROBE_FILE, APS5_DRY_TRACE).

lgqdaxia added a commit to lgqdaxia/AnyPS5 that referenced this pull request Oct 25, 2026
Remove the extra blank line between capture trace output and frame capture. The independent changes in boykopovar#11157 may merge in either order; the second branch keeps both entries when rebasing.

AI-assisted: Codex
@janzezno
janzezno force-pushed the feat/agc-frame-capture-linux branch from ae44408 to 9aad99b Compare October 25, 2026 23:32
@janzezno

Copy link
Copy Markdown
Contributor Author

@ponhei9502 thanks, and good to hear the replay works on a title that size. I'd like to keep #11157 to the Linux port, so I'd prefer these as follow-up PRs after it lands rather than rebased in. A few notes from going through the commits:

  • They're built on your fork's own Linux port (06605f9, d5f186c, a4422d6, 8997376), not on this branch, so none of them apply here as they are, not even f070d4c: Options already has your checkSubmits/dryCheck fields on your side.
  • 8997376 isn't needed here. feat(agc): capture and replay frames on Linux #11157 checks GuestMemory::Watched before it looks at the memfd, so direct memory that libkernel registered is recorded as Watched and gets deltas.
  • Aliases (ad9e917): WatchMapping in libkernel unregisters the write watch on every view once two mappings share physical pages. So neither alias is watched, and mirroring a delta to the other views never fires, because no view produces one. 0409274 also gives each Section range its own DirectMemoryAlloc, so the replay imports instead of copying, but thealiasing still isn't restored. On Linux a store to aliased direct memory isn't recorded at all, and the debt entry here says less than that, so I'll correct it in this PR. A real fix would record the backing (inode and offset) per range and map the aliases back onto one memfd in the replay. That fits better as its own PR.
  • The early-release default (f070d4c) changes the tool on Windows too, and the release exists for stores from threads that never call the driver (the Frame replay debt entry). A separate PR comparing captures with and without it, including the agc_driver_frame_replay* tests, would make that easier to review than one title's numbers.
  • 3–6 are separate tool options and diagnostics; each one is easiest to review as its own PR. Per the conventions they'd need the code comments and B71d tags removed.

@Dextroid17

Copy link
Copy Markdown
Contributor

@janzezno a Linux NVIDIA and lavapipe data point from a large 3D title. I tested ae44408; the force-push to 9aad99b changes only one debt line, so the code is the same.

Setup: RTX 3080 Ti with NVIDIA 615.78.08, and lavapipe (Mesa 26.2.4, LLVM 23.1.1). Linux 7.3 (CachyOS), i9-11900K, 32 GB.

Focused tests (ctest -R 'agc_frame_capture|agc_driver_frame_(capture_eop|replay)'): 11 of 11 passed. These ran without a GPU here, so they don't test either driver. I ran them without ANYPS5_REQUIRE_VULKAN and without the validation layer, which isn't installed here.

Ghost of Yōtei (PPSA26344, 01.008.000). The title needs local commits to run here, so I cherry-picked this PR's commit onto that build. Only libSceAgcDriver.prx changed. I wrote APS5_FRAME_CAPTURE_TRIGGER about 40 s after the first present, with APS5_FRAME_CAPTURE_FRAMES=10:

[frame-capture] started at frame boundary 385: 55 ranges, 581.8 MiB snapshot in 1.0 s
[frame-capture] 10 frames, 810 calls, 61 memory deltas (416.0 MiB), 1061.1 MiB raw, 100.4 MiB stored

unreadable_bytes 0, late_ranges 0, no warnings, 0 Xid errors. The captured frames are the PlayStation Studios logo in the intro, so this is a light scene, not gameplay.

Replay (agc_frame_replay capture --frames out), in run order:

device wait release replay_s p50 frame ms released_waits (synthesized) pixels_identical max_channel_diff
RTX 3080 Ti default (3 ms) 74.9 557.8 83 (44) 0.999742 99
lavapipe default (3 ms) 287.5 1224.3 93 (52) 0.238763 4
RTX 3080 Ti --no-release 121.3 558.3 0 1.000000 0
RTX 3080 Ti default (3 ms) 51.4 541.1 83 (45) 0.992742 3

Every run replayed 10 frames with unfinished 0 and compared 2 frames.

  • With --no-release, the RTX 3080 Ti replay matched the recorded frames exactly. The two default runs gave 0.9997 and 0.9927. That points the same way as ponhei9502's item 1. The second default run's largest channel difference was only 3. It's one capture and three NVIDIA runs, so treat it as input for the separate PR you suggested, not as a result.
  • Every run, on both devices, logged one pacing gave up at call 30566 (submit): serials N/8, the capture had 0 in flight (N = 5, or 3 with --no-release) and waited 24–30 s in pacing. It's the same call on both devices.
  • lavapipe's 0.239 comes from rounding: no channel is off by more than 4. ImageMagick compare -fuzz 2% counts 0 differing pixels in both buffers (0–3 on the RTX 3080 Ti).

AI-assisted: yes

@Dextroid17

Copy link
Copy Markdown
Contributor

@janzezno a follow-up on the --no-release point in my comment above. With more runs, it doesn't hold up.

I replayed the same capture 8 more times on the RTX 3080 Ti, alternating default and --no-release (4 each):

  • Default: 4 of 4 matched the recorded frames exactly (pixels_identical 1.000000).
  • --no-release: 3 of 4 matched exactly. One gave 0.996684 over the two compared frames: the first matched exactly, and the second had max_channel_diff 198.

Together with the first three runs, that's 4 of 6 exact with the default and 4 of 5 with --no-release. So mismatches happen in both modes. With counts this small I can't tell the modes apart, so this data point gives no sign that the early release causes them. Please disregard that bullet as input for the separate PR. This is about my data point only. It says nothing about ponhei9502's Demon's Souls capture, which is a different scene and a much larger effect.

A lavapipe run with --no-release gave byte-identical frames to the default run (0.238763, max diff 4), so the rounding there didn't change with the release (one run each). All 9 new runs logged the same one pacing timeout at call 30566, as the first 4 did.

AI-assisted: yes

@oneandonlydean oneandonlydean self-assigned this Oct 26, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 26, 2026
lgqdaxia added a commit to lgqdaxia/AnyPS5 that referenced this pull request Oct 26, 2026
Remove the extra blank line between capture trace output and frame capture. The independent changes in boykopovar#11157 may merge in either order; the second branch keeps both entries when rebasing.

AI-assisted: Codex
@janzezno
janzezno force-pushed the feat/agc-frame-capture-linux branch from 9aad99b to c867473 Compare October 26, 2026 15:16
@janzezno

Copy link
Copy Markdown
Contributor Author

Rebased onto main 7903201bb4 now that #11197 is in, as agreed in #10711:

The recorder and replay tool code are unchanged by the rebase. Details are in the updated body.

@Dextroid17 thanks for the Ghost of Yōtei runs and for following up on --no-release. The repeated pacing give-up at call 30566 on both devices looks worth a look in the follow-up work on the replay tool.

@JJenkx

JJenkx commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Linux follow-up: successful fixture qualification, but a native ArcRunner capture stops before completing a frame.

I started from your previous head 9aad99ba3cac, applied the product changes unchanged to a private ArcRunner integration (c247fcd7706, not clean main), and adapted CMake to retain merged #11197's unquarantined serial/concurrent checks. Private tested head: db1050d7f98a28b34588ff4d3706d202854b26db. Linux / Ryzen9 7950X / RX6650XT / RADV.

Your PR advanced to c86747300210 during the run. I checked that head before posting: FrameRecorder.cpp, tools/FrameReplay.cpp and HostMapping.hpp are byte-identical to the versions tested here. The latest head also already contains the concurrent-test Unix quoting fix described in the historical bundle; no further action is needed for that fix.

  • Baseline's four registered format/state tests pass. The integration passes all 13 format, EOP, state, serial-copy and concurrent-copy checks. These are CPU memory/driver-state fixtures and did not instantiate Vulkan; they are not GPU-validation evidence by themselves.
  • Separately, existing agc_driver_prepared_exports passes on both builds with ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation: one actual Khronos insertion per arm, no missing layer, skips or VUIDs.

Native limitation: ArcRunner PPSA21406 reached its saved doorway scene. A two-frame capture triggered at approximately316s started at boundary1529 and then stopped:

[frame-capture] started at frame boundary 1529: 835 ranges, 9493.8 MiB snapshot in 6.1 s
[frame-capture] stopped: the GPU work submitted before a memory delta did not finish within 10 s

The header records 0 completed frames,36 calls,9,954,955,264 raw snapshot bytes and152,387,584 delta bytes. After frame-start, the call stream contains35 submissions, one suspend and36 memory groups. The last recorded submit has queue32; the metadata tail is included. This identifies the failing recorder wait but does not establish its root cause or a Linux-specific regression. I did not replay the incomplete capture.

The title continued rendering after recording stopped and closed normally at430s, exit0, with saves/configuration/kernel limit restored and no remaining title process. Native run: two actual Khronos insertions and no VUIDs. No FPS claim.

Focused logs, exact private integration patch, historical quoting fix and capture metadata — SHA-256 2ed01df39cf4935a3fd3b66b558aedec11f626b5b059ab0ef5884fa9d39a165b (public download verified). No game memory, executable, captured shader modules or raw capture is included. The initial unwatched failure in the retained log was my local CMake adaptation missing your environment setting; corrected before the final13/13 run, not a defect in your PR.

AI-assisted: yes.

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 26, 2026
@janzezno

Copy link
Copy Markdown
Contributor Author

@JJenkx thanks, and for checking the new head against what you tested.

The stop comes from the wait #11197 added to writeDelta, not from the Linux port. When submissions are still in flight and the delta has blocks to read, the recorder waits up to 10 s for the driver to go idle, and stops the capture if it doesn't. That code is the same on Windows and Linux, and the "Frame capture" debt entry on main (kept in this PR) already lists the case: a capture stops when GPU work in flight at a delta waits on a CPU store the title only makes later. The delta is read inside the title's driver call, so a store that comes after that call can't release the wait. In your capture, the submission on queue 32 probably waits on such a store.

So ArcRunner hits a limitation of the recorder on both platforms. It doesn't show a problem in this PR. Finding which wait the queue was blocked on would need the serial tracing and --submits listing from #11436. A fix would need a different stable point for the delta than "the driver is idle", which is #11197's design, so it belongs in a separate PR.

@JJenkx

JJenkx commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

Thanks, agreed that the drain is the cross-platform recorder limitation, not the Linux port. I reduced it to a small game-independent two-queue reproducer, so further work does not require ArcRunner or its9.95GB snapshot.

  • Queue0x20: WAIT_REG_MEM for a synthetic label to equal1.
  • Queue0: a later WRITE_DATA of1 to that label.
  • After observing the consumer in the awaited-wait registry, dirty one unrelated byte to require a delta, then submit the producer.
  • No CPU store to the awaited label is made during either measured run. A15s fallback guard exists only for cleanup and never fired.

Same driver (db1050d7f98a, recorder/tool/HostMapping byte-identical to yourc86747300210): capture off accepts the producer in0.004ms; capture on takes10000.195ms, emits the same memory-delta drain warning and ends with0frames, then both queues complete once recording abandons the drain. Rebuilt with the portable build helper and repeated:0.005ms versus10000.194ms, same outcome. These are CPU driver-state checks, not GPU/FPS benchmarks.

Thus the limitation also covers a wait released by a later submission on another queue, without a later CPU store to the label: the recorder blocks that producer's API call before its acceptance. I also inspected the last ArcRunner submit offline: at dword32, it contains a32-bit WAIT_REG_MEM (compare3/equal, reference1, mask0xffffffff). That is consistent with this mechanism; the synthetic test is the isolated proof of the dependency, not proof of the title's producer path.

Portable reproducer source, Ninja/CMake build helper, instructions and two paired logs — SHA-256dcf56cc54833c76a0252694d8b0c82f36215e6cba7a6e2aa1b8cdd7451e64f00 (public download verified). No game assets or raw title capture included. This is supplementary evidence for a separate recorder design fix; no additional change is requested in the Linux port.

AI-assisted: yes.

@oneandonlydean
oneandonlydean merged commit e07c21c into boykopovar:main Oct 27, 2026
18 of 19 checks passed
lgqdaxia added a commit to lgqdaxia/AnyPS5 that referenced this pull request Oct 27, 2026
Remove the extra blank line between capture trace output and frame capture. The independent changes in boykopovar#11157 may merge in either order; the second branch keeps both entries when rebasing.

AI-assisted: Codex
Sam-mon7 added a commit to Sam-mon7/AnyPS5 that referenced this pull request Oct 27, 2026
…g, reservation errors

--cold-pipelines now clears an inherited APS5_HEADLESS_PIPELINE_CACHE on both platforms, the replay test
builds its command with boykopovar#11157's quoting so the redirect and --repeat work under sh and cmd, and a reservation
that fails in a repeated pass is reported and fails the replay instead of being dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chitters2004 added a commit to chitters2004/AnyPS5 that referenced this pull request Oct 28, 2026
…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>
lgqdaxia added a commit to lgqdaxia/AnyPS5 that referenced this pull request Nov 1, 2026
Remove the extra blank line between capture trace output and frame capture. The independent changes in boykopovar#11157 may merge in either order; the second branch keeps both entries when rebasing.

AI-assisted: Codex
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants