Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions crates/compositor/src/compositor_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2630,12 +2630,13 @@ impl Compositor {
// Calque ecran : mode 0 (rect droit, NV12 -> RGB) quand la rotation est
// neutre, mode 8 (warp bilineaire inverse dans la bbox du quad projete)
// sinon. Place par plan_frame (cover-fit + coins arrondis) ;
// `src = g.cut` (crop utilisateur + zoom en UV texture) dans les deux cas.
// `src = g.screen_src(g.cut)` (crop utilisateur + zoom en UV texture, bornes
// inversees pour un clip tourne) dans les deux cas.
//
// FLOU DE VELOCITE. Au mode 0, `src_prev`/`dst_prev` decrivent le MEME
// calque a la frame precedente ; le shader remappe chaque pixel de sortie
// par ce couple pour retrouver l'UV qu'il occupait alors, et floute le long
// du segment. `src_prev = g.cut` et non un `cut` d'avant : la coupe est
// du segment. `src_prev = g.screen_src(g.cut)` et non un `cut` d'avant : la coupe est
// identique aux deux frames (`plan_frame` ne fait varier que le rect de
// DESTINATION entre `s_dst` et `s_dst_prev`), ce que Windows documente
// aussi. Le mouvement vient donc entierement de `dst_prev`.
Expand All @@ -2652,19 +2653,21 @@ impl Compositor {
// Et la remontee du contour interieur du chrome au-dessus de l'ecran (`mb.z` au mode 0,
// `color.z` au mode 8) : l'arrondi du haut se fait par le cadre. 0 sans fenetre.
let top_lift = g.screen_top_lift_px([rw, rh]);
// The screen texture's source rect: a turned clip samples it with reversed bounds.
let src_uv = g.screen_src(g.cut);
let screen_layer = match tilt.as_ref() {
None => {
// Sous le masque d'un layout en bloc, rogne au slot (`mask_flat_screen`).
let (dst, src, quad_px, radius_px) =
g.mask_flat_screen(g.s_dst, g.cut, s_px, g.s_radius, [rw, rh]);
g.mask_flat_screen(g.s_dst, src_uv, s_px, g.s_radius, [rw, rh]);
LayerCB {
dst,
src,
quad_px,
radius_px,
mode: 0.0,
color: [1.0, 1.0, 1.0, 1.0],
src_prev: g.cut,
src_prev: src_uv,
dst_prev: g.s_dst_prev,
mb: [g.screen_pixel_taps([rw, rh]), g.mb_amount, top_lift, square_top],
..Default::default()
Expand All @@ -2675,7 +2678,7 @@ impl Compositor {
quad,
s_px,
quad_center_px,
g.cut,
src_uv,
g.focus_plane,
g.s_radius,
top_lift,
Expand Down Expand Up @@ -3847,7 +3850,7 @@ impl Compositor {
let footage = effects
.iter()
.any(|e| crate::shader_fx::reads_footage(e))
.then(|| self.encode_footage_copy(&mut encoder, (&sy, &su, &sv), g.content_cut, stw, sth));
.then(|| self.encode_footage_copy(&mut encoder, (&sy, &su, &sv), g.screen_src(g.content_cut), stw, sth));
for e in with_target(ShaderEffectTarget::Image) {
self.encode_shader_effect(&mut encoder, e, (&self.rt, &self.rt_view), &fx_context(e), footage.as_ref());
}
Expand Down
7 changes: 4 additions & 3 deletions crates/compositor/src/compositor_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2792,7 +2792,8 @@ impl Compositor {
}
let square_top = g.screen_square_top();
let top_lift = g.screen_top_lift_px([rw, rh]);
let [su0, sv0, su1, sv1] = g.cut;
// The screen texture's source rect: a turned clip samples it with reversed bounds.
let [su0, sv0, su1, sv1] = g.screen_src(g.cut);
// Sous le masque d'un layout en bloc, rogné au slot (`FrameGeometry::mask_flat_screen`).
let (dst, src, quad_px, radius_px) =
g.mask_flat_screen(g.s_dst, [su0, sv0, su1, sv1], s_px, g.s_radius, [rw, rh]);
Expand All @@ -2819,7 +2820,7 @@ impl Compositor {
quad,
s_px,
quad_center_px,
g.cut,
g.screen_src(g.cut),
g.focus_plane,
g.s_radius,
top_lift,
Expand Down Expand Up @@ -3094,7 +3095,7 @@ impl Compositor {
};
// L'enregistrement à sa résolution, peint une fois pour la frame si un effet le lit.
let footage = if effects.iter().any(|e| crate::shader_fx::reads_footage(e)) {
Some(self.fill_footage_copy(cmd_buf, &sy, &suv, g.content_cut, stw, sth)?)
Some(self.fill_footage_copy(cmd_buf, &sy, &suv, g.screen_src(g.content_cut), stw, sth)?)
} else {
None
};
Expand Down
34 changes: 31 additions & 3 deletions crates/compositor/src/compositor_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2384,7 +2384,8 @@ impl Compositor {
let mb_amount = g.mb_amount;
let source_t = g.source_t;
let _padding_scale = g.padding_scale;
let cut = g.cut;
// The screen texture's source rect: a turned clip samples it with reversed bounds.
let cut = g.screen_src(g.cut);
let s_dst = g.s_dst;
let s_dst_prev = g.s_dst_prev;
let s_ann = g.s_ann;
Expand Down Expand Up @@ -2486,7 +2487,7 @@ impl Compositor {
self.draw_video(
&LayerCB {
dst: [-over, -over, 1.0 + 2.0 * over, 1.0 + 2.0 * over],
src: [0.0, 0.0, u_max, v_max],
src: g.screen_src([0.0, 0.0, u_max, v_max]),
quad_px: [self.rw(), self.rh()],
mode: 0.0,
color: [1.0, 1.0, 1.0, 1.0],
Expand Down Expand Up @@ -2912,7 +2913,7 @@ impl Compositor {
};
// L'enregistrement à sa résolution, peint une fois pour la frame si un effet le lit.
let footage = if effects.iter().any(|e| crate::shader_fx::reads_footage(e)) {
Some(self.fill_footage_copy(&sy, &suv, g.content_cut, stw, sth)?)
Some(self.fill_footage_copy(&sy, &suv, g.screen_src(g.content_cut), stw, sth)?)
} else {
None
};
Expand Down Expand Up @@ -4127,6 +4128,33 @@ mod tests {
assert!(left[0].abs_diff(right[0]) < 128, "l'image composée sépare les colonnes : {left:?} {right:?}");
}

/// Sur un clip retourné, `footage()` lit l'enregistrement tel qu'on le voit, retourné : la
/// colonne 10 affichée est la colonne 501 de la texture, noire.
#[test]
fn footage_reads_a_turned_clip_turned() {
let Ok(gpu) = crate::d3d::Gpu::create_auto(false) else {
eprintln!("pas de device D3D11 — test sauté");
return;
};
let pixel = |turned: bool| {
let comp = Compositor::new_sized(&gpu, 320, 180).expect("compositeur");
let code = "fn effect(uv: vec2<f32>) -> vec4<f32> { return footage(vec2<f32>(10.5 / 512.0, 0.5)); }";
let mut json = shader_fx_json(serde_json::json!([fx(code, "image")]));
if turned {
json["rotationByClip"] = serde_json::json!([180]);
}
comp.set_scene(Some(Scene::from_json(&json.to_string()).expect("scène")));
let (screen, _tex) = nv12_columns(&gpu, 512, 512);
comp.set_timeline_time(Some(2.0));
comp.set_programme_time(Some(2.0));
unsafe { comp.compose_frame(&*screen, &*screen, 0.0, &fx_cfg()) }.expect("compose");
let (_, _, rgba) = unsafe { comp.readback_direct() }.expect("readback");
rgba[0]
};
assert!(pixel(false) >= 253, "droit : colonne 10 blanche");
assert!(pixel(true) <= 2, "retourné : colonne 501 noire");
}

/// La copie de l'enregistrement coûte une passe : un effet qui ne lit pas `footage()` ne la
/// fait jamais faire.
#[test]
Expand Down
37 changes: 37 additions & 0 deletions crates/compositor/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,23 @@ impl CursorTrack {
}
}

/// The same track seen on a picture turned by 180°: every position mirrored, times,
/// clicks, visibility and cursor types unchanged. A turned clip is planned entirely in
/// what the viewer sees, so its telemetry is turned once here rather than at every reader.
pub fn turned(&self) -> CursorTrack {
let flip =
|v: &[(f32, f32, f32)]| v.iter().map(|&(t, x, y)| (t, 1.0 - x, 1.0 - y)).collect();
CursorTrack {
samples: flip(&self.samples),
follow_samples: flip(&self.follow_samples),
clicks: self.clicks.clone(),
click_points: self.click_points.iter().map(|&(x, y)| (1.0 - x, 1.0 - y)).collect(),
visibility: self.visibility.clone(),
types: self.types.clone(),
view_cache: Default::default(),
}
}

/// Visibilité enregistrée au temps `t`. Les anciens sidecars qui n'ont pas ce champ sont
/// visibles par défaut, comme les lecteurs TypeScript.
pub fn visible_at(&self, t: f32) -> bool {
Expand Down Expand Up @@ -672,6 +689,26 @@ fn cursor_spring_config(smoothing_factor: f32) -> (f32, f32, f32) {
mod tests {
use super::*;

#[test]
fn turned_track_mirrors_positions_and_keeps_times() {
let t = CursorTrack::new(
vec![(0.0, 0.2, 0.3), (1.0, 0.6, 0.9)],
vec![0.5],
vec![(0.0, "arrow".to_string())],
);
let r = t.turned();
let (x, y) = r.at(0.0).unwrap();
assert!((x - 0.8).abs() < 1e-6 && (y - 0.7).abs() < 1e-6, "({x}, {y})");
assert_eq!(r.clicks_between(0.0, 1.0), t.clicks_between(0.0, 1.0));
assert_eq!(r.type_at(0.0), t.type_at(0.0));
let (fx, fy) = r.follow_at(1.0).unwrap();
let (gx, gy) = t.follow_at(1.0).unwrap();
assert!((fx - (1.0 - gx)).abs() < 1e-5 && (fy - (1.0 - gy)).abs() < 1e-5);
// The click point turns with the picture: the click at 0.5 s was at (0.4, 0.6).
let (px, py) = r.click_points[0];
assert!((px - 0.6).abs() < 1e-6 && (py - 0.4).abs() < 1e-6, "({px}, {py})");
}

/// Zone morte du suivi auto : un pointeur qui tremble au centre de la vue ne la bouge pas ;
/// un pointeur qui part loin est rattrapé, jusqu'au bord de la zone et pas au-delà.
/// Les vues d'une région sont rejouées une fois puis relues : même réponse dans n'importe quel
Expand Down
Loading
Loading