Skip to content

Latest commit

 

History

History
465 lines (344 loc) · 28.8 KB

render-profiles.md

File metadata and controls

465 lines (344 loc) · 28.8 KB

渲染档位与性能(v2.0)

本文档回答四个问题:有哪些档位、质量/速度/体积怎么权衡、快门与并行是怎么实现的、4K60 得要多大的机器。 所有档位都是可选项 —— 不传任何新参数时走 balanced,行为等价于旧的 1080p30 默认。

相关入口:scripts/render_video.mjs(渲染器)、scripts/blur_integrate.py(快门积分器)、 scripts/bench_render.py(基准工装)。


0. 渲染通道:PNG / PNG-fast / JPEG(★ 渲染前必须问用户)

通道 = 中间帧存成什么格式,与最终编码质量(--crf / --preset)解耦: 换通道只换中间帧容器,不代表要降成片码率。

硬规则(SKILL.md 确认点 5):动手渲染之前,把下表摆给用户挑,不许默认开工; 选定值写进 consent.json 的 render_channel,由 gate_check.py --phase render 挡在渲染之前。 用户在打样阶段选定后可沿用同一轮;换通道/换档位要重新确认一次。

通道 参数 相对速度(纯 CSS 图形帧并行 / 满幅照片帧) 画质(客观口径) 中间帧体积(1080p · 纯图形帧实测) 定位
PNG-fast ★默认推荐 --png-fast ×1.02 / ×4.4 逐像素无损(PSNR 99 dB、最大差 0) 0.10 MB/帧(8525 帧 ≈ 0.85 GB) 纯 CSS/MG 图形帧上与 JPEG q95 同速、体积更小、还是无损 → 默认就用它
JPEG q95 --jpeg --jpeg-quality 95 ×1.00 / ×13 PSNR 41.65 dB,低于 x264 crf18 成片自身失真 0.12 MB/帧(8525 帧 ≈ 1.0 GB) 含满幅照片 / 重合成帧、或 4K 终稿时的首选(照片帧上 PNG 是 582ms/帧的黑洞)
PNG(1.4.x 默认) 不带开关 ×1.16 / ×1.0(基准) 逐像素无损 0.08 MB/帧 仅 --profile legacy 逐位复现旧成片、或 master 极限档
JPEG q82 --jpeg --jpeg-quality 82 最快 略低于 q95,仍高于多数成片编码失真 ~0.08 MB/帧 只做打样/迭代预览,不用于交付

体积口径提醒:上表体积为纯 CSS/MG 图形帧(大面积平色 + 文字)实测;同一批通道在 满幅照片 / 重合成帧上会整体抬高一到两个数量级(精细 PNG 单帧可达 1.6–2.0 MB)。

★ 纯 CSS/MG 图形帧上的实测(2026-10-05 · 1920×1080 · 本机 6 浏览器并行)—— 本文件的默认推荐据此修订: 上表的「×13 / ×4.4 / ×1.0」倍率是满幅照片口径。对纯 CSS/MG 图形帧(大面积平色 + 文字, 也就是本技能绝大多数片子的样子),实测吞吐为 JPEG q95 27.14 帧/秒 · PNG-fast 26.65 帧/秒(×1.02) · PNG 精细 23.41 帧/秒(×1.16), 体积 PNG 精细 0.08 MB/帧 < PNG-fast 0.10 < JPEG q95 0.12。 即:纯图形帧上 PNG-fast 与 JPEG q95 基本同速,而且体积更小 —— PNG 的 deflate 对平色块极其高效, 对「锐利文字 + 纯色块」比对 JPEG 更省。「慢 3×、占盘 8×」只对照片/高熵帧成立。 推论:纯 CSS/MG 片子默认走 PNG-fast —— 拿逐像素无损几乎不付代价; 而 JPEG q95 的「快 13×」在这类帧上并不存在,选它的理由只剩「含照片帧 / 4K 终稿时更稳」。

为什么 JPEG q95 不是「降质」:中间帧要再被 x264 压一次才成为成片, 所以中间帧的相对失真只要低于成片编码自身的失真,就完全不可观测 —— q95 的 PSNR 41.65 dB 正好低于 x264 crf18 的自身失真,等于「零代价」换来 13× 编码速度与 1/5 磁盘。 而 PNG/PNG-fast 的「逐像素无损」在这一环没有任何可观测收益(它的收益只在 --profile legacy 逐位复现、或中间帧被当素材二次调色的场合才兑现)。

询问话术(照抄,四条都要列全 —— 这是硬要求):

渲染通道你要哪条? ① PNG-fast(推荐 —— 纯 CSS/MG 图形帧上实测与 JPEG q95 同速、体积更小,且逐像素无损) ② JPEG q95(含满幅照片/重合成帧、或 4K 终稿时首选;照片帧上 PNG 是 582ms/帧的黑洞。 不失质:失真低于成片自身编码失真) ③ PNG(最慢,只有要逐位复现 1.4.x 老成片时才选) ④ JPEG q82(最快,只适合打样)

自动推荐口径(给建议,但仍要用户点头):

片子情况 建议通道 理由
默认(绝大多数片子,纯 CSS/MG 图形帧) --png-fast 与 JPEG q95 同速、体积更小,且逐像素无损 → 默认就用它
帧里有满幅照片 / 重合成 --jpeg --jpeg-quality 95(首选)或 --png-fast PNG 在照片帧上是 582ms/帧,是最大的时间与磁盘黑洞
4K 终稿 --jpeg --jpeg-quality 95 或 --png-fast 4K 精细 PNG 单帧 2–6 MB,磁盘与编码都会被吃干
中间帧必须逐像素无损(合规 / 归档 / 二次调色) --png-fast 无损,且纯图形帧上几乎不付速度代价
看节奏的打样 --profile draft + --preview 档位内已含 png-fast,不必再指定通道

档位表里的 shot 字段(draft/balanced/final = png-fast)保持原样不动 —— 它现在与推荐口径是同一个值:不传通道参数就走 PNG-fast,两者已不再冲突。 只有照片 / 4K 场景才显式加 --jpeg --jpeg-quality 95。

为什么换通道是最大的提速杠杆:中间帧编码在浏览器进程内串行, --concurrency 完全无效(实测并发 1/3/6 路总吞吐 1.80 / 1.86 / 1.87 帧/秒)。 想快只有两条路 —— 换通道,或 --workers 真·多进程。

本机端到端实测(i7-10700 / 16 逻辑核 / 1080p30 / 纯 CSS 图形帧 / --workers 8): --shutter 0 --jpeg --jpeg-quality 95 → 18.4 帧/秒,8525 帧 9 分 7 秒渲完。 对照:同一片子开默认快门(180° / 8 样本)→ 12–110 帧/分,要 10 小时,且中途写满磁盘 (见下方「开快门之前先把磁盘峰值算出来」)。


1. 档位表

--profile 给的是成套预设;任何显式开关(--quality / --fps / --shutter / --workers / --crf / --preset) 都会覆盖档位里的对应项。查看全部档位:

node <skill>/scripts/render_video.mjs . --list-profiles
档位 画质 帧率 快门 截图模式 CRF / preset workers 用途
legacy 1080p 项目 fps(默认 30) 关 png(无损精细) 18 / medium 0(单浏览器 + 场景级并发) 逐位复现 v1.4.x 旧成片
draft 1080p 30 关 png-fast 20 / veryfast 6 打样 / 迭代,最快
balanced 1080p 30 180° png-fast 18 / medium 6 默认推荐(质量/速度平衡)
final 4K 60 180° png-fast 16 / slow 8 终稿(耗时约 1080p30 的 8–12×)
master 4K 60 180° png(精细) 14 / veryslow 8 极限画质(只在最终交付用,很慢)

