Repository navigation
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
Conversation
…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.
# Conflicts: # CHANGELOG.md
ZLHad
marked this pull request as ready for review
October 18, 2026 16:28
ZLHad
enabled auto-merge (squash)
October 18, 2026 16:28
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rendering a long film is mechanical work every film needs; this makes
bin/vh renderhandle it.What and why · 改了什么、为什么
--workersbrowsers take the next chunk when they finish one. Each chunk goes into its own x264, so the video no longer depends on--workersor on which browser drew which chunk. The fixed GOP grid, the concat of whole-GOP chunks thatbin/vh patchcuts on, and the sidecar are unchanged (chunksnow lists one entry per GOP; newreused_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.--rangerenders · 续渲,不用再手拼。 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.jsonthere records what the chunks were made from: the page and the sha256 of every file the browsers were sent (host.mjsnow records what it serves), the options that shape the video (everything except--workers,--frame-timeoutand 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.--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-estimateskips it, andbin/vh patchpasses that flag. The existing flat-film disk floor check is unchanged.--fpsvs the page's own · 帧率不一致时提醒。 The page contract gets an optionalfpsfield (engines/CONTRACT.md). The canvas-film template exposesFILM.fpsthrough it, and render readsFILM.fpsdirectly for copies of the template made before this change. When the two differ, render prints one line naming both.--outbase · 相对路径的基准。 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 printedout/x.mp4, which looks like the project's ownout/. Now, when a relative--outlands 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 --committedpasses withVH_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.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 1the 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 listschunksper GOP andreused_chunks: 5.--fps 60, run from another folder with a relative--out: render prints the--outnote, 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.plan.jsonkept, no Chrome left; the next run resumes and draws only the rest.bin/vh patchon a template film rendered by the new code passes every check, with ∞ dB on the redrawn frames.tools/readpath.py: no change.Review focus · 请重点看
--frame-timeoutand--workersare deliberately left out.Checklist · 自查
tools/ci.sh --committedpasses · 通过CHANGELOG.md→ Unreleased · 已记进 CHANGELOGCLAUDE.md→ ranbin/vh sync-agents· 未改 CLAUDE.md,不需要