Skip to content

perf(preview): pool de décodeurs — plus de réouverture au franchissement de clip au scrub - #209

Merged
EtienneLescot merged 1 commit into
release/v1.8.0from
perf/decoder-pool
Jul 30, 2026
Merged

perf(preview): pool de décodeurs — plus de réouverture au franchissement de clip au scrub#209
EtienneLescot merged 1 commit into
release/v1.8.0from
perf/decoder-pool

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Contexte

Le plus gros à-coup ressenti au scrub, c'est franchir une frontière de clip (les pics p99 100-167 ms de la sonde de fluidité). Dé-risque headless (offscreen, sur cette machine AMD) : un franchissement cross-média coûte ~120 ms de blocage synchrone sur le thread de rendu —

étape screen webcam sous-total
Decoder::open (avformat + init D3D11VA + avcodec_open2) ~33 ms ~6 ms ~39 ms (32 %)
seek keyframe + décode-avant ~47 ms ~35 ms ~81 ms (68 %)

Et jusqu'ici on jetait les décodeurs sortants (apply_prefetched), donc revenir sur un clip — le motif A→B→A ultra-fréquent quand on traîne la tête à travers une coupe — rouvrait tout à neuf plus un seek keyframe complet.

Le correctif

Un petit pool LRU (cap 3) de paires de décodeurs INACTIVES, gardées ouvertes à leur dernière position. Au franchissement cross-média (chemin scrub active_clip_request uniquement — la lecture libre est déjà préchargée, sans stall) : si la cible est en pool → reseek au lieu de rouvrir ; sinon ouverture neuve. Dans les deux cas la paire QUITTÉE part en pool (dédup par clé + éviction LRU) au lieu d'être fermée.

Comme un décodeur poolé reste à sa dernière position, revenir près de là — exactement ce qu'on fait en scrubant autour d'une coupe — emprunte le chemin rapide decode_forward au lieu d'un seek keyframe.

Mesures (headless, OPENSCREEN_CLIPSWITCH_TIMING=1)

cas avant après
scrub qui saute loin ~120 ms ~50-120 ms (open éliminé, seek keyframe restant)
scrub près d'une frontière (le cas ressenti) ~120 ms ~5 ms (≈ 20×)

Sûreté

La classe « frame sans texture → preview noire définitive » a déjà mordu dans ce fichier, donc :

  • la paire poolée est reseekée AVANT d'être installée ; si le seek échoue (position hors flux, EOF non rembobinable) on la jette et on ouvre à neuf (chemin connu sûr) — jamais une frame vide (seek_pair rend false) ;
  • le cache de SRV (keyé sur l'adresse de texture) reste vidé à chaque franchissement comme avant — over-clear est sûr et bon marché, ce qui écarte tout « image du clip précédent » ;
  • lecture libre inchangée : apply_prefetched / advance_to_next_scene_clip droppent toujours (déjà préchargé) ; seul le scrub alimente/consulte le pool ;
  • pool borné à DECODER_POOL_CAP=3 (VRAM : chaque paire retient son pool de surfaces D3D11VA) ; vidé quand la vue est détruite / le document rechargé.

Vérification

cargo check OK. Harnais headless sur 3 phases — sauts, retours locaux, et cyclage de 5 assets distincts (force l'éviction LRU) : le pool reste borné à 3 et aucun franchissement, éviction comprise, ne produit de frame noire (générations = l'assertion, cf. la convention de test natif du repo — pas de CI Rust). Re-vérifié après rebase sur la base courante (rework macOS des 54 commits ; la logique décodeurs Windows/D3D11 est inchangée).

…ent de clip au scrub

Mesuré (headless, offscreen) : franchir un clip cross-média coûtait ~120 ms de blocage
synchrone sur le thread de rendu — 2× `Decoder::open` (avformat + init D3D11VA + avcodec_open2,
~39 ms) puis 2× seek keyframe + décode-avant (~81 ms). C'est le plus gros à-coup du scrub
(les pics p99 100-167 ms de la sonde), ~13× le readback. Et jusqu'ici on JETAIT les décodeurs
sortants (`apply_prefetched`), donc revenir sur un clip (motif A→B→A ultra-fréquent) rouvrait
tout à neuf + reseek keyframe complet.

On garde désormais un petit pool LRU (cap 3) de paires INACTIVES, ouvertes à leur dernière
position. Au franchissement cross-média (chemin scrub `active_clip_request`, hors lecture libre
déjà préchargée) : si la cible est en pool → reseek au lieu de rouvrir ; sinon ouverture neuve.
Dans les deux cas la paire QUITTÉE part en pool (dédup par clé + éviction LRU) au lieu d'être
fermée.

Effet mesuré, borné honnêtement :
  - scrub qui SAUTE loin  : POOL_HIT ~50-120 ms (open éliminé, seek keyframe restant) vs ~120 ms.
  - scrub PRÈS d'une frontière (le cas ressenti : on traîne la tête à travers la coupe) :
    le décodeur poolé est à ~1 frame → chemin rapide `decode_forward` → POOL_HIT ~5-9 ms,
    soit ~20× moins que les ~120 ms d'avant.

Sûreté (la classe « frame sans texture → preview noire définitive » a déjà mordu ici) :
  - la paire poolée est reseekée AVANT installation ; échec du seek → on la jette et on ouvre
    à neuf (chemin connu sûr), jamais une frame vide (`seek_pair` rend `false`).
  - le cache de SRV (keyé sur l'adresse de texture) reste vidé à chaque franchissement comme
    avant — over-clear est sûr et bon marché, ce qui écarte tout « image du clip précédent ».
  - lecture libre inchangée : `apply_prefetched`/`advance_to_next_scene_clip` droppent toujours
    (déjà préchargé, pas de stall) ; seul le scrub alimente/consulte le pool.

Vérifié headless (harness bench-clipswitch, OPENSCREEN_CLIPSWITCH_TIMING=1) sur 3 phases —
sauts, retours locaux, et cyclage de 5 assets distincts (force l'éviction LRU) : le pool reste
borné à 3 et AUCUN franchissement, éviction comprise, ne produit de frame noire. cargo check OK.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 12eb7c4f-554f-4322-acc6-db64f9172895

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot
EtienneLescot merged commit 8766c17 into release/v1.8.0 Jul 30, 2026
11 checks passed
@EtienneLescot
EtienneLescot deleted the perf/decoder-pool branch July 30, 2026 12:48
EtienneLescot added a commit that referenced this pull request Jul 30, 2026
…nt bute sur l'EOF

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant