Skip to content

Commit 155cfff

Browse files
committed
fix(preview): stop re-seeking the native view at every cut and in speed regions
While playing, the native view runs its own clock and crosses clip boundaries by itself, preloading the next clip ahead of the cut. Two things kept steering it anyway: - The overlay re-sent the clip at every cut, with a pause around it. The view then sought back to a place it had already passed, or threw away the clip it had preloaded and opened it synchronously: a hitch at every cut, which on a transcript-edited take means every few seconds. - The playback sync guessed drift from the wall clock at 1x speed. Inside a 2x speed region that guess was 100 ms off every 100 ms, and the view was re-seeked about ten times a second. Every frame now carries where the view was when it composed it (clip index, source time, both transports). The overlay compares that with the playhead on the trim-compressed programme timeline, where a cut is no jump: a cut crossed in play is left to the view, a jump is followed at once, and a gap over 150 ms that holds for 100 ms re-anchors the view, at most every 500 ms. The wall-clock guess is gone. An addon that reports no position is driven as before.
1 parent 72574f0 commit 155cfff

13 files changed

Lines changed: 646 additions & 64 deletions

File tree

‎crates/compositor-view-napi/src/lib.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ pub struct FramePacket {
204204
/// Le métrage passe de ses coins à l'image par leur homographie (caméra réelle), pas par
205205
/// leur interpolation bilinéaire.
206206
pub footage_projective: bool,
207+
/// Clip actif de la scène quand la frame a été composée (`FramePosition`).
208+
pub clip_index: u32,
209+
/// Temps source de la frame écran composée, en secondes.
210+
pub source_time_sec: f64,
207211
}
208212

209213
/// Renvoie la dernière frame readback du thread de rendu SI elle est plus récente que
@@ -243,7 +247,7 @@ pub fn read_frame(id: i32, since_gen: f64) -> Result<Option<FramePacket>> {
243247
v.latest_frame_since(since_gen.max(0.0) as u64)
244248
}
245249
};
246-
Ok(slot.map(|(gen, w, h, pixels, footage)| {
250+
Ok(slot.map(|(gen, w, h, pixels, footage, position)| {
247251
debug_assert_eq!(pixels.len(), (w as usize) * (h as usize) * 4);
248252
FramePacket {
249253
gen: gen as f64,
@@ -252,6 +256,8 @@ pub fn read_frame(id: i32, since_gen: f64) -> Result<Option<FramePacket>> {
252256
data: Buffer::from(pixels),
253257
footage: footage_corners(footage),
254258
footage_projective: footage.is_some_and(|q| q.projective),
259+
clip_index: position.clip_index,
260+
source_time_sec: position.source_time_sec,
255261
}
256262
}))
257263
}
@@ -276,6 +282,8 @@ pub struct SharedFramePacket {
276282
pub height: u32,
277283
pub footage: Option<Vec<f64>>,
278284
pub footage_projective: bool,
285+
pub clip_index: u32,
286+
pub source_time_sec: f64,
279287
}
280288

281289
/// Livrer les frames de la vue par textures partagées plutôt que par `read_frame`. Rend
@@ -314,6 +322,8 @@ pub fn read_shared_frame(id: i32, since_gen: f64) -> Result<Option<SharedFramePa
314322
height: f.height,
315323
footage: footage_corners(f.footage),
316324
footage_projective: f.footage.is_some_and(|q| q.projective),
325+
clip_index: f.position.clip_index,
326+
source_time_sec: f.position.source_time_sec,
317327
}))
318328
}
319329

