Skip to content
Merged
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
27 changes: 16 additions & 11 deletions crates/compositor/src/compositor_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1285,10 +1285,20 @@ impl Compositor {
});

// ANNOTATIONS -- calque le plus haut, place relativement au rect ecran
// `g.s_dst` (les coords x/y/w/h de l'annotation sont des fractions de ce
// rect, cf. `scene.rs`). Port de `compositor_macos::draw_annotations` :
// memes modes, memes replis, meme ordre. Seul le texte diverge, tinte
// cote shader (atlas R8) au lieu d'une couleur bakee dans la texture.
// (les coords x/y/w/h de l'annotation sont des fractions de ce rect, cf.
// `scene.rs`). Le rect est `g.s_ann`, l'ecran SANS ZOOM, et surtout pas
// `g.s_dst` : le contrat de `SceneAnnotation` dit « deliberately NOT
// affected by the zoom crop », donc annotations et sous-titres tiennent
// en place pendant que le contenu grossit dessous. `s_dst` a tenu ce role
// gratuitement tant que le zoom vivait dans la coupe source ; depuis
// l'issue #179 il vit dans la BOITE, et l'ancrer dessus fait zoomer les
// sous-titres avec l'ecran. Windows et macOS ont ete corriges alors, ce
// backend non -- d'ou le passage par `FrameGeometry::annotation_dst`, qui
// ne laisse plus le choix. Le natif peignant AUSSI l'apercu, la derive se
// voyait des l'edition, pas seulement a l'export.
// Port de `compositor_macos::draw_annotations` : memes modes, memes
// replis, meme ordre. Seul le texte diverge, tinte cote shader (atlas R8)
// au lieu d'une couleur bakee dans la texture.
struct AnnDraw {
_buf: wgpu::Buffer,
/// Gardent l'atlas / la texture image en vie jusqu'au submit. `None`
Expand Down Expand Up @@ -1324,12 +1334,7 @@ impl Compositor {
if !visible(a) {
continue;
}
let dst = [
g.s_dst[0] + a.x * g.s_dst[2],
g.s_dst[1] + a.y * g.s_dst[3],
a.w * g.s_dst[2],
a.h * g.s_dst[3],
];
let dst = g.annotation_dst(a.x, a.y, a.w, a.h);
let quad_px = [dst[2] * rw, dst[3] * rh];
// Une boite degeneree ferait un atlas 0x0 et un draw invisible ;
// macOS l'ecarte de la meme facon.
Expand Down Expand Up @@ -1475,7 +1480,7 @@ impl Compositor {
content: text.content.clone(),
color,
background,
font_size_px: text.font_size_rel * (g.s_dst[3] * rh),
font_size_px: text.font_size_rel * g.annotation_anchor_h_px(rh),
font_family: text.font_family.clone(),
bold: text.font_weight == "bold",
italic: text.font_style == "italic",
Expand Down
20 changes: 10 additions & 10 deletions crates/compositor/src/compositor_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -976,14 +976,19 @@ impl Compositor {
}


/// Annotations : calque le plus haut, ancré sur `screen_dst` — le conteneur que reçoit
/// l'overlay web. Port de `compositor_windows::draw_annotations`.
/// Annotations : calque le plus haut, ancré sur `s_ann` — le rect écran SANS ZOOM, le
/// conteneur que reçoit l'overlay web. Port de `compositor_windows::draw_annotations`.
///
/// Le paramètre s'appelle `s_ann` et pas `screen_dst` parce que c'est le seul rect
/// correct : lui passer `s_dst` fait dériver et grossir les sous-titres sous un zoom
/// (issue #179, puis #397 sur Linux). L'arithmétique elle-même vit dans
/// `frame_geometry::annotation_dst_in`, partagée par les trois backends.
unsafe fn draw_annotations(
&self,
cmd: &metal::CommandBufferRef,
scene: Option<&Scene>,
t: f32,
screen_dst: [f32; 4],
s_ann: [f32; 4],
) -> Result<()> {
let Some(scene) = scene else { return Ok(()) };
if scene.annotations.is_empty() {
Expand Down Expand Up @@ -1017,12 +1022,7 @@ impl Compositor {
if !visible(a) {
continue;
}
let dst = [
screen_dst[0] + a.x * screen_dst[2],
screen_dst[1] + a.y * screen_dst[3],
a.w * screen_dst[2],
a.h * screen_dst[3],
];
let dst = crate::frame_geometry::annotation_dst_in(s_ann, a.x, a.y, a.w, a.h);
let quad_px = [dst[2] * rw, dst[3] * rh];
if quad_px[0] <= 0.0 || quad_px[1] <= 0.0 {
continue;
Expand Down Expand Up @@ -1137,7 +1137,7 @@ impl Compositor {
color: parse_hex(&text.color).unwrap_or([1.0, 1.0, 1.0, 1.0]),
background: parse_hex(&text.background_color)
.unwrap_or([0.0, 0.0, 0.0, 0.0]),
font_size_px: text.font_size_rel * (screen_dst[3] * rh),
font_size_px: text.font_size_rel * (s_ann[3] * rh),
font_family: text.font_family.clone(),
bold: text.font_weight == "bold",
italic: text.font_style == "italic",
Expand Down
25 changes: 16 additions & 9 deletions crates/compositor/src/compositor_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1578,12 +1578,18 @@ impl Compositor {
Ok(())
}

/// Dessine les annotations visibles à `t`. `screen_dst` = rect écran en fractions de sortie.
/// Dessine les annotations visibles à `t`. `s_ann` = rect écran SANS ZOOM, en fractions de
/// sortie.
///
/// Le paramètre s'appelle `s_ann` et pas `screen_dst` parce que c'est le seul rect correct :
/// lui passer `s_dst` fait dériver et grossir les sous-titres sous un zoom (issue #179, puis
/// #397 sur Linux). L'arithmétique elle-même vit dans `frame_geometry::annotation_dst_in`,
/// partagée par les trois backends.
///
/// Seule la « figure » (flèche) est rendue à ce stade ; texte, image et flou suivront. Les
/// types non gérés sont ignorés silencieusement plutôt que dessinés de travers : mieux vaut
/// l'absence connue qu'un placeholder qui ferait croire à un bug de style.
unsafe fn draw_annotations(&self, scene: Option<&Scene>, t: f32, screen_dst: [f32; 4]) {
unsafe fn draw_annotations(&self, scene: Option<&Scene>, t: f32, s_ann: [f32; 4]) {
let Some(scene) = scene else { return };
if scene.annotations.is_empty() {
return;
Expand All @@ -1610,12 +1616,13 @@ impl Compositor {
if !visible(annotation) {
continue;
}
let dst = [
screen_dst[0] + annotation.x * screen_dst[2],
screen_dst[1] + annotation.y * screen_dst[3],
annotation.w * screen_dst[2],
annotation.h * screen_dst[3],
];
let dst = crate::frame_geometry::annotation_dst_in(
s_ann,
annotation.x,
annotation.y,
annotation.w,
annotation.h,
);
let quad_px = [dst[2] * self.rw(), dst[3] * self.rh()];
if quad_px[0] <= 0.0 || quad_px[1] <= 0.0 {
continue;
Expand Down Expand Up @@ -1750,7 +1757,7 @@ impl Compositor {
// `font_size_rel` est une fraction de la HAUTEUR DU RECT ÉCRAN (cf. le contrat
// et `annotationScale.ts`) : on la ramène en pixels de sortie ici, avec le même
// produit que la preview applique contre sa propre boîte.
let screen_h_px = screen_dst[3] * self.rh();
let screen_h_px = s_ann[3] * self.rh();
let spec = crate::text::TextSpec {
content: text.content.clone(),
color: parse_hex(&text.color).unwrap_or([1.0, 1.0, 1.0, 1.0]),
Expand Down
137 changes: 130 additions & 7 deletions crates/compositor/src/frame_geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,44 @@ pub struct FrameGeometry {
pub shape_fade: f32,
}

/// Rect de destination d'une annotation dans un rect d'ancrage, en fractions de la sortie.
///
/// `anchor` est TOUJOURS `s_ann`, le rect écran sans le zoom — jamais `s_dst`. Les deux
/// coïncident sans zoom, ce qui rend l'erreur invisible sur la moitié des scènes ; sous
/// zoom, `s_dst` grandit et emmène annotations et sous-titres avec lui, alors que le
/// contrat de `SceneAnnotation` les veut « deliberately NOT affected by the zoom crop ».
///
/// Version libre plutôt que méthode : Windows déstructure `FrameGeometry` dès l'entrée de
/// `compose_frame`, donc il n'a plus de `&self` à offrir quand il dessine les annotations.
/// Les trois backends partagent malgré tout CETTE arithmétique-ci — le bug est reparu sur
/// Linux après avoir été corrigé sur Windows et macOS (issue #179) parce que chacun en
/// gardait sa copie.
///
/// Attention : le choix du rect passé en `anchor` reste, lui, au call site des backends
/// Metal et D3D (leur `draw_annotations` prend le rect en paramètre). Seul Linux part
/// directement de `FrameGeometry`. Passer `s_dst` ici reste donc possible sur deux
/// backends sur trois — d'où le nom du paramètre côté appelants, et les tests.
pub fn annotation_dst_in(anchor: [f32; 4], x: f32, y: f32, w: f32, h: f32) -> [f32; 4] {
[anchor[0] + x * anchor[2], anchor[1] + y * anchor[3], w * anchor[2], h * anchor[3]]
}

impl FrameGeometry {
/// `annotation_dst_in` appliqué à `s_ann`, pour les backends qui tiennent la géométrie
/// entière — c'est-à-dire ceux qui n'ont aucune raison de choisir un rect.
pub fn annotation_dst(&self, x: f32, y: f32, w: f32, h: f32) -> [f32; 4] {
annotation_dst_in(self.s_ann, x, y, w, h)
}

/// Hauteur en px du rect d'ancrage des annotations, pour `rh` px de sortie.
///
/// `font_size_rel` est une fraction de cette hauteur (cf. `annotationScale.ts`) :
/// la prendre sur `s_dst` ferait grossir le texte avec le zoom, exactement comme
/// `annotation_dst` le déplacerait.
pub fn annotation_anchor_h_px(&self, rh: f32) -> f32 {
self.s_ann[3] * rh
}
}

/// Où va chaque calque, pour une frame — sans toucher au GPU.
///
/// C'est la première moitié de `compose_frame`, mot pour mot : 353 lignes qui ne
Expand Down Expand Up @@ -1291,10 +1329,11 @@ mod tests {
}
}

/// La même scène, avec une région de zoom active à `t = 1.5 s`.
fn zoomed_golden_scene() -> Scene {
Scene::from_json(
r##"{
/// Le JSON de la scène zoomée, brut : `tilted_golden_scene` n'en change QUE la
/// rotation, et le faire par substitution garantit que les deux scènes ne diffèrent
/// pas ailleurs sans qu'on s'en aperçoive.
fn zoomed_golden_scene_json() -> &'static str {
r##"{
"clips":[{"screenPath":"/s.mp4","webcamPath":"/w.mp4","sourceStartSec":0,"sourceEndSec":10,"webcamOffsetSec":0,"hasAudio":true}],
"layout":{"preset":"picture-in-picture","webcamSize":0.44,"webcamShape":"circle","webcamMirror":false,
"webcamPosition":{"cx":0.8577,"cy":0.8159},"webcamReactiveZoom":false},
Expand All @@ -1304,9 +1343,12 @@ mod tests {
"cursor":{"show":true,"size":7.76,"smoothing":0,"motionBlur":0.35,"clickBounce":1,"clipToBounds":false,"theme":"default"},
"cropByClip":[{"x":0,"y":0,"width":0.61,"height":0.61}],
"output":{"width":1170,"height":658,"fps":60}
}"##,
)
.expect("zoomed golden scene")
}"##
}

/// La même scène, avec une région de zoom active à `t = 1.5 s`.
fn zoomed_golden_scene() -> Scene {
Scene::from_json(zoomed_golden_scene_json()).expect("zoomed golden scene")
}

/// L'ancre des annotations ne bouge PAS avec le zoom, alors que la boîte écran, si.
Expand Down Expand Up @@ -1339,6 +1381,87 @@ mod tests {
assert_eq!(a.s_ann, a.s_dst, "sans zoom, ancre et boîte écran coïncident");
}

/// La même scène, zoomée ET inclinée par un préset de rotation 3D.
fn tilted_golden_scene() -> Scene {
Scene::from_json(
&zoomed_golden_scene_json().replace(r#""rotation":"none""#, r#""rotation":"iso""#),
)
.expect("tilted golden scene")
}

/// Le rect et la taille de police d'une annotation ne bougent ni sous le zoom ni sous
/// une rotation 3D.
///
/// `the_annotation_anchor_ignores_the_zoom` prouve que `plan_frame` **calcule** la
/// bonne ancre ; il ne dit rien de ce que le backend en fait. Linux, lui, refaisait
/// l'arithmétique contre `s_dst` — donc sous-titres qui grossissent et dérivent, sur
/// la seule plateforme qui n'avait pas été corrigée. Ce test porte sur les fonctions
/// que les backends appellent maintenant, pas sur le champ brut : la méthode côté
/// Linux ET `annotation_dst_in`, par où passent Metal et D3D.
///
/// Ce qu'il ne couvre toujours PAS : le choix du rect au call site de Metal et D3D,
/// qui prennent leur ancre en paramètre. Ce niveau-là n'est vérifiable qu'en rendant
/// des pixels — c'est `compose_linux_annotation_ancree_hors_zoom`, opt-in.
///
/// La rotation compte autant que le zoom : un préset iso/left/right est une propriété
/// de région de zoom, donc l'incliner amenait aussi la boîte — et les sous-titres
/// partaient avec elle, sans pour autant suivre le plan incliné. Les deux symptômes,
/// une seule cause.
#[test]
fn the_annotation_rect_and_font_ignore_zoom_and_rotation() {
let cfg = crate::config::all().pop().expect("au moins une config");
let rh = 658.0;
// Un rect d'annotation quelconque, décentré : au centre, un rect qui suivrait le
// zoom garderait le même centre et la moitié de l'erreur passerait inaperçue.
let (x, y, w, h) = (0.04, 0.78, 0.92, 0.22);

let plain = plan_frame(&golden_input(&golden_scene(), &cfg));
let zoomed = plan_frame(&golden_input(&zoomed_golden_scene(), &cfg));
let tilted = plan_frame(&golden_input(&tilted_golden_scene(), &cfg));

// Le garde-fou : sans lui, un `plan_frame` qui cesserait d'appliquer le zoom
// rendrait les assertions suivantes vraies pour la mauvaise raison.
assert_ne!(
plain.s_dst, zoomed.s_dst,
"le zoom doit agir sur la boîte écran — sinon ce test ne prouve rien"
);
assert!(
!crate::regions::is_identity_rotation(tilted.zoom_rotation),
"le préset iso doit produire une rotation — sinon ce test ne prouve rien"
);

let expected = plain.annotation_dst(x, y, w, h);
for (name, g) in [("zoom", &zoomed), ("rotation 3D", &tilted)] {
let got = g.annotation_dst(x, y, w, h);
assert_eq!(
got, expected,
"le rect de l'annotation a suivi le {name} : {got:?} au lieu de {expected:?}"
);
assert_eq!(
g.annotation_anchor_h_px(rh),
plain.annotation_anchor_h_px(rh),
"la taille de police a suivi le {name}"
);
// Metal et D3D n'appellent pas la méthode : ils passent leur rect d'ancrage à
// `annotation_dst_in`. Les deux chemins doivent rendre le MÊME rect, sinon le
// « corrigé sur une plateforme seulement » recommence par le bas.
assert_eq!(
annotation_dst_in(g.s_ann, x, y, w, h),
got,
"le chemin des backends Metal/D3D diverge de la méthode sous le {name}"
);
// Et le garde-fou qui donne un sens aux deux précédents : nourrie avec `s_dst`,
// la même fonction rend un rect DIFFÉRENT. Sans ça, un `annotation_dst_in`
// devenu constant satisferait tout ce qui précède.
assert_ne!(
annotation_dst_in(g.s_dst, x, y, w, h),
expected,
"sous le {name}, ancrer sur `s_dst` devrait déplacer le rect — \
si les deux coïncident, ce test ne prouve plus rien"
);
}
}

/// **Le golden iso-render.**
///
/// Les deux backends ne peuvent pas tourner sur la même machine, donc « iso avec
Expand Down
Loading