portrait: the mirror clears the stage latch, so a reopened window restages
`Booth::staged` records which pane's bake a booth holds, and only `stage_pane_look` ever wrote it. Nothing cleared it, so a booth that fell back to mirroring the live unit still claimed to hold the pane's bake. That is a stale latch with a delay fuse. A pane's bake lives as long as its room, and every pane numbers its own bakes from 1, so a window that closes and reopens brings the same pane back at revision 1 — meeting a latch that still said `(that pane, 1)` from its previous life. `needs_restage` read that as "already current" and the window came back showing whatever the mirror had left in the booth. It was the same shape as the pane-to-pane collision the latch already guards, one lifetime further out. Clearing on the way through the mirror can only cost one extra restage. Also adds `[booth] pane geom`, a `WOW_BOOTH_LOG` line per pooled slot whenever the aspect its camera is framed through, the square bake it renders into, or the square the UI samples changes — the three numbers that decide whether a pane is drawn un-stretched, and the ones a pane whose content and framing both check out must be disagreeing on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
@@ -847,6 +847,9 @@ fn resize_pane_targets(
|
||||
sizes: Res<PaneBakeSizes>,
|
||||
mut booths: ResMut<Booths>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
// 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<HashMap<&'static str, (u32, u32, u32)>>,
|
||||
) {
|
||||
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)]
|
||||
|
||||
Reference in New Issue
Block a user