From e7a0b2f2b8dc84f786864a764c02f2fdf504653b Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Thu, 30 Jul 2026 15:07:38 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(preview):=20seek=5Fto=20retombe=20sur?= =?UTF-8?q?=20un=20seek=20complet=20quand=20le=20d=C3=A9code-avant=20bute?= =?UTF-8?q?=20sur=20l'EOF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suite du pool de décodeurs (#209). Un décodeur réactivé depuis le pool est souvent laissé en FIN de flux (on venait d'y scruber près de la fin avant de le quitter). Quand on y revient et qu'on le repositionne un poil plus loin, le chemin rapide `decode_forward_to` bute sur l'EOF et rendait `null` — ce qui forçait `swap_clip_pooled` à JETER la paire poolée et à tout ROUVRIR : mesuré ~190 ms, les pires à-coups ressentis au franchissement (13 % des franchissements dans un scrub réel, cf. les logs `OPENSCREEN_CLIPSWITCH_TIMING`). Correctif dans `seek_to` (donc utile aussi à `seek_active`, pas seulement au pool) : quand le décode-avant atteint l'EOF avant la cible, on NE rend plus `null` — on retombe sur le seek keyframe complet en dessous, qui rembobine + réarme le décodeur et repart proprement. Si la cible est réellement au-delà de l'EOF, le seek complet rend `null` lui aussi : comportement inchangé pour ce cas. Appliqué aux deux backends (`pipeline_windows` + `pipeline_macos`). Effet mesuré (headless, GPU au repos) : les reseeks près de l'EOF passent de OPEN ~190 ms à POOL_HIT ~50-80 ms. Jamais pire que l'état d'avant (pire cas identique : même repli ouverture). Sûreté vérifiée (harnais avec lecture des pixels, pas seulement le compteur de générations) : sur 46 franchissements — sauts, retours locaux, éviction LRU 5 assets, ET près de l'EOF — **0 preview noire, 0 frame stale** ; chaque franchissement produit une frame fraîche valide. (Les "frames absentes" d'un premier jet étaient des faux positifs : un `pump` trop court qui expirait avant qu'un seek keyframe près de l'EOF, GPU chargé, ait fini.) cargo check OK. --- crates/compositor/src/pipeline_macos.rs | 8 +++++++- crates/compositor/src/pipeline_windows.rs | 13 ++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/crates/compositor/src/pipeline_macos.rs b/crates/compositor/src/pipeline_macos.rs index 18ed75667..8fec0fb23 100644 --- a/crates/compositor/src/pipeline_macos.rs +++ b/crates/compositor/src/pipeline_macos.rs @@ -250,7 +250,13 @@ impl Decoder { // 2) La cible est DEVANT et à portée : dérouler depuis ici plutôt que de // repartir d'une image clé (cf. `pipeline_windows::SEEK_FORWARD_MAX_SEC`). if cur < seconds && seconds - cur <= SEEK_FORWARD_MAX_SEC { - return self.decode_forward_to(seconds, tb_sec); + let f = self.decode_forward_to(seconds, tb_sec)?; + if !f.is_null() { + return Ok(f); + } + // EOF atteint avant la cible (décodeur réactivé depuis le pool, laissé en fin + // de flux) : on retombe sur le seek keyframe complet au lieu de rendre `null` + // — qui forçait une réouverture complète. Voir `pipeline_windows::seek_to`. } } } diff --git a/crates/compositor/src/pipeline_windows.rs b/crates/compositor/src/pipeline_windows.rs index 1126d8e48..bbdab2642 100644 --- a/crates/compositor/src/pipeline_windows.rs +++ b/crates/compositor/src/pipeline_windows.rs @@ -619,7 +619,18 @@ impl Decoder { // repartir d'une image clé redevient moins cher — un seek coûte en moyenne // un demi-GOP, soit ~0,5 s sur nos captures. if cur < seconds && seconds - cur <= SEEK_FORWARD_MAX_SEC { - return self.decode_forward_to(seconds, tb_sec); + let f = self.decode_forward_to(seconds, tb_sec)?; + if !f.is_null() { + return Ok(f); + } + // `decode_forward_to` a atteint l'EOF avant la cible — typiquement un + // décodeur réactivé depuis le pool (`live::swap_clip_pooled`), laissé en fin + // de flux (`sent_eof`), qui ne peut plus avancer. On NE rend PAS `null` : ça + // forçait l'appelant à tout ROUVRIR (~190 ms mesurés), le pire à-coup ressenti + // au franchissement. On retombe sur le seek keyframe complet ci-dessous, qui + // rembobine + réarme le décodeur et repart proprement. Si la cible est + // réellement au-delà de l'EOF, ce seek complet rendra `null` lui aussi : + // comportement inchangé pour ce cas. } } } From 34362f4e611402e5816c8d06a0e7e9b8f39483a0 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Thu, 30 Jul 2026 15:22:45 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(preview):=20supprimer=20l'overlay=20?= =?UTF-8?q?=C2=AB=20Loading=20preview=E2=80=A6=20=C2=BB=20qui=20recouvrait?= =?UTF-8?q?=20une=20image=20valide?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'overlay reflétait l'état du `