From 01a963cffaf8428ae4e468c1b75580616d7a0898 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Sun, 27 Sep 2026 01:10:59 +0200 Subject: [PATCH 1/2] docs(compositor): drop the retired undistort pipeline from the comments The render target follows the output now: the export builds it at the output's size, the preview at that geometry fitted to the panel. Nothing is stretched afterwards, and the undistort, inverse_undistort and uniform_stretch pass is gone. Twelve comments in compositor_windows.rs and frame_geometry.rs still described it, and one misled a reviewer on #843 into reporting a frame distortion that cannot happen. Comments only: no code or behaviour changes. --- crates/compositor/src/compositor_windows.rs | 85 +++++++++------------ crates/compositor/src/frame_geometry.rs | 46 +++-------- 2 files changed, 48 insertions(+), 83 deletions(-) diff --git a/crates/compositor/src/compositor_windows.rs b/crates/compositor/src/compositor_windows.rs index c86c45b72..bbae15703 100644 --- a/crates/compositor/src/compositor_windows.rs +++ b/crates/compositor/src/compositor_windows.rs @@ -194,23 +194,26 @@ pub struct Compositor { /// Historiquement c'était la constante `OUT_W`×`OUT_H` : un canvas 16:9 figé, /// étiré en fin de pipeline vers la vraie sortie. Cette constante produisait /// deux défauts distincts, tous deux issus d'elle seule : - /// - une **forme** fausse dès que la sortie n'est pas 16:9 → rattrapée en - /// aval par `apply_undistort` (9 correctifs successifs sur l'écran, la - /// webcam, le curseur, les ombres, les coins, le crop, le fond) ; + /// - une **forme** fausse dès que la sortie n'est pas 16:9 → rattrapée à + /// l'époque en aval par `apply_undistort` (9 correctifs successifs sur + /// l'écran, la webcam, le curseur, les ombres, les coins, le crop, le fond) ; /// - une **résolution** plafonnée → jamais rattrapée, parce qu'aucun /// correctif au niveau du calque ne peut recréer des pixels qui n'ont pas /// été rastérisés (un export 4K était du 1080p agrandi). /// - /// Rendre cette taille variable retire la cause commune. `OUT_W`/`OUT_H` ne - /// sont plus qu'une valeur par défaut, jamais une référence géométrique. + /// Rendre cette taille variable a retiré la cause commune, et `apply_undistort` + /// avec elle : le RT porte la géométrie de sortie. L'export le crée à la taille de + /// sortie, la preview à cette géométrie ramenée au panneau (`preview_render_size`). + /// `OUT_W`/`OUT_H` ne sont plus qu'une valeur par défaut, jamais une référence + /// géométrique. render_size: Cell<(u32, u32)>, - /// Ressources de resize export (allouées paresseusement à la 1re taille de sortie ≠ - /// OUT_W×OUT_H — le live et les exports "Source"/1080p restent sur `rgb_to_nv12` inchangé, - /// zéro coût). Voir `rgb_to_nv12_scaled`. + /// Ressources de resize, allouées paresseusement à la première cible qui diffère de la + /// taille de rendu. Le RT suivant la sortie, le cas nominal n'y passe pas et ne paie rien. + /// Voir `rgb_to_nv12_scaled`. resize_target: RefCell>, - /// Cache de la staging texture de readback live, dimensionnée à la dernière taille - /// de prévisualisation demandée (variable, contrairement au `staging` fixe à - /// OUT_W×OUT_H). Recréée quand la taille change — voir `readback_resized`. + /// Cache de la staging texture de `readback_resized`, dimensionnée à la dernière taille + /// demandée (le `staging` principal, lui, suit la taille de rendu). Recréée quand la taille + /// change. La preview n'y passe plus : elle relit le RT tel quel (`readback_direct`). live_readback_staging: RefCell>, /// Cible + staging pour extraire la frame webcam à la résolution du modèle de /// segmentation. Créée à la première capture, jamais redimensionnée : le modèle a une @@ -243,10 +246,9 @@ pub struct Compositor { cpu_backend: bool, } -/// Ressources d'un resize export à une taille cible : RGBA intermédiaire (résultat du -/// redimensionnement bilinéaire du RT composé, toujours rendu en interne à OUT_W×OUT_H) + -/// sa propre texture NV12 à cette même taille cible (le NV12 principal du `Compositor` reste -/// fixé à OUT_W×OUT_H, partagé par le live). +/// Ressources d'un resize à une taille cible : RGBA intermédiaire (le RT composé, rendu à la +/// taille de rendu, redimensionné en bilinéaire) + sa propre texture NV12 à cette même taille +/// cible (le NV12 principal du `Compositor` reste à la taille de rendu). struct ResizeTarget { w: u32, h: u32, @@ -1113,12 +1115,9 @@ impl Compositor { ) -> Result<()> { let (srv, iw, ih) = self.cached_image(path)?; let ai = iw as f32 / ih as f32; - // Le fond remplit TOUJOURS le cadre (dst=[0,0,1,1], jamais rétréci par `undistort`), - // mais le canvas interne est un 16:9 fixe étiré ensuite vers le VRAI ratio de sortie - // (`blit_resized`, non uniforme) : le crop "cover" doit donc être calculé contre ce vrai - // ratio de sortie (`output_aspect`, = final_out_w/final_out_h), pas contre le ratio fixe - // du canvas — sinon l'image, déjà cover-fittée pour du 16:9, se retrouve re-déformée par - // l'étirement final vers un ratio différent (ex. 9:16, cf. rapport utilisateur). + // Crop « cover » contre le ratio de la zone remplie (`output_aspect`) : l'image garde ses + // proportions, rognée sur l'axe en trop. Le RT porte la géométrie de sortie, donc rien ne + // la ré-étire ensuite. let ao = output_aspect; let (u0, v0, u1, v1) = if ai > ao { let vis = ao / ai; // rogne horizontalement @@ -1753,17 +1752,11 @@ impl Compositor { } /// Ombre portée (§7 E4) sous un quad `dst` (normalisé) de taille `size_px`. - /// Le quad d'ombre est élargi de `spread` px et décalé de `offset_px`. - /// `spread`/`offset_px` sont des px RÉELS de la sortie finale (même convention que - /// `radius_px` pour l'arrondi normal, cf. `compose_frame`) — PAS des px du canvas fixe - /// 16:9. Convertis ici en marge/décalage CANVAS (avant l'étirement final anisotrope de - /// `blit_resized`), par axe (`/stretch_x`, `/stretch_y`), pour que ce halo redevienne un - /// vrai halo isotrope une fois cet étirement appliqué — sans ça (ancien calcul : marge - /// identique en fraction canvas quel que soit l'axe) l'ombre ressort visiblement elliptique - /// dès que la sortie n'est pas 16:9 (rapport utilisateur, ex. export vertical 9:16). - /// `stretch_x`/`stretch_y` sont aussi transmis au shader (`mb.yz`) pour pré-déformer la SDF - /// elle-même — même technique que l'arrondi normal (mode 0) — sinon la COURBURE des coins - /// de l'ombre reste elliptique même une fois sa taille globale corrigée. + /// Le quad d'ombre est élargi de `spread` px et décalé de `offset_px`, des px de SORTIE + /// (même convention que `radius_px` pour l'arrondi normal, cf. `compose_frame`), convertis + /// ici en fractions du render target, axe par axe. Le RT porte la géométrie de sortie : le + /// halo reste isotrope sans correction. `mb.yz` reste à 1 et le shader ne le lit plus : du + /// temps du canvas figé en 16:9, il portait l'étirement de sortie que la SDF devait annuler. pub unsafe fn draw_shadow( &self, dst: [f32; 4], @@ -2745,17 +2738,14 @@ impl Compositor { Ok(()) } - /// Redimensionne (bilinéaire) le RT composé (OUT_W×OUT_H) vers `resize_target.rgba`, avant - /// la conversion NV12 dans `rgb_to_nv12_scaled`. + /// Redimensionne (bilinéaire) le RT composé, à sa taille de rendu, vers `resize_target.rgba` + /// (`target_w`×`target_h`), avant la conversion NV12 dans `rgb_to_nv12_scaled`. /// - /// Étirement PLEIN CADRE volontaire, y compris non uniforme quand `target_w`×`target_h` - /// n'a pas le ratio de OUT_W×OUT_H : le fond (wallpaper) doit remplir tout le cadre de - /// sortie quel que soit le ratio choisi — ce n'est PAS lui qu'il faut préserver en "fit". - /// L'écran et la webcam, eux, sont protégés de cet étirement en amont, dans - /// `compose_frame` (rétrécissement inverse de leur rect de destination AVANT ce blit — - /// voir le commentaire sur `undistort` juste avant leur dessin) : ils gardent leur ratio - /// d'origine (letterboxé/pillarboxé sur le fond, qui lui reste plein cadre) sans qu'il - /// faille toucher au viewport ici. + /// Plein cadre, sans viewport ni bandes. Le RT porte déjà la géométrie de sortie (l'export + /// le crée à la taille de sortie) et les appelants demandent cette géométrie : l'échelle est + /// donc la même sur les deux axes, à l'arrondi au pair de `normalize_render_size` près. Rien + /// n'est pré-compensé en amont : la passe `undistort`, qui rétrécissait l'écran et la webcam + /// avant un étirement non uniforme, a disparu avec le canvas figé en 16:9. unsafe fn blit_resized(&self, target_w: u32, target_h: u32) -> Result<()> { self.ensure_resize_target(target_w, target_h)?; let cache = self.resize_target.borrow(); @@ -2916,12 +2906,11 @@ impl Compositor { Ok((rw, rh, out)) } - /// Comme `rgb_to_nv12`, mais redimensionne d'abord (bilinéaire, `ps_tex`/`sampler` déjà - /// utilisés partout ailleurs dans le fichier) le RT composé — toujours rendu en interne à - /// OUT_W×OUT_H, quelle que soit la taille de sortie demandée — vers `target_w`×`target_h` - /// avant la conversion NV12. Identique à `rgb_to_nv12` (donc coût inchangé) quand la cible - /// égale la résolution interne : le live et les exports "Source"/1080p ne paient rien pour - /// cette fonctionnalité. + /// Comme `rgb_to_nv12`, mais vers `target_w`×`target_h` : si la cible diffère de la taille + /// de rendu, le RT composé est d'abord redimensionné (bilinéaire, `ps_tex`/`sampler` déjà + /// utilisés partout ailleurs dans le fichier, cf. `blit_resized`). Le RT suivant la sortie, + /// c'est l'exception : dans le cas nominal la cible est la taille de rendu, et le coût est + /// celui de `rgb_to_nv12`. pub unsafe fn rgb_to_nv12_scaled( &self, target_w: u32, diff --git a/crates/compositor/src/frame_geometry.rs b/crates/compositor/src/frame_geometry.rs index 59d5ae31f..c64b98fff 100644 --- a/crates/compositor/src/frame_geometry.rs +++ b/crates/compositor/src/frame_geometry.rs @@ -2984,13 +2984,6 @@ pub fn plan_frame(input: &FrameGeometryInput) -> FrameGeometry { let (nw, nh) = (dst[2] * s, dst[3] * s); [ax - nw * anchor[0], ay - nh * anchor[1], nw, nh] }; - // Le ratio de sortie réel (peut différer du canvas interne 16:9 fixe) et le facteur - // d'étirement non uniforme que `blit_resized` appliquera en fin de pipeline — nécessaires - // ici (avant `undistort`, plus bas) pour que le fit ci-dessous cible le ratio de boîte tel - // qu'il apparaîtra APRÈS cet étirement, pas tel qu'il est dans l'espace canvas pré-étirement - // (sinon le fit et l'undistort composent deux corrections indépendantes et sur-rétrécissent - // le contenu — cf. rapport utilisateur : crop 9:16 + sortie 9:16 + padding 0% laissait - // quand même une grosse marge, alors que le crop correspond déjà exactement au cadre). // Le crop de l'utilisateur (dialogue "Edit clip") a son PROPRE ratio (ex. une bande // verticale 9:16 recadrée dans une source 16:9) — le zoom appliqué ensuite (§ // `screen_source_rect`) le préserve (mêmes facteurs sur les deux axes), donc c'est bien @@ -3006,13 +2999,8 @@ pub fn plan_frame(input: &FrameGeometryInput) -> FrameGeometry { _ => scw / sch.max(0.0001), }; // Contain (parité `centerRectInBounds`) : rétrécit `dst` (centré) pour que son ratio - // devienne `aspect`, sans jamais dépasser sa boîte d'origine — mais la boîte de référence - // doit être mesurée telle qu'elle apparaîtra APRÈS l'étirement de sortie (`dst` * ratio de - // sortie), pas dans l'espace canvas 16:9 pré-étirement : sinon le fit cible le mauvais - // ratio de boîte dès que la sortie n'est pas 16:9. `undistort` (plus bas) annule ensuite - // exactement ce même facteur, donc convertir le résultat en fraction canvas se fait par - // `/ uniform_stretch` (propriété de `undistort` : le ratio final ne dépend que de la - // taille de `dst` en PIXELS CANVAS, jamais du ratio de sortie choisi). + // devienne `aspect`, sans jamais dépasser sa boîte d'origine. La boîte se mesure en px du + // render target, qui porte la géométrie de sortie : son ratio est celui qu'on verra. let fit_dst_to_aspect = |dst: [f32; 4], aspect: f32| -> [f32; 4] { let box_w_px = dst[2] * rw; let box_h_px = dst[3] * rh; @@ -3188,18 +3176,14 @@ pub fn plan_frame(input: &FrameGeometryInput) -> FrameGeometry { // le padding n'affecte QUE l'écran (la quantité de fond révélée). La webcam reste ancrée // en bas-droite à sa marge fixe, quelle que soit la valeur de padding (pas de scale_frame) // — SAUF quand l'app a résolu un placement explicite (`app_webcam_rect`, drag-to-reposition - // compris). Ce rect est déjà exprimé en fraction du VRAI output (calculé côté web par + // compris). Ce rect est déjà exprimé en fraction de la sortie (calculé côté web par // `computeCompositeLayout` avec les vraies dimensions de sortie), position ET aspect déjà - // corrects — `fit_cam_aspect`/`scale_corner_br` (chemin preset par défaut) sont donc - // doublement inadaptés ici : ils réancrent au coin bas-droite (ignorant la position - // choisie par l'utilisateur) ET recalculent l'aspect en pixels du canvas fixe 16:9 - // (`OUT_W`×`OUT_H`), une référence différente du vrai output dès que la sortie n'est pas - // 16:9 (rapport utilisateur : webcam glissée au coin bas-gauche en 9:16, JSON envoyé au - // natif confirmant une position flush, mais rendu native visiblement décalé ET trop - // petit). On garde seulement `scale_anchored` (zoom réactif, préserve l'ancre et l'aspect) - // puis on pré-compense par `inverse_undistort` pour annuler le `undistort()` générique - // appliqué plus bas à tous les calques (écran compris) — sans quoi ce rect déjà correct - // se ferait déformer une seconde fois par cet undistort partagé. + // corrects. `fit_cam_aspect`/`scale_corner_br` (chemin preset par défaut) y sont donc + // inadaptés : ils le réancreraient au coin bas-droite, en ignorant la position choisie par + // l'utilisateur (rapport : webcam glissée au coin bas-gauche, rendue ailleurs), et + // recalculeraient un aspect déjà juste. On garde seulement `scale_anchored` (zoom réactif, + // préserve l'ancre et l'aspect), et le rect est dessiné tel quel : le render target porte + // la géométrie de sortie, rien ne le déforme après coup. let mut w_dst = if app_webcam_rect.is_some() { scale_anchored(p.webcam.dst, webcam_size_scale) } else { @@ -3231,16 +3215,8 @@ pub fn plan_frame(input: &FrameGeometryInput) -> FrameGeometry { w_dst = fullscreen_dst(w_dst, cam_progress); w_dst_prev = fullscreen_dst(w_dst_prev, cam_progress_prev); - // Contre-étirement "fit" : le canvas interne compose TOUJOURS en OUT_W×OUT_H (16:9), - // puis `blit_resized` étire tout, de façon non uniforme si besoin, vers la résolution - // de sortie demandée — voulu pour que le FOND (dessiné plus bas en dst=[0,0,1,1]) - // remplisse tout le cadre quel que soit le ratio choisi. Mais l'écran et la webcam ne - // doivent PAS être déformés par cet étirement : on rétrécit ici leur rect de - // destination (centré, dans cet espace 16:9 PRÉ-étirement) par l'inverse du plus fort - // des deux facteurs d'étirement, pour qu'après l'étirement final leur ratio d'origine - // reste préservé (letterboxé/pillarboxé sur le fond, qui lui reste plein cadre) — mode - // "fit"/contain. Si l'utilisateur veut un rendu "fill" (remplir sans bandes), il ajuste - // le crop lui-même ; le natif ne fait plus ce choix à sa place en étirant l'image. + // Aucun contre-étirement ici : le render target porte la géométrie de sortie, rien n'est + // étiré après coup, donc aucun rect n'est rétréci pour compenser. // Le dessin du coin (SDF, shaders.hlsl) compare le rayon à `quad_px`, exprimé en px du // RENDER TARGET : c'est donc dans cet espace-là qu'il faut le lui donner. // From 2f6a43884f70ce1cd863d648e453d0d746321fb2 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Sun, 27 Sep 2026 01:26:19 +0200 Subject: [PATCH 2/2] docs(compositor): name where OUT_W and OUT_H still place things --- crates/compositor/src/compositor_windows.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/compositor/src/compositor_windows.rs b/crates/compositor/src/compositor_windows.rs index bbae15703..ff2543212 100644 --- a/crates/compositor/src/compositor_windows.rs +++ b/crates/compositor/src/compositor_windows.rs @@ -204,8 +204,10 @@ pub struct Compositor { /// Rendre cette taille variable a retiré la cause commune, et `apply_undistort` /// avec elle : le RT porte la géométrie de sortie. L'export le crée à la taille de /// sortie, la preview à cette géométrie ramenée au panneau (`preview_render_size`). - /// `OUT_W`/`OUT_H` ne sont plus qu'une valeur par défaut, jamais une référence - /// géométrique. + /// `OUT_W`/`OUT_H` ne sont plus qu'une valeur par défaut : la taille du RT de + /// `Compositor::new`, et la référence en px des placements de repli + /// (`preset_placements`, la fixture `timeline`) quand la scène n'apporte pas ses + /// rects. Une scène de l'app, qui fournit ses rects, ne s'y réfère pas. render_size: Cell<(u32, u32)>, /// Ressources de resize, allouées paresseusement à la première cible qui diffère de la /// taille de rendu. Le RT suivant la sortie, le cas nominal n'y passe pas et ne paie rien.