Skip to content

Claude/snap precopydata registry wsr7hr - #91

Merged
jmquigs merged 4 commits into
masterfrom
claude/snap-precopydata-registry-wsr7hr
Aug 27, 2026
Merged

jmquigs merged 4 commits into
masterfrom
claude/snap-precopydata-registry-wsr7hr

Conversation

@jmquigs

@jmquigs jmquigs commented Aug 26, 2026

Copy link
Copy Markdown
Owner

No description provided.

claude and others added 4 commits August 22, 2026 19:54
For an indexed triangle list, the maximum unique verts a draw can touch
is prim_count * 3. When compute_prim_vert_count derives num_vertices from
the full bound VB (vb_size / vert_size) it can vastly exceed that bound,
which is a strong signal the VB is shared across many draws or the
geometry is CPU/software-animated and not usefully snapshottable. It also
tends to coincide with a missing-index-buffer error. Log a warning in
snapshot::take so users get a clear in-log hint instead of a confusing
error chain.

No behavior change; diagnostic only.

Authored by Claude (claude-opus-4-7)
Character-mesh parts packed into a large dynamically-updated vertex/index
buffer (a "megabuffer") failed to snapshot, logging "failed to get index
buffer data, was not previously saved" and a vertex count equal to the
whole bound buffer rather than the draw's range.

Two native fixes (DX11 only; managed/interop unchanged, no version bump):

1. Capture buffers filled after creation. hook_CreateBuffer only copied
   data supplied as pInitialData. Now, when precopy is enabled, also hook
   ID3D11DeviceContext::Map/Unmap and UpdateSubresource to copy index and
   vertex buffer bytes whenever the game fills them. CreateBuffer records
   per-buffer metadata (type + size) so the new hooks can identify tracked
   VB/IBs cheaply; a createtime entry is pushed only on new keys so the
   expiry GC stays correct and continuously-updated buffers self-heal.

2. Snapshot only the draw's sub-region. set_buffers_d3d11 assumed per-mesh
   buffers. It now slices the prim_count*3 indices starting at start_index
   (honoring bind offsets), finds the referenced vertex range, carves out
   those vertices, and re-bases the indices to 0 so managed reads a
   self-contained mesh from offset 0 (its tested path). num_vertices,
   base_vertex_index, min_vertex_index and start_index are set accordingly.

Authored by Claude

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DsyY4xhCQs2tQ1f3BkmDTL
Authored by Claude (claude-opus-5)

Builds on the preceding commit, which could capture and slice buffers the
game fills after creation but was not usable in practice: it could not be
turned on without restarting the game, it made the game unplayably slow
once on, and when it did produce a snapshot there was no way to tell a
good one from a garbage one.  Introduces "dynamic buffer" as the shorthand
for an index or vertex buffer whose contents we get by watching the game
write it, rather than from the pInitialData it was created with.

Gated behind a new `snapshot-dynamic-buffers` Cargo feature, off by
default, since snapshotting is still slower on this path than the plain
whole-buffer one and the path it replaces has stricter size checks.
Declared on hook_core, propagated to hook_snapshot and shared_dx.  With it
off the DLL behaves as it did before this work: the write hooks move to a
gated hook_dynamic_buffers module and are not installed, their fn pointers
leave HookDirect3D11Context (which is Copy and returned by value on every
draw), hook_CreateBuffer records no metadata, and set_buffers_d3d11 hands
over whole bound buffers with the original checks.

Enabling precopy in game now works.  The write hooks decided whether a
resource was a tracked VB/IB by consulting metadata only hook_CreateBuffer
writes, and that is only hooked when precopy was already on when the
device was hooked -- so every buffer the game had already created was
invisible, which is precisely the long-lived megabuffers this exists for.
They now resolve unknown resources themselves via GetType/GetDesc and
cache the answer, negative results included so that constant buffer writes
stay cheap.

Capture runs only while a snapshot is in progress rather than for the rest
of the session.  The cost is one copy of an entire buffer per write, and a
game may write one many times per frame, which is the difference between
slow and seconds per frame.  SnapPreCopyAlways=1 restores always-on
capture.  The copy also reuses its destination allocation: at these sizes
the allocator returns large blocks to the OS, so a fresh Vec per write
meant faulting in new zero pages every time, costing more than the copy.

Fixes a snapshot window that could fail to terminate.  An earlier attempt
here discounted capture time from the window's elapsed wall clock so a
slow captured frame would not expire it; that is a feedback loop, since
capture only runs while the window is open, and a frame that is nearly all
capture advances the clock by nearly nothing.  The window is wall clock
again; snap_ms is the knob for making it longer.

Adds a capture-provenance diagnostic, since a megabuffer part sometimes
snapshots as poly soup and nothing in the log distinguished a stale copy
from a current one.  Each captured buffer records the write path that last
produced its bytes (including the D3D11_MAP type), a global capture
sequence number, a capture count and the stored length; the index and
vertex buffer's are logged at snapshot time.  Comparing the two sequence
numbers says whether the pair came from the same batch of writes.

Holding shift with the clear-texture-lists key drops everything captured
from a dynamic buffer.  These maps are keyed on raw buffer pointers,
nothing hooks buffer Release, and the capture paths' staleness guards only
run when a capture happens -- so after a scene change a snapshot can read
bytes that no longer describe what the draw will read.  Buffers filled at
creation are kept, since DX11 will not read a buffer back and they are not
affected.  The two are told apart by the provenance above.

The DX11Metrics counters this adds are named dyn_precopy_* and report
under "dyn precopy:", to mark them as measuring only the atypical dynamic
path; they stay zero in a default build.
- Add some comments to devnotes on top of claude's thing which is mostly about details
- Update some of the code comments that claude added
@jmquigs
jmquigs merged commit 1c1d9ea into master Aug 27, 2026
1 check passed
@jmquigs
jmquigs deleted the claude/snap-precopydata-registry-wsr7hr branch August 27, 2026 17:09
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