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.

Constat Sévérité Symptôme
1 Critical Une 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.
5 High camera_pan_duration supérieur à une durée de scène téléporte la caméra.
2 High Le fond de la scène sortante reste à pleine intensité pendant tout le pan, puis disparaît en une frame.
3 High Le 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).
4 High persist: true repasse l'opacité à 1.0 alors que la scène est en plein fondu sortant.
6 Medium Une transition entre deux vues duplique la frame de jonction de chaque côté.
7 Medium scene.freeze_at n'est lu par aucun chemin de code world.
8 Medium scene.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.

Constat Avant Après
5 — saut caméra inter-frame 245,33 px (frame 46) 21,33 px
2 — saut d'avg-luma du fond 109,0 (frames 71→72) ≤ 4,82
3 — opacité au milieu du pan plancher à 0,500 0,670
4 — discontinuité persist 0,326 0,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.
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Aug 8, 2026
@LeadcodeDev LeadcodeDev self-assigned this Aug 8, 2026
@LeadcodeDev
LeadcodeDev merged commit 72289ca into chantier/audit-remediation Aug 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant