From f34e7b4c4be22d77d3c883823273cec8943cbf16 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Fri, 23 Oct 2026 15:57:41 +0200 Subject: [PATCH] fix(preview): rotate shared-texture slots so a just-released one is rewritten last SlotBook::claim took the lowest free slot, so the slot Chromium had just released was rewritten first while its GPU could still be copying it into the canvas. At 60 Hz the ring ping-ponged between two slots and a slot was reused ~50 ms after publish, the gap reported in #1301. Search round-robin from the slot after the last claim instead; the 40 ms release delay stays. Refs #1301 --- crates/compositor/src/shared_frames.rs | 46 +++++++++++++++++++------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/crates/compositor/src/shared_frames.rs b/crates/compositor/src/shared_frames.rs index cee0946187..c853899f05 100644 --- a/crates/compositor/src/shared_frames.rs +++ b/crates/compositor/src/shared_frames.rs @@ -60,24 +60,26 @@ pub struct SlotBook { ready: Option, /// Les cases prises par JS, que Chromium n'a pas encore relâchées, plus anciennes d'abord. held: Vec, + /// La case après la dernière prise : la recherche d'une case libre part de là. + next: u32, } impl SlotBook { - /// La case où écrire la prochaine frame. Une case libre d'abord ; à défaut, celle de la - /// frame prête que personne n'a prise, retirée puisqu'elle est périmée. `None` quand - /// Chromium tient toutes les cases : une case tenue n'est jamais réécrite, même perdue - /// (`lost`), puisqu'il peut encore la lire. + /// La case où écrire la prochaine frame. Une case libre d'abord, à tour de rôle à partir + /// de la dernière prise : la case que Chromium vient de relâcher est la dernière réécrite, + /// pas la première, puisque son GPU peut encore la copier dans le canvas (#1301). À défaut, + /// celle de la frame prête que personne n'a prise, retirée puisqu'elle est périmée. `None` + /// quand Chromium tient toutes les cases : une case tenue n'est jamais réécrite, même + /// perdue (`lost`), puisqu'il peut encore la lire. pub fn claim(&mut self, slots: u32) -> Option { let ready = self.ready.map(|frame| frame.slot); let is_held = |slot: u32| self.held.iter().any(|held| held.slot == slot); - if let Some(free) = (0..slots).find(|slot| !is_held(*slot) && Some(*slot) != ready) { - return Some(free); - } - if let Some(stale) = ready { - self.ready = None; - return Some(stale); - } - None + let slot = (0..slots) + .map(|i| (self.next + i) % slots) + .find(|slot| !is_held(*slot) && Some(*slot) != ready) + .or_else(|| self.ready.take().map(|frame| frame.slot))?; + self.next = (slot + 1) % slots; + Some(slot) } /// Une case est tenue depuis `LOST_SLOT_AFTER` : sa libération ne viendra plus. @@ -281,6 +283,26 @@ mod tests { assert_eq!(book.take(0, now), Some(frame(1, 0))); } + #[test] + fn a_released_slot_waits_its_turn_while_others_are_free() { + let now = Instant::now(); + let mut book = SlotBook::default(); + // Régime établi : chaque frame est prise par JS, et Chromium relâche la précédente. + let mut previous = None; + for gen in 1..=3 { + let slot = book.claim(RING_SLOTS).unwrap(); + book.publish(frame(gen, slot)); + book.take(gen - 1, now); + if let Some(slot) = previous { + book.release(slot, gen - 1); + } + previous = Some(slot); + } + // Les cases déjà relâchées peuvent encore être lues par le GPU de Chromium (#1301) : + // la suivante va plus loin dans l'anneau au lieu de reprendre la plus petite. + assert_eq!(book.claim(RING_SLOTS), Some(3)); + } + #[test] fn a_taken_slot_is_never_written_until_released() { let now = Instant::now();