Repository navigation
tools: AGC frame capture and replay benchmark - #505
Conversation
ANYPS5_AGC_CAPTURE=<dir> records one frame the Metal AGC driver executes: the pre-frame guest memory (resident GPU-visible ranges, LZ4 chunks), each queue's state, every submission with its shader registry and the guest pages that changed before it ran, and the scanout pixels the game showed. Hooks sit at the submission boundary (queue worker loop) and in Present. anyps5_agc_replay maps the capture at its guest addresses, feeds the recorded command buffers through the same driver K times and reports worker CPU ms, worker instructions, wall ms, GPU busy ms and the image hash, with --expect-hash / --expect-png regression checks and --dump-png. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 45569a7aff
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Each queue's submissions in order; the hook applies a submission's page delta just before it runs. | ||
| std::map<std::uint32_t, std::vector<const Submit*>> byQueue; | ||
| for (const auto* submit : submits) byQueue[submit->queue].push_back(submit); |
There was a problem hiding this comment.
Preserve global submission order during replay
When a captured frame uses multiple queues, grouping submissions only by queue lets their worker hooks run in scheduler order rather than the globally serialized order recorded in the file. Each page delta's previous hash depends on that global capture order, so if queue B runs before an earlier captured queue A, B can take the current != page.previous path and silently discard a required CPU delta as though replayed GPU work produced it, making both output and timing nondeterministic. Gate the hooks on the next global captured submission, not merely each queue's cursor.
Useful? React with 👍 / 👎.
| for (auto& table : tables) { | ||
| // A range that is no longer borrowed is not read by this submission. | ||
| const auto found = std::find_if(context.ranges.begin(), context.ranges.end(), [&](const auto& range) { | ||
| return range.guestAddress == table.guest && range.host.data() == table.host && range.host.size() == table.bytes; | ||
| }); | ||
| if (found == context.ranges.end()) continue; |
There was a problem hiding this comment.
Capture guest ranges added during the frame
If the title creates or replaces a GPU-visible allocation after capture begins, tables contains only the ranges snapshotted by Begin, and this loop never emits the new range or its contents even though it is present in context.ranges. A later captured submission may therefore reference an address absent from the replay's configured ranges, causing validation or host() lookup to fail, or leaving required data unavailable. Detect ranges not represented in tables and record their initial state before recording the submission.
Useful? React with 👍 / 👎.
| if (!AnyResident(*residentIn, base, page, data, bytes)) return; | ||
| hashesOut[i] = Hash(data, bytes); | ||
| if (hashesOut[i] == ZeroHash(bytes) && std::all_of(data, data + bytes, [](std::byte b) { return b == std::byte{0}; })) return; |
There was a problem hiding this comment.
Preserve initialized pages that are not resident
Under memory pressure, mincore can report an anonymous page as nonresident even though its initialized contents are preserved in compressed memory or swap; this early return treats such a page as zero and omits its chunk from the capture. If the GPU is the first consumer to fault that page back in during the recorded frame, the game reads the preserved bytes while replay reads a zero-filled mapping, silently invalidating the benchmark result. Residency can optimize when pages are read, but it cannot be used as proof that their contents are zero.
Useful? React with 👍 / 👎.
… mid-frame The replay hook now waits until a submission is the next one in the capture's global order before applying its page delta (5 s escape hatch, counted and reported). The capture records a range that appears after the snapshot with its contents before the submission that first sees it. The residency filter's reliance on MINCORE_PAGED_OUT is documented. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj
… a blank present Replay: a guest range the capture recorded mid-frame was mapped and filled before the first submission. It now exists only from the submission that introduced it (the driver's borrowed ranges are replaced after a drain, its chunks are written then), a later range replaces an overlapping earlier one, and the next repeat starts from the pre-frame set again. Capture: a present in the recorded window without a buffer took a slot but never counted towards completion, so the End record was not written. It now counts and only has no Display record. The two rules live in AgcCaptureLogic.hpp and are covered by metal_utility_tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj
What
Frame capture and replay for the Metal AGC driver. Perf and correctness work no longer needs a full game run for each iteration: capture a frame once, then replay it in seconds through the same driver code.
Capture (env-gated, off by default)
ANYPS5_AGC_CAPTURE=<dir>arms it.ANYPS5_AGC_CAPTURE_FLIP=<n>(default 600) selects the flip.ANYPS5_AGC_CAPTURE_AFTER_S=<s>instead selects the first flip after that many seconds.ANYPS5_AGC_CAPTURE_FRAMES=<n>records consecutive frames (default 1).<dir>/frame.agc(versioned, little-endian; format inAgcFrameCapture.hpp) with:game-<n>.png.ExecuteSubmission) and inPresent, plusSubmission::commandAddressand a GPU-busy completion handler that is off unless the replay enables it. The per-draw path is untouched.Replay
build-rosetta/rosetta/bin/anyps5_agc_replay <dir> [--frames N] [--repeat K] [--dump-png out.png] [--expect-hash HEX] [--expect-png ref.png [--max-diff-pixels N]] [--metallib P] [--quiet](x86_64, run witharch -x86_64)DirectDriverdoes, including working-set mode.MetalPresentation::ReadPixels.agc-queue-*threads, minus the replay's own page writes);--expect-hashexits 1 unless every timed repeat matches it.--expect-pngcompares against a reference PNG within a pixel budget. Compiled shaders go to<dir>/shader_cache, never the user's cache.Driver API additions (replay-only):
MetalDriver::ReplaceShaderRegistryForReplay,ReplaceQueueStateForReplay,ApplyDirectWorkingSet.Results (three frames of the test title, captured in game runs and replayed under load from other agents)
--expect-hashworks there. (On the live map, short--repeat 3runs with--expect-hashpassed 3 of 4 times; the one exit 1 was not diagnosed.)--expect-pngwith a small--max-diff-pixelsthere.Limitations
Codex review
AgcCaptureLogic.hppwith a unit test. Not exercised by the live-map capture (it has no mid-frame ranges); its replay hash is unchanged, 5 of 5 runs. Unmaps are still not recorded.MINCORE_PAGED_OUT, which xnu reports for compressor and swap pages. Only never-populated pages, which are zero, read as 0. Reading every page would fault the whole 13.5 GiB mapping into the game. The reasoning is now in a comment, andANYPS5_AGC_CAPTURE_READ_ALL=1disables the filter.🤖 Generated with Claude Code
https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj