Skip to content

Commit 1322223

Browse files
committed
fix(compositor): show the webcam background effects on click, not on the next scrub
Segmentation is asynchronous: the first compose starts the worker, the second submits the frame, the third uploads the mask. Paused, the render loop recomposed exactly once per change, so the mask never landed — cutout, blur and custom background all stayed inert until a scrub produced the missing composes. Keep recomposing for 500 ms after a paused change, at the segmentation rate rather than the loop's, so the mask arrives on its own.
1 parent 1a33936 commit 1322223

1 file changed

Lines changed: 59 additions & 0 deletions

File tree

‎crates/compositor/src/live.rs‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,22 @@ type PendingPrefetch = (usize, std::sync::mpsc::Receiver<Result<PrefetchedClip>>
10961096
/// décodeurs ouvertes plus longtemps que nécessaire.
10971097
const 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)]
19071941
mod 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

Comments
 (0)