‎crates/compositor/src/live.rs‎

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -759,15 +759,34 @@ fn scene_for_clip(scene: &Scene, clip_index: usize) -> Scene {
759759

760760
/// Dernière frame readback vers CPU, prête pour le napi `read_frame`.
761761
///
762-
/// `(gen, w, h, vec)` où `vec.len() == w*h*4` octets RGBA8 tightly-packed (R, G, B, A
763-
/// en mémoire — cf. `Compositor::readback_resized`). `gen` est une génération monotone
762+
/// `(gen, w, h, vec, métrage, position)` où `vec.len() == w*h*4` octets RGBA8 tightly-packed
763+
/// (R, G, B, A en mémoire — cf. `Compositor::readback_resized`). `gen` est une génération monotone
764764
/// (≥ 1, `0` réservé à « le consommateur n'a encore rien vu ») incrémentée à CHAQUE
765765
/// publication, càd uniquement quand une nouvelle frame a réellement été composée (le
766766
/// thread de rendu ne republie pas une frame identique — cf. `stepped || first`). Elle
767767
/// est l'IDENTITÉ de la frame : le consommateur (`read_frame`) ne repaie le clone + l'IPC
768768
/// que lorsqu'elle change. `None` = "aucune frame composée pour l'instant" (toutes les
769769
/// lectures avant la 1re frame composée retournent `None` côté napi, jamais un buffer vide).
770-
pub type LatestFrame = (u64, u32, u32, Vec<u8>, Option<crate::frame_geometry::FootageQuad>);
770+
pub type LatestFrame = (
771+
u64,
772+
u32,
773+
u32,
774+
Vec<u8>,
775+
Option<crate::frame_geometry::FootageQuad>,
776+
FramePosition,
777+
);
778+
779+
/// Où en est la vue quand elle compose une frame : le clip actif de la scène et le temps
780+
/// source de la frame écran. Voyage avec chaque frame publiée, quel que soit le transport,
781+
/// pour que l'app compare la position RÉELLE de la vue à sa propre tête de lecture au lieu
782+
/// de la deviner à partir de l'horloge murale.
783+
#[derive(Debug, Clone, Copy, PartialEq)]
784+
pub struct FramePosition {
785+
/// Index du clip actif dans `scene.clips`.
786+
pub clip_index: u32,
787+
/// pts de la frame écran composée, en secondes du fichier source.
788+
pub source_time_sec: f64,
789+
}
771790

772791
/// État partagé thread appelant → thread de rendu (commandes sans blocage).
773792
struct Shared {
@@ -1840,7 +1859,11 @@ unsafe fn render_thread(
18401859

18411860
if stepped || first || publish_pending {
18421861
if pw > 0 && ph > 0 {
1843-
match publish_shared(&shared, &gpu, &comp, &mut ring) {
1862+
let position = FramePosition {
1863+
clip_index: active_clip_index as u32,
1864+
source_time_sec: player.screen_time_sec(),
1865+
};
1866+
match publish_shared(&shared, &gpu, &comp, &mut ring, position) {
18441867
SharedPublish::Published => {
18451868
first = false;
18461869
publish_pending = false;
@@ -1874,7 +1897,7 @@ unsafe fn render_thread(
18741897
// l'ancienne dérivation tant que le slot n'est pas vidé.
18751898
let next_gen = shared.frame_gen.fetch_add(1, Ordering::Relaxed) + 1;
18761899
if let Ok(mut slot) = shared.latest_frame.lock() {
1877-
*slot = Some((next_gen, rw, rh, rgba, comp.footage_quad()));
1900+
*slot = Some((next_gen, rw, rh, rgba, comp.footage_quad(), position));
18781901
}
18791902
first = false;
18801903
publish_pending = false;
@@ -1918,6 +1941,7 @@ unsafe fn publish_shared(
19181941
gpu: &Gpu,
19191942
comp: &Compositor,
19201943
ring: &mut Ring,
1944+
position: FramePosition,
19211945
) -> SharedPublish {
19221946
if !shared.shared_frames.load(Ordering::Relaxed) {
19231947
return SharedPublish::Off;
@@ -1967,6 +1991,7 @@ unsafe fn publish_shared(
19671991
width,
19681992
height,
19691993
footage: comp.footage_quad(),
1994+
position,
19701995
});
19711996
}
19721997
SharedPublish::Published
@@ -1976,7 +2001,13 @@ unsafe fn publish_shared(
19762001
}
19772002

19782003
#[cfg(not(windows))]
1979-
unsafe fn publish_shared(_: &Shared, _: &Gpu, _: &Compositor, _: &mut Ring) -> SharedPublish {
2004+
unsafe fn publish_shared(
2005+
_: &Shared,
2006+
_: &Gpu,
2007+
_: &Compositor,
2008+
_: &mut Ring,
2009+
_: FramePosition,
2010+
) -> SharedPublish {
19802011
SharedPublish::Off
19812012
}
19822013

@@ -2113,7 +2144,7 @@ pub fn run_standalone(screen: &str, webcam: &str, cursor_json: &str) -> Result<(
21132144
// standalone n'affiche pas réellement les pixels ici (l'embed Electron est
21142145
// le consumer réel). On imprime juste une frame de temps en temps pour
21152146
// confirmer que la chaîne fonctionne.
2116-
if let Some((_gen, fw, fh, _pixels, _)) = view.latest_frame() {
2147+
if let Some((_gen, fw, fh, _pixels, _, _)) = view.latest_frame() {
21172148
if (fw, fh) != (w, h) {
21182149
// garde-fou : la staging de readback suit `set_rect` côté thread
21192150
// de rendu, donc ce serait une désynchro transitoire — acceptable.

‎crates/compositor/src/shared_frames.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
//! macOS (IOSurface) et Linux (dmabuf) restent sur le readback.
1414
1515
use crate::frame_geometry::FootageQuad;
16+
use crate::live::FramePosition;
1617
use std::time::{Duration, Instant};
1718

1819
/// Cases de l'anneau : une prête, une en transit vers le renderer (tenue jusqu'à ce que
@@ -37,6 +38,7 @@ pub struct SharedFrame {
3738
pub width: u32,
3839
pub height: u32,
3940
pub footage: Option<FootageQuad>,
41+
pub position: FramePosition,
4042
}
4143

4244
/// Une case prise par JS : la génération qu'elle porte et quand elle est partie.
@@ -244,7 +246,15 @@ mod tests {
244246
use super::*;
245247

246248
fn frame(gen: u64, slot: u32) -> SharedFrame {
247-
SharedFrame { gen, slot, handle: 0, width: 2, height: 2, footage: None }
249+
SharedFrame {
250+
gen,
251+
slot,
252+
handle: 0,
253+
width: 2,
254+
height: 2,
255+
footage: None,
256+
position: FramePosition { clip_index: 0, source_time_sec: 0.0 },
257+
}
248258
}
249259

250260
/// Toutes les cases prises par JS, la case `n` portant la génération `n + 1`.

‎electron/native-bridge/services/compositorViewService.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,8 @@ export class CompositorViewService {
703703
height: frame.height,
704704
footage: frame.footage ?? null,
705705
footageProjective: frame.footageProjective ?? false,
706+
clipIndex: frame.clipIndex,
707+
sourceTimeSec: frame.sourceTimeSec,
706708
};
707709
const api = this.sharedTextureApi();
708710
let imported: Electron.SharedTextureImported | undefined;

‎electron/native/compositor-view/addon.d.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ export interface NativeFramePacket {
3939
footage?: number[] | null;
4040
/** The footage maps from those corners by their homography (real camera), not bilinearly. */
4141
footageProjective?: boolean;
42+
/** Where the view was when it composed this frame: the active clip in `scene.clips` and the
43+
* screen frame's time in its source file. Absent from an older `.node`. */
44+
clipIndex?: number;
45+
sourceTimeSec?: number;
4246
}
4347

4448
/** A preview frame left in a shared GPU texture instead of copied into RAM (Windows, hardware
@@ -53,6 +57,10 @@ export interface NativeSharedFramePacket {
5357
height: number;
5458
footage?: number[] | null;
5559
footageProjective?: boolean;
60+
/** Where the view was when it composed this frame: the active clip in `scene.clips` and the
61+
* screen frame's time in its source file. Absent from an older `.node`. */
62+
clipIndex?: number;
63+
sourceTimeSec?: number;
5664
}
5765

5866
export interface ExportStats {

0 commit comments

Comments
 (0)