legacy 的 fps: 0 表示「不覆盖项目 fps」——所以它渲出来的帧数与旧版完全一致,这是向后兼容的硬证据。

画质档(--quality)

画质只改 deviceScaleFactor,不改任何布局:同一份 HTML 只是采样更密,构图逐像素不变。

--quality 目标高度 1920×1080 项目上的 deviceScaleFactor 输出分辨率
1080p 1080 1× 1920 × 1080
2k 1440 1.333× 2560 × 1440
4k 2160 2× 3840 × 2160

项目画布不是 1080 高时,scale 按 目标高度 / 项目高度 换算。所以 --quality 是「输出多高」, 与项目本身写的是 1080 还是 720 无关。

自由组合

--quality 与 --fps 可任意组合,不被档位绑死:

node <skill>/scripts/render_video.mjs . --quality 2k --fps 60 --shutter 180 --workers 8

2. 质量 / 速度 / 体积的权衡

三个旋钮各管一件事,互不重叠:

旋钮 影响画面 影响体积 影响耗时
--quality(分辨率) 清晰度 ↑↑ ↑↑(像素平方)
--fps(帧率) 流畅度(运动连贯) ↑ ↑(帧数线性)
--shutter(运动模糊) 运动段不重影 几乎不变 ↑↑(直到"动帧") —— 兜底判据阈值=0,见 §3
--crf / --preset 编码失真 ↑↓ ↑(preset 越慢编码越久)
截图模式(png/png-fast/jpeg) 中间帧精度 中间帧体积 ↑↑↑(最大杠杆)
--workers(并行) 无 无 ↓(≈ 核数上限)

要点:

  • 中间帧编码是最大的耗时项(满幅照片 / 复杂合成帧上,PNG 编码能占 96% 的帧时间,且与并发无关)。
    • --png-fast(★默认推荐):逐像素无损。本机实测纯 CSS/MG 图形帧上与 JPEG q95 同速 (6 进程并行 ×1.02;体积还更小,0.10 vs 0.12 MB/帧);照片帧口径约 4.4×。 「慢 3×、占盘 8×」只对照片/高熵帧成立。
    • --jpeg --jpeg-quality 95:含满幅照片 / 重合成帧、或 4K 终稿时的首选 —— 照片帧上约 13× 提速、 体积约 1/5;q95 实测 PSNR 41.65 dB,已低于 x264 crf18 自身的失真,成片里不可观测。
  • preset 只影响编码,不影响画质:medium→slow/veryslow 是省体积换时间,不是变清晰。 要明显提升清晰度请调 --quality 或降 --crf。
  • 运动模糊不加体积:它只改「动帧」的像素,被判为静帧的帧原样落盘,码率几乎不变, 但观感提升明显。注意:省下的是体积,不是时间 —— 时间代价见 §3。
  • --workers 只降时间:不改变任何像素输出(帧是确定性的、按帧号分派)。

3. 快门运动模糊的原理

不是 blur 滤镜,是线性光下的多样本积分。 一个 180° 快门意味着曝光窗 = 半个帧周期:

openS = (shutter / 360) / fps        # 曝光窗长度(秒)
t_i   = t + openS * (i/K - 0.5)      # 在这一窗内均匀取 K 个采样时刻

对每个「动帧」,渲染器在这些时刻各截 1 张,共 K 张(默认 --samples 8), 再由 blur_integrate.py 在线性光下逐像素平均:

  1. sRGB → 线性光(用 uint16 LUT,不是每像素 pow);
  2. 累加到 uint32;
  3. 除以样本数、截回 uint16。

为什么必须在线性光下平均:sRGB 是伽马压缩过的,直接平均等于在非线性域做插值, 暗部会偏亮、亮部会偏暗,快速横移的边缘会出现「灰边」。线性光平均才是物理上正确的运动模糊。

