Skip to content

fix(world): stop the camera teleporting and the frame budget losing frames - #152

Merged
LeadcodeDev merged 1 commit into
chantier/audit-remediationfrom
fix/world-transition-frame-accounting
Aug 8, 2026
Merged

fix(world): stop the camera teleporting and the frame budget losing frames#152
LeadcodeDev merged 1 commit into
chantier/audit-remediationfrom
fix/world-transition-frame-accounting

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Round 3 du chantier d'audit — lot transitions & vue world, 8 constats vérifiés.

C'est le lot le plus visible à l'œil : la vue world est le seul mécanisme censé produire un plan continu, et chacun des défauts ci-dessous se voit à la lecture.

ConstatSévéritéSymptôme
1CriticalUne transition plus longue que la scène sortante supprime des frames de la scène entrante. La scène sortante ne peut pas dépenser plus de frames qu'elle n'en a, mais le budget était calculé comme si — le dépassement était prélevé sur la suivante.
5Highcamera_pan_duration supérieur à une durée de scène téléporte la caméra.
2HighLe fond de la scène sortante reste à pleine intensité pendant tout le pan, puis disparaît en une frame.
3HighLe cross-fade fait tomber la frame entière à 50 % d'opacité au milieu de chaque pan (les deux scènes à 0,5, rien derrière).
4Highpersist: truerepasse l'opacité à 1.0 alors que la scène est en plein fondu sortant.
6MediumUne transition entre deux vues duplique la frame de jonction de chaque côté.
7Mediumscene.freeze_at n'est lu par aucun chemin de code world.
8Mediumscene.effects (post-effets) est ignoré sur les frames world et sur les transitions de vues.

Mesures avant / après

Chaque chiffre vient d'un test ajouté dans cette PR, pas d'une estimation.

ConstatAvantAprès
5 — saut caméra inter-frame245,33 px (frame 46)21,33 px
2 — saut d'avg-luma du fond109,0 (frames 71→72)≤ 4,82
3 — opacité au milieu du panplancher à 0,5000,670
4 — discontinuité persist0,3260,034

Notes d'implémentation

  • Le budget de frames sortantes est borné par le nombre de frames que la scène possède réellement (actual_outgoing_transition), et la comptabilité est vérifiée frame par frame : chaque index local de la scène entrante doit être rendu exactement une fois.
  • ViewTransition utilise désormais une progression en intervalle ouvert, ce que SlideTransition garantissait déjà — d'où la disparition de la frame dupliquée.

Vérification

cargo test -p rustmotion sur cette branche seule : 144 tests, 0 échec.

…rames
Eight confirmed audit findings on scene transitions and the `world` view —
the one mechanism meant to read as a single continuous shot, and the one
where every defect below is plainly visible on screen.
- A transition longer than the scene it leaves silently deleted frames from
the *entering* scene. The outgoing scene cannot spend more frames than it
has, but the budget was computed as if it could, and the overrun was
taken out of the next scene. Now clamped to the outgoing scene's own
frame count, with the accounting asserted frame by frame.
- `camera_pan_duration` longer than a scene teleported the camera:
measured jump of 245.33px between two consecutive frames, now 21.33px.
- The outgoing scene's background held at full strength through the whole
pan, then vanished in one frame — an avg-luma step of 109.0 between two
frames, now at most 4.82.
- The scene cross-fade drove the *whole* frame to 50% opacity at every pan
midpoint (both scenes at 0.5, nothing behind them). Mid-pan opacity is
now 0.670 instead of sitting on that floor.
- `persist: true` snapped opacity back to 1.0 while the scene was still
fading out — a 0.326 discontinuity, now 0.034.
- A view-to-view transition duplicated the junction frame on both sides.
`ViewTransition` progress is now an open interval, matching what
`SlideTransition` already guaranteed.
- `scene.freeze_at` was read by no world code path at all.
- `scene.effects` (post-effects) were dropped on world frames and on view
transition frames.
Every figure above is a before/after measurement from the tests added here,
not an estimate.
Tests: 144 pass on this branch alone.
@LeadcodeDevLeadcodeDev added the bug Something isn't working label Aug 8, 2026
@LeadcodeDevLeadcodeDev self-assigned this Aug 8, 2026
@LeadcodeDev
LeadcodeDev merged commit 72289ca into chantier/audit-remediationAug 8, 2026
3 checks passed
LeadcodeDev added a commit that referenced this pull request Aug 10, 2026
…rames (#152)
Eight confirmed audit findings on scene transitions and the `world` view —
the one mechanism meant to read as a single continuous shot, and the one
where every defect below is plainly visible on screen.
- A transition longer than the scene it leaves silently deleted frames from
the *entering* scene. The outgoing scene cannot spend more frames than it
has, but the budget was computed as if it could, and the overrun was
taken out of the next scene. Now clamped to the outgoing scene's own
frame count, with the accounting asserted frame by frame.
- `camera_pan_duration` longer than a scene teleported the camera:
measured jump of 245.33px between two consecutive frames, now 21.33px.
- The outgoing scene's background held at full strength through the whole
pan, then vanished in one frame — an avg-luma step of 109.0 between two
frames, now at most 4.82.
- The scene cross-fade drove the *whole* frame to 50% opacity at every pan
midpoint (both scenes at 0.5, nothing behind them). Mid-pan opacity is
now 0.670 instead of sitting on that floor.
- `persist: true` snapped opacity back to 1.0 while the scene was still
fading out — a 0.326 discontinuity, now 0.034.
- A view-to-view transition duplicated the junction frame on both sides.
`ViewTransition` progress is now an open interval, matching what
`SlideTransition` already guaranteed.
- `scene.freeze_at` was read by no world code path at all.
- `scene.effects` (post-effects) were dropped on world frames and on view
transition frames.
Every figure above is a before/after measurement from the tests added here,
not an estimate.
Tests: 144 pass on this branch alone.
LeadcodeDev added a commit that referenced this pull request Aug 11, 2026
)
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.
@LeadcodeDev
LeadcodeDev deleted the fix/world-transition-frame-accounting branch August 11, 2026 09:44
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugSomething isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@LeadcodeDev