@@ -1096,6 +1096,22 @@ type PendingPrefetch = (usize, std::sync::mpsc::Receiver<Result<PrefetchedClip>>
10961096/// décodeurs ouvertes plus longtemps que nécessaire.
10971097const PREFETCH_LEAD_SEC : f64 = 0.75 ;
10981098
1099+ /// Durée pendant laquelle la boucle continue de recomposer après un changement en pause, le
1100+ /// temps qu'un effet asynchrone (segmentation webcam) livre son résultat. Généreuse : à
1101+ /// l'échelle d'une pause, une demi-seconde de recomposes ne coûte rien, alors qu'une fenêtre
1102+ /// trop courte laisse l'effet invisible sur une machine lente — exactement le bug d'origine.
1103+ const SETTLE_WINDOW : Duration = Duration :: from_millis ( 500 ) ;
1104+ /// Cadence des recomposes dans cette fenêtre : celle de la segmentation (`SEGMENTATION_HZ`),
1105+ /// pas celle de la boucle — recomposer à 250 Hz n'accélérerait pas une inférence limitée à 30 Hz.
1106+ const SETTLE_STEP : Duration = Duration :: from_millis ( 33 ) ;
1107+
1108+ /// Faut-il recomposer alors que RIEN n'a changé ? Oui tant que la fenêtre de stabilisation
1109+ /// court et que la cadence le permet. Extrait de la boucle pour être vérifiable sans GPU.
1110+ fn should_settle ( now : Instant , settle_until : Option < Instant > , last_settle : Instant ) -> bool {
1111+ settle_until. is_some_and ( |deadline| now < deadline)
1112+ && now. duration_since ( last_settle) >= SETTLE_STEP
1113+ }
1114+
10991115/// Démarre le préchargement du clip suivant sur un thread dédié dès qu'on entre dans la
11001116/// fenêtre `PREFETCH_LEAD_SEC` avant la fin du clip actif — pour que la bascule à la
11011117/// frontière (`advance_to_next_scene_clip`) trouve les décodeurs déjà ouverts et positionnés
@@ -1348,6 +1364,15 @@ unsafe fn render_thread(
13481364 let mut last_preview_size: ( u32 , u32 ) = ( 0 , 0 ) ;
13491365 let mut last_ip: Option < InspectorParams > = None ;
13501366 let mut last_smoothing: f32 = -1.0 ; // force la 1re application (0.0 est une valeur valide)
1367+ // Fenêtre de stabilisation après un changement EN PAUSE. Un seul recompose ne suffit pas
1368+ // quand l'effet demandé est asynchrone : la segmentation webcam (détourage / flou / fond
1369+ // personnalisé) démarre son worker au 1er compose, ne SOUMET la frame qu'au 2e et ne
1370+ // téléverse le masque qu'au 3e — d'où l'effet qui n'apparaissait qu'au scrub suivant, le
1371+ // scrub étant la seule chose qui recomposait encore.
1372+ // ponytail: fenêtre fixe plutôt qu'un vrai signal « masque en attente » exposé par les
1373+ // trois compositeurs ; à remplacer si une machine met plus que ça à inférer.
1374+ let mut settle_until: Option < Instant > = None ;
1375+ let mut last_settle = Instant :: now ( ) ;
13511376 // La vue live est TOUJOURS pilotée par la scène de l'app. Tant qu'aucune scène n'a été
13521377 // appliquée, on refuse de jouer le layout fixture (POC) : un fallback fixture ne ferait que
13531378 // MASQUER un scene-push cassé. On attend la scène avant de produire le 1er frame.
@@ -1687,6 +1712,15 @@ unsafe fn render_thread(
16871712 }
16881713 } else if first || ip_changed || scene_changed || clip_changed || resized {
16891714 // pause : recompose la frame courante (param / scène / clip / résolution changés).
1715+ settle_until = Some ( now + SETTLE_WINDOW ) ;
1716+ last_settle = now;
1717+ let _ = player. recompose ( & comp, & cfg) ;
1718+ stepped = true ;
1719+ } else if should_settle ( now, settle_until, last_settle) {
1720+ // Rien n'a changé, mais un masque de segmentation peut encore être en vol : on
1721+ // recompose à la cadence de la segmentation (pas à celle de la boucle) jusqu'à ce
1722+ // que la fenêtre expire.
1723+ last_settle = now;
16901724 let _ = player. recompose ( & comp, & cfg) ;
16911725 stepped = true ;
16921726 }
@@ -1905,6 +1939,31 @@ pub fn run_standalone(_screen: &str, _webcam: &str, _cursor_json: &str) -> anyho
19051939
19061940#[ cfg( test) ]
19071941mod tests {
1942+ use super :: { should_settle, SETTLE_STEP , SETTLE_WINDOW } ;
1943+ use std:: time:: Instant ;
1944+
1945+ /// Le bug d'origine : en pause, un seul recompose par changement, donc le masque de
1946+ /// segmentation (asynchrone, 3 composes de latence) n'arrivait jamais avant un scrub.
1947+ #[ test]
1948+ fn settle_recomposes_within_the_window_at_the_segmentation_rate ( ) {
1949+ let t0 = Instant :: now ( ) ;
1950+ let deadline = Some ( t0 + SETTLE_WINDOW ) ;
1951+
1952+ assert ! ( !should_settle( t0, None , t0) , "aucune fenêtre ouverte : rien à faire" ) ;
1953+ assert ! (
1954+ !should_settle( t0 + SETTLE_STEP / 2 , deadline, t0) ,
1955+ "dans la fenêtre mais trop tôt : on ne recompose pas à la cadence de la boucle" ,
1956+ ) ;
1957+ assert ! (
1958+ should_settle( t0 + SETTLE_STEP , deadline, t0) ,
1959+ "dans la fenêtre et la cadence est due : c'est le tour qui livre le masque" ,
1960+ ) ;
1961+ assert ! (
1962+ !should_settle( t0 + SETTLE_WINDOW , deadline, t0) ,
1963+ "fenêtre expirée : on retombe en pause inerte plutôt que de recomposer sans fin" ,
1964+ ) ;
1965+ }
1966+
19081967 use super :: * ;
19091968
19101969 fn multiclip_scene ( ) -> Scene {
0 commit comments