三级收敛:不把快门成本花在不关键的帧上(v2.0.3)

快门的代价是「每个动帧 K 次截图 + K 张样本落盘 + 一次积分」。而一部片子里真正需要拖影的 往往只有几处运镜,其余是静态排版或入场动画的尾段。三级闸门从粗到细把它们摘出去:

级别 开关 粒度 判据
① 场景级 --shutter-only <id,id> 一个场景 白名单:只有列出的场景开快门,其余走零成本单张(不采样、不落盘、不积分)
② 帧级(可靠) --motion-hold <px>(默认 1) 一帧 页面导出 window.__motion(t0,t1) 时,位移 < N 个设备像素的帧直接单张落盘
③ 帧级(兜底) 自动,无开关 一帧 没有 __motion 时退回 first.equals(last) 逐字节比对

★ ② 和 ③ 差一个数量级。 ③ 的阈值等于 0 —— 快门窗口只有 (shutter/360)/fps ≈ 16.7 ms, 任何亚像素级抗锯齿差异都会让两张 PNG 不等。实测一部 31 场 / 8525 帧的片子, ③ 只判掉 ~20% 的帧(那 20% 是真的逐字节完全没动),剩下 80% 全按 8 样本付费, 截图吞吐从 18.4 降至 2.9 帧/秒(≈6.4×)。② 才是「没用到动效就不开快门」的 可靠做法 —— 它的判据是作者声明的真实位移,不是像素巧合。

要省快门成本,就让帧诚实导出 window.__motion(t0, t1)。 写法见 motion-library.md;assets/motion.js 的 screenTravel(c0, c1) 直接返回这段运镜 在屏幕上走了多少 px,套进 __motion 即可。

⚠ 一个反直觉的坑:为了让静态帧「别死在那儿」,很多人会给整场加一次全时长缓慢推镜。 它每帧只走零点几像素(肉眼毫无拖影),但让每一帧都非零 → ②变成无操作、③必然不等 → 整场全额付快门成本。慢推镜本身是闸门能摘掉的那类(< 1 px/帧),前提是你导出了 __motion; 没导出,它在大盘上就等于「全场动帧」。

  • 样本数仍按 --motion-gap(默认 24 px)从位移反推,夹在 --samples-min–--samples-max(默认 4–12)。
  • 反过来:快动作 > 80 px/帧 若不开快门,会重影成串(每帧是一个瞬间,人眼补不出来)—— 这是唯一真正非开不可的场景,也正是 --shutter-only 该点名的场景。

跳过机制与硬切

被判定为 hold 的帧不写样本、直接沿用单张 —— 元数据里分别记作 still_frames(总数)· motion_holds(② 判掉的)· shutter_frames(真正走了快门的帧数)· shutter_scenes(① 白名单,null = 全片开)。这些帧在成片里是干净的硬切 (切场景的那一帧就该是硬的,不该被「糊」)。

看日志核对(截图结束时会打印两行):

截图完成:8525 帧 / 41230 次截图 · 其中 1830 帧是 hold(位移闸门 0 + 逐字节 1830) · 耗时 512s · 16.6 帧/秒
快门覆盖面:8525/8525 帧走快门采样(100.0%) —— 其余 0 帧是零成本单张,不产生采样也不进积分

位移闸门 0 是告警信号:说明这片子的帧没导出 window.__motion,② 完全没起作用。 只要 ② 真正生效,快门覆盖面 会明显小于 100%。


4. 并行的原理

多浏览器进程级并行(--workers N)

--workers N 启动 N 个相互独立的 Chromium 进程,按帧号分派(worker i 负责 frame % N == i)。

关键:不是在一个浏览器里开 N 个 page。Chrome 的截图编码在一个浏览器进程内是串行的, 同浏览器开多 page 并发是白挂的(拿不到加速)。必须真的多进程。

