Skip to content

Long renders: one-GOP chunks taken in turn, resume a stopped or failed render, an estimate first; --fps vs the page's, --out's base - #113

Merged
ZLHad merged 2 commits into
mainfrom
claude/render-long
Oct 18, 2026
Merged

ZLHad merged 2 commits into
mainfrom
claude/render-long

Conversation

@ZLHad

@ZLHad ZLHad commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Rendering a long film is mechanical work every film needs; this makes bin/vh render handle it.

What and why · 改了什么、为什么

  1. Balanced workers · 负载均衡。 Before, the frames were split evenly by count across the workers, so one heavy chapter (3D at 4K) kept one worker busy long after the others had finished. Now the span is cut into chunks of one GOP each, and the --workers browsers take the next chunk when they finish one. Each chunk goes into its own x264, so the video no longer depends on --workers or on which browser drew which chunk. The fixed GOP grid, the concat of whole-GOP chunks that bin/vh patch cuts on, and the sidecar are unchanged (chunks now lists one entry per GOP; new reused_chunks). The join now passes each chunk's exact length, as patch's join already does. A browser goes on to its next chunk while the previous chunk's encoder is still flushing.
  2. Resume instead of hand-joined --range renders · 续渲,不用再手拼。 A render that is stopped (Ctrl-C, SIGTERM) or fails (a frame past --frame-timeout, an encoder that stops) keeps the chunks it finished in .<out>.parts/. Running the same command again draws only the rest. plan.json there records what the chunks were made from: the page and the sha256 of every file the browsers were sent (host.mjs now records what it serves), the options that shape the video (everything except --workers, --frame-timeout and the estimate), the browser, ffmpeg, and the render tools themselves. When any of that changed, the render names what changed and starts over. A chunk in which the page reported an error is never kept. After a failure, the other workers finish the chunk they are on, so that chunk is kept too.
  3. Estimate first · 先估算。 A span of more than 600 frames still to draw (any span with --estimate) first draws six places of three consecutive frames in one browser and encodes them with the film's settings. It prints the ms per frame, the time with N workers, the video size as a range, and the disk needed at the peak (the chunks plus the joined copy). It refuses with exit 2 when even the low end does not fit. It takes about 1 s at 1080p; --no-estimate skips it, and bin/vh patch passes that flag. The existing flat-film disk floor check is unchanged.
  4. --fps vs the page's own · 帧率不一致时提醒。 The page contract gets an optional fps field (engines/CONTRACT.md). The canvas-film template exposes FILM.fps through it, and render reads FILM.fps directly for copies of the template made before this change. When the two differ, render prints one line naming both.
  5. --out base · 相对路径的基准。 I checked every tool: all of them resolve a relative output path against the current folder (frames, textcheck, storyboard, rhythm, deliver, mux, fonts subset). So render keeps that base, and the docs now say so. The surprise came from the short form of the path: run from the harness with --out out/x.mp4, render printed out/x.mp4, which looks like the project's own out/. Now, when a relative --out lands outside the project, render prints the full path at the start and at the end.

Design choice for item 2 · 第 2 项为什么这样做

I picked resume over a "join range renders" command. Hand-joined range renders had two causes: one worker holding the render up (fixed by item 1), and a failure late in the render losing everything (fixed by resume). With both gone there is nothing left to join, and every film keeps one render with one sidecar, so patch works on it unchanged. A join command would have to check that the pieces were made with the same arguments, on one GOP grid, in the same browser. Resume does those same checks, with the plan file holding the record. One-GOP chunks make resume exact: a kept chunk is the same bytes a fresh render would write, whatever the worker count.

Bytes vs. today · 和改动前的字节

The frames drawn are the same as before. The encoded bytes are the same as an earlier render that also had one GOP per chunk; I checked this on the fixture with 4 workers. Where an earlier render had several GOPs per chunk, the bytes differ at x264's rate-control level: on the canvas-film template (4 chunks of 3 GOPs before) the average PSNR was 54 dB, with 84 of 695 decoded frames identical. Before this change, a render's bytes also depended on --workers (1 vs 4 workers: 20 of 90 decoded frames identical). Now 1, 2 and 4 workers give 90/90 identical decoded frames. There is a side effect for patch: its redrawn GOPs now come back byte-identical to the film's (frames drawn again only to reach the keyframes at least ∞ dB). Before, they came back at about 54 dB.

