Repository navigation
Description
The three.js adapter docs recommend registering a promise on window.__hf.buildReady for setup the
runtime cannot see (building meshes, compiling shaders, baking data). When that promise rejects, the
runtime treats the hold as released: the render runs every frame, writes the file, reports "Render
complete" and exits 0. The rejection reason is not logged anywhere. snapshot saves frames and check
reports 0 errors.
In a real three.js film the scene was never built, so every frame is empty or broken, and a pipeline that
trusts the exit code ships it. We hit this with WebGPU: with --no-browser-gpu three silently falls back
to WebGL 2, our build check rejects the hold, and the render still exits 0 with a blank 42 s film.
(data-requires-webgpu on the root does fail the render, exit 1 - but only for that one cause.)
Minimal repro
index.html (no dependencies):
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<script>
window.__hf = window.__hf || {}; window.__hf.buildReady = window.__hf.buildReady || {};
window.__hf.buildReady.scene = new Promise((res, rej) => { window.__sceneReady = res; window.__sceneFail = rej; });
</script>
<style>
html, body { margin: 0; width: 1920px; height: 1080px; background: #000; }
#c { position: absolute; inset: 0; width: 1920px; height: 1080px; }
</style>
</head>
<body>
<div id="root" data-composition-id="main" data-start="0" data-duration="2"
data-width="1920" data-height="1080" data-no-timeline>
<canvas id="c" width="1920" height="1080"></canvas>
</div>
<script>
const ctx = document.getElementById("c").getContext("2d");
// the build fails (here: bad baked data); the hold rejects with a reason
try { JSON.parse("{not json"); window.__sceneReady(); }
catch (err) { window.__sceneFail(new Error("build failed: " + err.message)); }
addEventListener("hf-seek", (e) => {
ctx.fillStyle = "#e0893c"; ctx.fillRect(0, 0, 1920, 1080);
ctx.fillStyle = "#000"; ctx.font = "120px sans-serif"; ctx.fillText("t = " + e.detail.time.toFixed(2), 200, 600);
});
</script>
</body>
</html>npx hyperframes@0.8.157 check # 0 errors, 0 warnings, exit 0
npx hyperframes@0.8.157 render --no-best-effort -o out.mp4
echo $? # 0, "Render complete", 2.0 s video
The log contains no line with "build failed", "reject" or "buildReady".
(If the build fails because an asset 404s, check does report http_error, but render still exits 0.
The repro above uses a failure with no network error, which is the common case: a parse error, a missing
GPU feature, a shader that does not compile, a font that does not load.)
Expected
A rejected build hold fails render and snapshot with a non-zero exit and the rejection reason in the
output (at least under --no-best-effort; arguably always - there is no useful output from a composition
that says its own build failed). check could report it as an error too, since it loads the page.
Environment
hyperframes 0.8.157 and 0.8.156 · macOS 26 (Darwin 25.6), Apple M1 Max · Node 26.9.0 ·
chrome-headless-shell 152.0.7977.30 · hardware GPU and --no-browser-gpu both affected.
Workaround we use
Wrap the seek handler and paint a loud colour when the build failed, then check rendered frames for it:
if (!built) { clear to magenta; return; }.
Activity
fix(runtime): fail readiness waits when a buildReady promise rejects
miguel-heygen commentedon Oct 24, 2026
Thanks for the minimal repro, it made this quick to confirm.
Reproduced on 0.8.193 (Linux, your
index.htmlunchanged):checkreports 0 errors and passes, andrender --no-best-effortexits 0 with a 2 s video. Nothing in the render log mentions the rejection. The cause is that the runtime treats a rejectedwindow.__hf.buildReadypromise as settled and drops the reason.#5746 fixes this: a rejected hold fails
render,snapshotandcheck, and the message names the key and the rejection reason. It's approved and waiting only on commit signing. Until it ships, your magenta-frame check is a reasonable workaround.fix(runtime): fail readiness waits when a buildReady promise rejects
fix(runtime): fail readiness waits when a buildReady promise rejects
fix(runtime): fail readiness waits when a buildReady promise rejects
fix(runtime): fail readiness waits when a buildReady promise rejects
fix(runtime): fail readiness waits when a buildReady promise rejects (h…