日志会明确打印 N 个**独立浏览器**进程级按帧步进 以区别于旧行为。

断点续渲(--resume)

--resume 跳过已经落到磁盘的帧与已经积分的快门目录。判据是「同名帧文件是否存在」 (fs.existsSync),不是 mtime —— 改过帧内容但文件名没变时它不会重渲, 要强制重渲请先把那些帧删掉(或换 --only 点名那几场)。

两条路径都支持(v2.0.6 起对齐):

  • 路径 B(--workers N):把帧展开成清单后过滤掉已存在的帧 / 快门目录;
  • 路径 A(精细 PNG,单浏览器):按全局帧号判同名帧在不在盘上 —— shotOne() 写的本来就是 sc.gf + i + 1,所以判据可以精确到帧; 整场都在盘上时连页面都不加载,直接进入合成。

⚠ v2.0.6 之前,路径 A 的 --resume 是静默 no-op:不报错、也不跳帧 (与 --workers 在 useLegacy 下同样无效同族)。更糟的是看门狗挂死时的提示 恰恰写着「加 --resume 可直接接着跑」—— 等于把人骗进第二次白跑。 而精细 PNG 只能走路径 A,所以这条路径恰恰是最需要续渲的那条。

4K 长片渲到一半挂掉时,重跑加 --resume 即可从断点继续,不重头来。

定期重启浏览器(--recycle N)

每渲 N 帧(默认 150)重启一次浏览器,4K 长片防内存泄漏 / OOM。

积分阶段也并行

blur_integrate.py 用多进程(默认 min(核数-1, 12))跑积分,与截图阶段解耦,同样支持续算。

