fix(render): make freeze_at impossible for a render path to forget - #171
Merged
Conversation
Closes #164. PR #152 fixed a real bug — `scene.freeze_at` was read by no code path in the `world` view — by adding a fifth hand-written copy of the clamp "for parity with the other four". The cause it left untouched was that no single place computes a scene's render time, so every new render path has to remember to redo it. Writing the parameterized test first, before any fix, paid for itself twice. Four of the five paths agreed; the fifth did not, and the reason was not the cosmetic `.min()`-versus-`if` drift already noted on the issue: `render_world_frame_scaled` painted the active scene's animated background from the raw world clock, never clamped. PR #152's fifth copy was itself incomplete, and nothing had caught it. `RenderContext::time` is now a `SceneTime` rather than an `f64`. Its single field is private to a submodule and its only two constructors both apply the clamp, so it cannot be built anywhere else — verified by trying: error[E0423]: cannot initialize a tuple struct which contains private fields Since `render_with_new_pipeline` — the only function that paints a scene's component tree — requires a `&RenderContext`, a sixth render path is held to the clamp by the compiler rather than by discipline. That does not stop someone writing `frame_index as f64 / fps as f64` from scratch, but it removes the failure mode actually observed here: copying an existing path for parity. Every path left to copy now goes through `SceneTime`. Two findings worth recording rather than burying: - `time_scale`/`time_offset` were already available on `card`, `flex`, `grid`, `container` and `positioned` — PR #64 generalized them long before this chantier, with the affine composition, its tests and its rule documentation. Nothing here needed to change. - `freeze_at` does not need to enter that composition. Because `time_scale` is validated strictly positive, every composed remap is monotonic, so clamping the global time before composing is equivalent to clamping after, at any nesting depth. A test pins the brief's own example — a `card` at `time_scale: 2` over a `flex` at `time_offset: -1` — rather than leaving that as an argument. Not covered: a world crossfade with two scenes visible keeps the raw world clock for the background. Freezing it means deciding which scene wins during the transition, which is a design question, not an oversight; the comment says so where it applies.
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.
Closes #164.
PR #152 fixed a real bug —
scene.freeze_atwas read by no code path in theworldview — by adding a fifth hand-written copy of the clamp, "for parity with the other four". The cause it left untouched: no single place computes a scene's render time, so every new render path has to remember to redo it.Writing the test first paid for itself twice
The brief asked for a parameterized test across all five render paths, written before any fix — none existed, since #152 tested only the path it was repairing. Either outcome would have been informative.
Four of the five paths agreed. The fifth did not, and not for the cosmetic
.min()-versus-ifdrift already noted on the issue:render_world_frame_scaledpainted the active scene's animated background from the raw world clock, never clamped — only the scene tree and camera were. PR #152's fifth copy was itself incomplete, and nothing had caught it.The structural answer
RenderContext::timeis now aSceneTime, not anf64. Its single field is private to a submodule, and its only two constructors both apply the clamp. It cannot be built anywhere else — verified by trying rather than asserting:render_with_new_pipeline— the only function that paints a scene's component tree — requires a&RenderContext. A sixth render path is therefore held to the clamp by the compiler, not by discipline.To be precise about what that does and does not buy: it does not stop someone writing
frame_index as f64 / fps as f64from scratch. It removes the failure mode actually observed here — copying an existing path for parity. Every path left to copy now goes throughSceneTime.Two findings worth recording
My brief's premise was wrong, and the agent checked instead of complying. I wrote that
time_scale/time_offsetwere confined tocardandflex. They are oncard,flex,grid,containerandpositioned— PR #64 generalized them long before this chantier, with the affine composition, its tests andrules/time-remapping.md. Nothing here needed to change, and nothing was changed. (My preparation grepped only the two files I already suspected: I confirmed a hypothesis instead of testing it.)freeze_atdoes not belong in that composition. Becausetime_scaleis validated strictly positive, every composed remap is monotonic, so clamping global time before composing is equivalent to clamping after, at any nesting depth. That is whybox_builder.rsneeded no change. A test pins the brief's own example — acardattime_scale: 2over aflexattime_offset: -1— rather than leaving it as an argument.Verification
cargo test --workspace: 25 targets, 1033 tests, 0 failurescargo fmt --all --checkandcargo clippy --workspace --all-targets -- -D warnings: cleanmain(and declares none offreeze_at/time_scale/time_offset)Not covered
freeze_atas a field on containers, the literal reading of "time container on any node". Doing it properly means carrying the freeze intoBuiltScene.time_params, consumed bylegacy_dispatch.rs, outside this change's scope. Half-doing it — freezing resolved CSS but not internal animations like counters and typewriters — would be worse than not doing it.--strict-animsamples pastfreeze_at.grep -n freeze_atincommands/geometry.rsreturns nothing: it can evaluate a transform at an instant the render never reaches. Pre-existing, unrelated to this change, and that file was read-only here. Suggested patch, not applied: