Skip to content

Reuse a captured page for the rest of its uniform words - #315

Closed
SkippySteve wants to merge 1 commit into
NotRexed:mainfrom
SkippySteve:fix/submitted-page
Closed

SkippySteve wants to merge 1 commit into
NotRexed:mainfrom
SkippySteve:fix/submitted-page

Conversation

@SkippySteve

Copy link
Copy Markdown
Contributor

Summary

  • After the first submit-time capture, memory_read_as_submitted took the pool lock on every word of guest memory the replay reads (uniform and storage descriptors). It then checked whether that 4 KB page was in a capture. The next words of the same buffer are the same page.
  • The replay thread keeps that page's answer: either the capture's 4 KB copy, or the fact that guest memory stands. The next read of the page copies from the kept bytes, or skips the lock, until cap_view_gen changes. A page stamp, the written-page collection, and taking a capture all bump that generation, so a newer write is not served from the old copy.

Test plan

  • python -I -B tests/test_submitted_page.py passes: a page is reused only when the page, its write generation, and the capture generation all match, and memory_read_as_submitted checks that before pools_mu
  • A stamp, the written-page collection, and taking a capture each call cap_view_bump. Removing the stamp's bump fails the test
  • CI: "Check a captured page is read once"

Every word of a uniform buffer took the pool lock to ask whether that page was in the submit-time capture. The following words are the same page, until a stamp or the next capture.
@NotRexed

NotRexed commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Thanks! Measured together with the other replay PRs (replay 27 -> 24-25 ms a frame, fps 23 -> 26 in Kokiri) and applied by hand (conflicts only in the CI workflow; #310 and #315 needed one declaration moved), pushed in 40c3f85.

@NotRexed NotRexed closed this Nov 7, 2026
valonsodev added a commit to valonsodev/Hookshot that referenced this pull request Nov 9, 2026
memory_read_as_submitted kept the last page it read (NotRexed#315) so the next read of that page
skipped pools_mu. A handle program reads a uniform word on one page and a storage buffer
word on another, so with one page kept those reads took turns missing: each took pools_mu
and copied the whole 4 KB page for its 4 bytes. In Hyrule Castle Town, on a test route,
that was about a quarter of the replay thread's draw time (program_for's answer handles).

The answers are kept per page now, 32 a thread, by page number; a page is reused under
the same rule as before (same page, page generation and capture view).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
valonsodev added a commit to valonsodev/Hookshot that referenced this pull request Nov 10, 2026
memory_read_as_submitted kept the last page it read (NotRexed#315) so the next read of that page
skipped pools_mu. A handle program reads a uniform word on one page and a storage buffer
word on another, so with one page kept those reads took turns missing: each took pools_mu
and copied the whole 4 KB page for its 4 bytes. In Hyrule Castle Town, on a test route,
that was about a quarter of the replay thread's draw time (program_for's answer handles).

The answers are kept per page now, 32 a thread, by page number; a page is reused under
the same rule as before (same page, page generation and capture view).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
valonsodev added a commit to valonsodev/Hookshot that referenced this pull request Nov 10, 2026
…opy kept a thread

memory_read_as_submitted kept, for a page a capture holds, a 4 KB copy of it a thread (NotRexed#315), and
the 32 kept pages (the commit before) made that 128 KB of TLS a thread. A read of a captured page
now copies the words it asks for from the capture under pools_mu, as a read that misses already did;
the 32 slots keep only "in no capture", 24 bytes each.

On NVIDIA Linux the 4 KB copies were a third of the replay's thread in Kokiri Forest
(djs5008's measurements on NotRexed#1375). Where the copy is cheap it still cost more than the extra
lock: OoT's Market on a test route, Linux, RTX 3060 Ti (cached host memory, 0.3 us a 4 KB read),
only hookshot_vk.so swapped, 4 rounds alternating after a warm-up (one of the first's runs hung at
boot waiting on a GPU fence, as runs before this change did too):

                       fps (median)   replay thread (Presentation Thread)
  before (one commit)  32.96          66.8 %
  after                35.48          60.9 %

Every run after (34.1-35.8) was above every run before (32.0-33.5). Reads that went to the
captures rose from ~0.3 to ~1.0 million a run, each a few bytes under pools_mu.

Co-authored-by: djs5008 <35700921+djs5008@users.noreply.github.com>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
valonsodev added a commit to valonsodev/Hookshot that referenced this pull request Nov 10, 2026
memory_read_as_submitted kept the last page it read (NotRexed#315) so the next read of that page
skipped pools_mu. A handle program reads a uniform word on one page and a storage buffer
word on another, so with one page kept those reads took turns missing: each took pools_mu
and copied the whole 4 KB page for its 4 bytes. In Hyrule Castle Town, on a test route,
that was about a quarter of the replay thread's draw time (program_for's answer handles).

The answers are kept per page now, 32 a thread, by page number; a page is reused under
the same rule as before (same page, page generation and capture view).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
valonsodev added a commit to valonsodev/Hookshot that referenced this pull request Nov 10, 2026
…opy kept a thread

memory_read_as_submitted kept, for a page a capture holds, a 4 KB copy of it a thread (NotRexed#315), and
the 32 kept pages (the commit before) made that 128 KB of TLS a thread. A read of a captured page
now copies the words it asks for from the capture under pools_mu, as a read that misses already did;
the 32 slots keep only "in no capture", 24 bytes each.

On NVIDIA Linux the 4 KB copies were a third of the replay's thread in Kokiri Forest
(djs5008's measurements on NotRexed#1375). Where the copy is cheap it still cost more than the extra
lock: OoT's Market on a test route, Linux, RTX 3060 Ti (cached host memory, 0.3 us a 4 KB read),
only hookshot_vk.so swapped, 4 rounds alternating after a warm-up (one of the first's runs hung at
boot waiting on a GPU fence, as runs before this change did too):

                       fps (median)   replay thread (Presentation Thread)
  before (one commit)  32.96          66.8 %
  after                35.48          60.9 %

Every run after (34.1-35.8) was above every run before (32.0-33.5). Reads that went to the
captures rose from ~0.3 to ~1.0 million a run, each a few bytes under pools_mu.

Co-authored-by: djs5008 <35700921+djs5008@users.noreply.github.com>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants