Found while re-scoring the Remotion differential. This is a weakness in a chantier fix, not a pre-existing defect.
The finding
PR #152 fixed a real bug — scene.freeze_at was read by no code path in the world view. But it fixed it by adding a fifth copy of the same logic rather than unifying. The comment it left says so itself:
// crates/rustmotion/src/engine/render/scene.rs:958
// Apply freeze_at (parity with the other four render paths — …)
The five sites, all in crates/rustmotion/src/engine/render/scene.rs:
| Line |
Form |
| 138 |
if let Some(freeze_at) = scene.freeze_at { if time > freeze_at { time = freeze_at } } |
| 670 |
same |
| 966 |
anim_time = anim_time.min(freeze_at) |
| 1119 |
same as line 138 |
| 1180 |
same as line 138 |
Why it matters
The bug PR #152 fixed will recur. Its root cause was not "we forgot freeze_at in the world view"; it was "there is no single place where freeze_at applies, so every new render path has to remember to redo it". That cause is intact. A sixth render path — and the chantier added one, render_scene_frame_scaled_with_prev_bg — will start out with the same omission, and nothing will flag it.
One of the five copies has already drifted in form. Line 966 uses .min() where the other four use an if. The result is equivalent today, but that is exactly the signature of a duplication starting to diverge — and no test asserts that the five paths treat freeze_at identically.
Direction
Apply freeze_at once, where scene time is computed, before it reaches the render paths — rather than inside each one. The exact shape depends on how each path derives its time; if no common point exists yet, creating one is precisely the work.
This is also the first brick of the Remotion gap "generic time container on any node": freeze_at (scene), time_scale and time_offset (today restricted to card and flex) are three facets of the same local time remap, each wired separately. Unifying them closes this debt and opens that gap at once.
Suggested verification
A test parameterized over the five render paths, asserting that at time > freeze_at each produces the frame at freeze_at. None exists today: PR #152 tested only the world path, the one it was repairing.
Found while re-scoring the Remotion differential. This is a weakness in a chantier fix, not a pre-existing defect.
The finding
PR #152 fixed a real bug —
scene.freeze_atwas read by no code path in theworldview. But it fixed it by adding a fifth copy of the same logic rather than unifying. The comment it left says so itself:The five sites, all in
crates/rustmotion/src/engine/render/scene.rs:if let Some(freeze_at) = scene.freeze_at { if time > freeze_at { time = freeze_at } }anim_time = anim_time.min(freeze_at)Why it matters
The bug PR #152 fixed will recur. Its root cause was not "we forgot
freeze_atin the world view"; it was "there is no single place wherefreeze_atapplies, so every new render path has to remember to redo it". That cause is intact. A sixth render path — and the chantier added one,render_scene_frame_scaled_with_prev_bg— will start out with the same omission, and nothing will flag it.One of the five copies has already drifted in form. Line 966 uses
.min()where the other four use anif. The result is equivalent today, but that is exactly the signature of a duplication starting to diverge — and no test asserts that the five paths treatfreeze_atidentically.Direction
Apply
freeze_atonce, where scene time is computed, before it reaches the render paths — rather than inside each one. The exact shape depends on how each path derives itstime; if no common point exists yet, creating one is precisely the work.This is also the first brick of the Remotion gap "generic time container on any node":
freeze_at(scene),time_scaleandtime_offset(today restricted tocardandflex) are three facets of the same local time remap, each wired separately. Unifying them closes this debt and opens that gap at once.Suggested verification
A test parameterized over the five render paths, asserting that at
time > freeze_ateach produces the frame atfreeze_at. None exists today: PR #152 tested only the world path, the one it was repairing.