健壮性(挂死自愈)

  • 每个 setupPage / 截图 / 启动 / 关闭都带超时(HX_SHOT_TIMEOUT_MS,默认 60s);
  • 单帧失败最多重试 3 次(HX_FRAME_RETRY),仍失败则重启浏览器再试;
  • 全局看门狗:若 SHOT_TIMEOUT + 45s(默认 105s)内没有新帧产出,直接退出码 3 并 按路径给出续跑办法,而不是无限挂着。 两条路径都必须按「帧」打点:路径 B 在 doneFrames++ 处,路径 A 在逐帧循环里 pokeWatchdog()。v2.0.6 之前路径 A 一次都没打点,而它的 doneFrames 又只在 整场渲完才累加 → 看门狗量到的其实是「单场耗时」,等价于 「单场超过 105s 即判挂死」。实测 1080p 精细 PNG 约 3.5 帧/秒 ⇒ 任何超过 ~12 秒的场必被误杀, 且丢掉的正是那一场的后半段(见 lessons.md #132)。

5. 基准数据

基准工装会合成一个复杂度可控的项目(N 场 × 每秒动元素),在同机上多档位各渲一遍:

python <skill>/scripts/bench_render.py \
    --out .scratch/bench --scenes 2 --sec 1.0 --fps 30 \
    --configs legacy,draft,balanced,4k30,4k60 [--heavy] [--json]
  • fps = 渲染帧数 / 截图耗时(不含编码、不含积分)——看的是截图吞吐;
  • 相对 legacy 的加速比才是重点,绝对值随机器和帧内容变化很大;
  • --heavy 打开「满幅渐变 + 多重 shadow + 半透明叠加」模拟高熵帧(照片型), 这是 PNG 最慢、--png-fast / --jpeg 收益最大的场景。

★ 先看真实片子的端到端,微基准只用来比档位之间的相对关系。 2026-10-05 一条 3 分 20 秒 / 1080p30 / 5982 帧的纯 CSS/MG 片子(本机 6 浏览器进程、 快门 180°/8 样本只开 3 场),端到端实测:--png-fast(默认)469.2 s ≈ 7 分 49 秒、 --jpeg --jpeg-quality 95 460.7 s(差 +1.85%)。派生:全片平均截图吞吐 16.4 帧/秒、平均单次截图 30.5 ms、快门只占 13.7% 耗时(落在 35.3% 的帧上)。 下面 5.1 的合成项目只有 2 场 × 1.0s,切场 / 预载开销占比过高,绝对吞吐不可外推到整片 —— 拿它比档位、比回归可以,别拿它估一条片子的墙钟。

工装会自动把每个档位的 --fps 同步到项目声明的总帧数(layout._total.total_frames)—— 渲染器采信声明帧数(有意为之,避免帧数分歧导致 QC 假截断),所以工装必须同步改它, 否则 4k60 这种档位会按旧 fps 的帧数渲染、时长少一半,基准就不可比了。

5.1 本机量级(16 核 / Windows / Chrome / 纯 CSS 图形帧)

2 场 × 1.0s、1080p 项目、--workers 由档位决定:

档位 输出 fps 帧数 截图 s 积分 s 总 s 帧/秒 加速比 体积 MB 截图模式
legacy 1920×1080 30 60 21.4 0.0 23.6 2.8 ×1.00 0.38 png
draft 1920×1080 30 60 6.2 0.0 8.0 9.7 ×3.47 0.37 png-fast
balanced 1920×1080 30 60 16.6 7.1 27.5 3.6 ×1.29 0.49 png-fast
4k30 3840×2160 30 60 16.9 5.9 26.4 3.5 ×1.26 0.49 png-fast
4k60 3840×2160 60 120 25.4 8.8 37.5 4.7 ×1.69 0.57 png-fast

三个关键结论:

  1. 并行比分辨率更划算:4k30(4× 像素)的截图吞吐(3.5 帧/秒)几乎追平 1080p 的 balanced(3.6 帧/秒)—— 因为 4K 档配了 8 个浏览器进程。legacy 慢不是因为 1080p 贵,而是因为它是单浏览器串行; 换成多进程后,连 4K60 全片都只要 37.5 s。
  2. draft 是最强的加速杠杆:无快门 + png-fast + 6 进程 = 9.7 帧/秒(×3.47)。 迭代期一律先用 draft 看节奏,定稿再上 balanced / final。
  3. ★ 快门会让「动帧」按 K 倍付费,而「哪些帧算动帧」不能指望自动判定。 (本行旧版写的是「hold 静帧只截 2 张即跳过,所以几乎不加时间」—— 已被 2026-10-05 的实测推翻: 兜底判据 first.equals(last) 的阈值等于 0,一部 8525 帧的片子里只有 ~20% 被判成静帧。) 默认档选 balanced 而非 draft,换来的运动段观感提升仍是成倍的 —— 但要真省下时间, 得靠 --shutter-only / --motion-hold 两级闸门,见 §3。

数字是本机量级,随机器与帧内容(纯 CSS 图形 ↔ 满幅照片)能差 10× 以上。 换机器后重跑同一命令对比自己的基线,别把这张表当承诺值。

5.2 优化前后的根因与修法

这是「同速的前提下多拿到新能力」的依据(同一台机器、同一份 108 帧项目):

指标 优化前 优化后 说明
balanced 总耗时 70.4 s 33.0 s 快 2.1×
其中积分 + 清理 53.6 s 8.5 s 快 6.3×
逐帧并行加速比 — 10.2×(12 进程) 证明并行真有效
样本目录清理 ~121 s(96 文件) 0.62 s 见下

三个根因与修法:

  1. 并行被"删除"拖垮:旧实现把「删样本目录」放在积分的关键路径上, 而本机对用户目录下的批量删除有实时扫描节流(96 个文件要 ~121 s)。 → 把清理移出关键路径;改用整目录 rename 到系统 Temp 再删(0.084 s)。
  2. 积分本身吃内存带宽:逐像素 pow 线性化 + 大量中间数组。 → 换成 uint16 LUT + uint32 累加,数值逐位不变(实测 PSNR 138.1 dB、最大差 0)。
  3. 刚写完的 PNG 被扫描器占用:保存偶发 PermissionError。 → _save_png 带指数退避重试;探测灰度时用步进抽样(rgb[::8,::8])不再 reshape 大数组。

结论:v2.0 的默认档不是「加了功能所以变慢」,而是在同速的前提下多拿到了运动模糊、并行、断点续渲。


6. 4K60 的硬件要求

4K60 的原始工作量是 1080p30 的 4×(像素)× 2×(帧数)≈ 8×,再叠加更大的 PNG 编码,约 8–12×。

但实测墙钟差距远小于此,因为 4K 档配了更多并行进程 —— 本机 4k60(120 帧 / 8 进程)总耗时 37.5 s, 而 1080p30 的 balanced(60 帧 / 6 进程)是 27.5 s,只差 1.4×。换句话说: 并行能在很大程度上吃掉分辨率的代价,4K60 不是「8 倍慢」,而是「多核机器上完全可做」。

读表提醒:基准里各档位的 --workers 不同(legacy 1 / draft·balanced 6 / 4K 8), 所以横向比总耗时是端到端体验,不是"同并行度下的纯分辨率代价"。要比纯分辨率代价,请固定 --workers 再跑。

资源 建议 说明
CPU ≥ 8 物理核(--workers 8 才吃得满) 截图是 CPU 密集;核数 = 并行上限
内存 ≥ 16 GB(长片 32 GB 更稳) 每个 Chromium 进程数百分 MB–1 GB+
磁盘 SSD,预留 10–80 GB 有快门护栏时峰值 ≈ --shutter-flush × 1(默认 ≤8 GB);关掉护栏才会出现"全片之和",见下
浏览器 Chrome / Edge 任一 自动探测;无需额外安装

★ 快门的磁盘峰值:--shutter-flush 护栏(v2.0.2)

快门把每个动帧拆成 --samples(默认 8)张样本图写进 render/shutter/f_XXXXXX/, 积分与清理原本只在全部截图结束之后做一次 —— 于是采样体积在截图阶段单调堆积, 峰值不是「最后一帧」而是全片之和:

不加护栏时的峰值 ≈ 帧数 × 样本数 × 单样本体积  +  帧数 × 单帧体积

实测(1080p / 满屏图形帧):单样本 1.33 MB、最终 JPEG 帧 0.23 MB(PNG 帧 1.6 MB)。 一部 284 s / 8525 帧的片子用默认快门(8 样本)→ 采样约 90 GB、加最终帧 ~104 GB。 两个后果:① 跑到一半 ENOSPC;② 采样涨到 ~20 GB 后速率从 110 帧/分掉到 12 帧/分 (进程活着、日志不报错、chrome 一个不少)—— 看起来像"渲染卡住",其实是磁盘 I/O 在打架。

护栏怎么工作

--shutter-flush <GB>:样本攒到阈值就全体停手做一次「积分 + 清理」,然后一起继续。 峰值从「全片之和」压到「阈值 × 1」。

# 默认按可用磁盘自动取:可用空间的 25%,夹在 1–8 GB
node <skill>/scripts/render_video.mjs . --shutter 180 --shutter-flush 4
node <skill>/scripts/render_video.mjs . --shutter 180 --shutter-flush 0   # 关掉护栏(旧的"全程堆积"行为)
  • 必须是真屏障,不能只等"碰巧大家都空闲":8 个 worker 的帧间空隙只有微秒级, "全部同时空闲"在长片里几乎不会自然发生(第一版护栏整片只触发了一次,等于没护栏)。 现在:攒够阈值 → 举 flushReq → 各 worker 在帧边界停手 → 等 active === 0 且全员到齐 → 由最后一个到齐的人执行积分 → 统一放行。带 FLUSH_WAIT_MS 超时兜底, 凑不齐就放行退回堆积 —— 宁可慢,不可死锁。
  • 为什么必须全体停手:blur_integrate.py 的收尾会把 shutter 根目录整个 rename 到 Temp 再删 (0.08 s,比逐个文件删快两个数量级)→ 期间绝不能有新样本落盘,否则连新样本一起删; 也不能有"写了一半的目录",否则会拿残缺样本积分、产出错误的模糊。
  • 护栏只在并行路径(--workers N)生效;单浏览器路径会打警告。
  • 实况写进 out/<slug>.render.json:shutter_flush_bytes / shutter_flushes / shutter_peak_bytes —— 事后核对"到底堆了多少"看它,不用猜。
  • 护栏不增加总积分工作量(总帧数不变),只是把原来集中在片尾的一次积分摊到途中; 代价是每次积分会暂停截图(本机 8 帧约 5 s;一次 276 帧约 109 s)。

⚠ 看门狗联动:积分期间没有新帧产出,而默认看门狗 105 s 内无新帧就判挂死退出 —— 一次 276 帧的积分要 109 s,不喂看门狗的话第一次积分就会被杀掉。渲染器已在积分前后 pokeWatchdog()。任何"长时间不产新帧但确实在干活"的新阶段都要记得喂它。

成本对照(同一部 8525 帧片子,本机 i7-10700 / 16 逻辑核)

配置 截图吞吐 全片耗时 中间产物峰值
--shutter 180(8 样本,无护栏) 1.8 → 0.2 帧/秒(越跑越慢) ~10 小时(且写满盘) ~104 GB → 中途 ENOSPC
--shutter 180 --shutter-flush 4 由积分占比决定 截图 + 全部积分串行 ≤ 4 GB
--shutter 0 --jpeg q95 ★ 18.4 帧/秒 9 分 7 秒 1.9 GB

判据:先算峰值再对 df。不够时按代价从小到大选:① --shutter 0(丢掉运动模糊, 但 1080p30 的 MG 图形帧上肉眼几乎无感,且提速 8× 以上);② 降 --samples; ③ 调小 --shutter-flush;④ 分场景批渲。 排查顺序:先看 du -sh render/shutter 和 df,再怀疑代码。

4K 强制建议:

  • 中间帧用 --jpeg --jpeg-quality 95 或 --png-fast —— 4K 精细 PNG 会把磁盘和编码时间吃干; q95 的失真低于 crf16 自身,看不出。
  • 一定加 --resume —— 4K60 长片中途失败概率不低,续渲省掉整轮重跑。
  • 一定加 --recycle 150(或更小) —— 4K 长片不重启浏览器容易 OOM。
  • --workers 别超过物理核数 —— 超了会互相抢 CPU 反而更慢。
# 4K60 终稿:JPEG 中间帧 + 续渲 + 定期重启
node <skill>/scripts/render_video.mjs . --profile final \
     --jpeg --jpeg-quality 95 --workers 8 --recycle 120 --resume \
     --audio audio/narration-full.mp3

7. 环境变量(渲染健壮性)

变量 默认 作用
HX_SHOT_TIMEOUT_MS 60000 单次截图/启动/关闭的超时;挂死机器可调小
HX_FRAME_RETRY 3 单帧最大重试次数(超过则重启浏览器)

浏览器挂死的排查顺序:调小 HX_SHOT_TIMEOUT_MS → 降 --workers → 加 --resume 重跑。


8. 选档速查

你现在要干嘛 用什么
快速看节奏、迭代画面 --profile draft --preview N
出正式 1080p 成片 --profile balanced --audio …
要 2K/60 --quality 2k --fps 60 --shutter 180
出 4K60 终稿 --profile final --jpeg --jpeg-quality 95 --workers 8 --recycle 120 --resume
像素级对比旧版 --profile legacy
验证本机性能 python scripts/bench_render.py --configs legacy,draft,balanced