Skip to content

feat(inspector): File > Cut Replay -- cut a sub-range into a new .vtx - #46

Merged
virtexalejandro merged 9 commits into
mainfrom
feat/inspector-cut-replay
Jul 27, 2026
Merged

feat(inspector): File > Cut Replay -- cut a sub-range into a new .vtx#46
virtexalejandro merged 9 commits into
mainfrom
feat/inspector-cut-replay

Conversation

@onur-zenos

@onur-zenos onur-zenos commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What

File > Cut Replay... -- writes a new .vtx containing a sub-range of the loaded replay.

Range modes:

  • Time / Frame / UTC -- exact cuts. When a bound falls inside a chunk, that edge chunk is re-serialized with only the kept frames; interior chunks are copied byte-verbatim. UTC accepts ISO-8601 or unix seconds/ms/100ns-ticks numbers.
  • Chunk (whole chunks) -- keeps chunks [start..end] untouched (pure verbatim copy).

The live preview shows the exact frames/time/chunks/size and how many frames each edge rewrite drops. The copy runs on a worker thread with a Save As dialog (defaults to <name>_cut.vtx).

How

ReplayCutService (new):

  • Header block copied verbatim (magic + length-prefixed payload) -- schema and recording metadata stay bit-identical. Rebuilding via SerializeHeader would stamp now() as the recorded time, so verbatim is the faithful option.
  • Rewritten edge chunks mirror ChunkedFileSink::SaveChunk framing exactly: frames read through a private SDK reader, SerializeChunk, zstd only-if-beneficial (>=512 B, must shrink, level 10), u32 length prefix, XXH3_64bits checksum of the on-disk payload, and a regenerated seek-table entry.
  • Footer rebuilt via FlatBuffersVtxPolicy::SerializeFooter: frame numbering rebased to 0, created_utc/game_time sliced with absolute tick values preserved, gaps/segments filtered+rebased, duration recomputed from the sliced UTC stamps. Timeline events are not carried over. Footer written uncompressed (the reader sniffs zstd by magic).

Testing

Headless harness on a real 16,556-frame capture, comparing per-frame entity content hashes against the source and validating seek-table contiguity:

  • mid-chunk cut 5100..9150 (both edges rewritten) -- PASS
  • double-trim inside a single chunk 5150..5160 (11 frames) -- PASS
  • chunk-aligned 5000..9999 (pure verbatim path) -- PASS
  • 301-frame cut in a single-frame-chunk tail zone -- PASS

Also GUI-validated end to end: a real cut reopens cleanly in the inspector and vtx_cli with contiguous seek table, exact time-table sizes, preserved header uuid/timestamp, and absolute UTC stamps.

Stacked on #45

Based on fix/inspector-timeline-utc (uses its TimelineViewService::FrameToElapsedSeconds/FrameAtElapsedSeconds). Merge #45 first, then this diff is just the cut feature.

The timeline window derived frame time as frame_index / (total_frames /
duration_seconds). On captures with recording stalls the average fps is
far below the live rate, so displayed times drift by the total stalled
time (observed: a frame stamped 13:54 wall clock displayed as ~6:47 on a
capture with 643s of stalls at ~29fps live rate).

Map frame -> elapsed seconds through the footer ReplayTimeData table
instead: created_utc preferred, game_time fallback, walking back to the
nearest stamped frame for frames recorded without a value. Files without
a time table keep the old linear mapping. Applies to the current-time
readout and the strip hover tooltip.
BuildDroppedFrameMap flags every frame whose wall-clock span from the
previous stamped frame (footer created_utc, game_time fallback) exceeds
1.5x the expected interval, and estimates the missing frame count per
gap. The timeline strip paints flagged frames red, the info panel shows
a gap/missing-frame summary plus an editable "Drop FPS" expected rate
(default 30), and the hover tooltip reports the gap duration and
estimated missing frames. Files without a time table flag nothing.
…tional

Each gap from the dropped-frame map is overlaid on the slider track as a
red band positioned and sized by wall clock (start_seconds / duration),
so band width reads as gap duration - unlike the frame strip, where a
two-minute stall spans only a few bars.
The slider axis is now elapsed seconds over the full capture duration:
the grab sits at the current frame's footer-table time, and dragging
resolves the frame via binary search for the last stamp at or before the
grabbed time (zero entries skipped; linear fallback without a table).
Equal slider distance now means equal wall-clock time, and the red gap
bands align with the grab. Inside a gap the grab resolves to the frame
at the gap's start, snapping to it on release.
Captures never sustain their nominal rate, so gap detection now runs at
entered_fps * 0.75 (30 -> 22.5 effective). The input label shows the
factor and the resulting effective rate.
ReplayCutService plans a cut snapped to whole chunks (the chunks
containing the requested first/last frame) and writes a new file:
header block and kept chunks copied verbatim, footer rebuilt with frame
numbering rebased to 0, the time table sliced (tick values stay
absolute), gaps/segments filtered+rebased, and duration recomputed from
the sliced UTC stamps. Timeline events are not carried over; the footer
is written uncompressed (the reader sniffs zstd by magic).

The Cut Replay window accepts the range as elapsed time, frames, or
absolute UTC (ISO-8601 or unix seconds/ms/100ns ticks), previews the
snapped chunks/frames/time/size live, and runs the copy on a worker
thread with a Save As dialog (defaults to <name>_cut.vtx).

Verified headless on a 16556-frame capture: mid cut (frames 5000-9999)
and tail cut (16000-16555) both reopen cleanly with matching frame
contents, sliced time tables, and absolute UTC stamps.
Time/Frame/UTC cut ranges are now exact: when a bound falls inside a
chunk, that edge chunk is re-serialized with only the kept frames
(frames copied via a private SDK reader, SaveChunk framing mirrored:
length prefix, zstd-if-beneficial, xxHash64 of the on-disk payload) and
its seek-table entry regenerated. Whole interior chunks still copy
verbatim. A new "Chunk (whole chunks)" range mode keeps the previous
snap-to-chunk behavior via start/end chunk indices, and the preview
reports how many frames each edge rewrite drops.

Verified headless on a real capture: mid-chunk cut 5100..9150 (both
edges rewritten), single-chunk double-trim 5150..5160, chunk-aligned
5000..9999 (pure verbatim path), and a 301-frame cut in the
single-frame-chunk tail zone -- all reopen cleanly with per-frame
entity content-hash matches, contiguous rebased seek tables, and
absolute UTC stamps intact.
@onur-zenos
onur-zenos force-pushed the fix/inspector-timeline-utc branch from c36c349 to a7e2cc2 Compare July 27, 2026 10:19
@onur-zenos
onur-zenos force-pushed the feat/inspector-cut-replay branch from 8483cab to fe101e8 Compare July 27, 2026 10:19
@virtexalejandro
virtexalejandro deleted the branch main July 27, 2026 10:34
@virtexalejandro
virtexalejandro changed the base branch from fix/inspector-timeline-utc to main July 27, 2026 10:35
@virtexalejandro
virtexalejandro merged commit 6e9aebe into main Jul 27, 2026
9 checks passed
@virtexalejandro
virtexalejandro deleted the feat/inspector-cut-replay branch July 27, 2026 10:44
virtexalejandro pushed a commit that referenced this pull request Jul 27, 2026
…#46)

* feat(inspector): File > Cut Replay -- write a sub-range as a new .vtx
* feat(inspector): exact-frame cuts with edge-chunk rewriting
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