How it was verified · 怎么验证的

  • tools/ci.sh --committed passes with VH_BASH=/bin/bash, with shellcheck and pyflakes really running (the uvx and uv recipe from CONTRIBUTING). One check was skipped: the HyperFrames runtime is not installed in a clean checkout. All the existing render and patch cases pass unchanged.
  • New CI cases:
    • On tools/film/fixture/uneven.html (frames from 2 s on take 30 ms each, and frame 75 takes 1.5 s): 3 workers make the video 1 worker makes (PSNR, since the runs use separate browsers). With --frame-timeout 1 the render exits 1 and keeps 5 of 6 chunks. After an edit to the page, the next run says the kept chunks are not used and names the page. The same command then resumes (5 of 6 chunks), and its video matches the whole render; the sidecar lists chunks per GOP and reused_chunks: 5.
    • The canvas-film template at --fps 60, run from another folder with a relative --out: render prints the --out note, the fps note (60 vs the page's 30) and the estimate. On a disk with room only for a flat film it is refused (exit 2) and leaves no parts folder behind.
  • By hand:
    • Ctrl-C after 2 chunks: exit 130, 2 chunks and plan.json kept, no Chrome left; the next run resumes and draws only the rest.
    • The estimate against real renders of the template: 9 s estimated vs 11 s taken; 6–12 MB estimated for 9.0 MB at 60 fps; 10–19 MB for 9.3 MB at 30 fps (just under the low end).
    • bin/vh patch on a template film rendered by the new code passes every check, with ∞ dB on the redrawn frames.
  • tools/readpath.py: no change.

Review focus · 请重点看

  1. The x264 bytes change for films whose earlier render had several GOPs per chunk (decoded frames ≥ 54 dB from before). An existing film plus a later patch still works, because patch only needs the GOP grid.
  2. What the resume key holds. Files the page loads from outside its folder (a CDN) are not in it. --frame-timeout and --workers are deliberately left out.
  3. The estimate rule: more than 600 frames by default; the size is printed as a range, and the render is refused only when the low end does not fit.
  4. Item 5 keeps the current-folder base, because that is what every other tool does, and only makes the path visible. The alternative is resolving against the project, which would make render the only tool with that base.

Checklist · 自查

  • tools/ci.sh --committed passes · 通过
  • User-visible change noted in CHANGELOG.md → Unreleased · 已记进 CHANGELOG
  • Changed CLAUDE.md → ran bin/vh sync-agents · 未改 CLAUDE.md,不需要

ZLHad added 2 commits October 19, 2026 00:18
…r resumes, an estimate first

bin/vh render split the frames evenly across its workers, so one heavy chapter held
the whole render up, and a late frame timeout threw a long 4K render away; the way
round (range renders joined by hand) left no sidecar for bin/vh patch.

- Chunks of one GOP each, taken by the --workers browsers as they finish one, each
  into its own x264: the video no longer depends on --workers. GOP grid, whole-GOP
  concat (now with each chunk's exact length) and the sidecar stay compatible.
- A stopped or failed render keeps its finished chunks (.<out>.parts/plan.json
  records the page, the sha256 of every file served, the options, browser, ffmpeg
  and tools); the same command resumes, anything changed starts over.
- Spans over 600 frames (or --estimate) are estimated first: time per frame, time
  with the workers, size range and peak disk, refused when the disk cannot hold it;
  --no-estimate skips it (patch does).
- --fps unlike the page's own (new optional window.film.fps; canvas-film's FILM.fps)
  is said; a relative --out outside the project is printed in full.
- Docs (help, bin/vh, engines/README, CONTRACT), CHANGELOG, CI cases on a new
  uneven-cost fixture and the canvas-film template.
@ZLHad
ZLHad marked this pull request as ready for review October 18, 2026 16:28
@ZLHad
ZLHad enabled auto-merge (squash) October 18, 2026 16:28
@ZLHad
ZLHad merged commit fd9d520 into main Oct 18, 2026
2 checks passed
@ZLHad
ZLHad deleted the claude/render-long branch October 18, 2026 16:40
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.

1 participant