diff --git a/crates/benilla-app/src/portrait/mod.rs b/crates/benilla-app/src/portrait/mod.rs index c7ae8d39..792d7e55 100644 --- a/crates/benilla-app/src/portrait/mod.rs +++ b/crates/benilla-app/src/portrait/mod.rs @@ -847,6 +847,9 @@ fn resize_pane_targets( sizes: Res, mut booths: ResMut, mut images: ResMut>, + // The last `(aspect, want, have)` logged per slot, so `WOW_BOOTH_LOG` prints a line per + // change rather than one a frame. + mut geom: Local>, ) { for slot in UNIT_PANE_SLOTS { // Only a pane that is on screen may size its bake. @@ -859,6 +862,19 @@ fn resize_pane_targets( let have = images .get(&booth.target) .map(|image| image.texture_descriptor.size.width); + // The three numbers that decide whether a pane is drawn un-stretched: the aspect the + // camera is framed through, the square bake it renders into, and the square the UI + // actually samples. A model that looks stretched or over-zoomed in a pane whose content + // and framing both check out is these disagreeing. + if booth_log() { + let seen = (booth.aspect.to_bits(), want, have.unwrap_or(0)); + if geom.insert(slot, seen) != Some(seen) { + eprintln!( + "[booth] pane geom slot={slot} aspect={:.4} want={want} have={have:?}", + booth.aspect, + ); + } + } if have.is_none_or(|have| have == want) { continue; } @@ -2425,7 +2441,8 @@ fn stage_pane_look( booth.staged = Some((pane, bake.revision)); if booth_log() { eprintln!( - "[booth] pane stage slot={slot} pane={pane:?} rev={} display={} parts={}", + "[booth] pane stage slot={slot} pane={pane:?} rev={} display={} parts={} \ + aspect={aspect:.4}", bake.revision, bake.display_id, bake.parts.len(), @@ -2550,6 +2567,17 @@ fn sync_body_booth( let Some(booth) = booths.0.get_mut(slot) else { return; }; + // Reaching the mirror means this booth is no longer holding a pane's own bake, so the stage + // latch has to say so. It is the mirror image of what [`stage_pane_look`] does on the way in, + // where staging clears `snap`/`baked`. + // + // Leaving it set is a stale-latch bug with a delay fuse: a pane's bake lives only as long as + // its room ([`crate::entities::attach::build_pane_previews`]) and every pane numbers its own + // bakes **from 1**, so a pane whose window closes and reopens comes back at revision 1 — and + // if the booth still claimed `(that pane, 1)` from its previous life, `needs_restage` read + // "already current" and the window reopened showing whatever the mirror had left in the + // booth. Clearing here can only ever cost one extra restage, which is the safe direction. + booth.staged = None; // Latch the pane's aspect while it is drawn. let aspect = panes.0.get(slot).copied().unwrap_or(booth.aspect); // The show edge (`0x505d00`): `BoothPanes` publishes a slot exactly while its pane is drawn. @@ -3718,6 +3746,24 @@ mod stage_latch_tests { let (pane, _) = two_panes(); assert!(needs_restage(None, pane, 1, 1.0, 1.0)); } + + /// The second half of the handover, and the reason [`super::sync_body_booth`] clears the + /// latch: a pane's bake lives only as long as its room, and every pane numbers its bakes from + /// 1. A window that closes and reopens brings the *same* pane back at revision 1, so a latch + /// still holding that pane's revision 1 from its previous life reads as "already current" + /// while the booth actually holds whatever the mirror left in it. Cleared, it is fresh again. + #[test] + fn the_same_pane_restages_after_the_mirror_clears_the_latch() { + let (pane, _) = two_panes(); + assert!( + !needs_restage(Some((pane, 1)), pane, 1, 1.0, 1.0), + "left set, a reopened window's first revision looks current — the bug" + ); + assert!( + needs_restage(None, pane, 1, 1.0, 1.0), + "cleared on the way through the mirror, the same pane and revision restage" + ); + } } #[cfg(test)]