Skip to content

Commit 66304d9

Browse files
committed
fix(compositor): draw the cursor under the camera and the annotations on Linux
Linux drew the cursor last, over the camera and the annotations, where Windows and macOS draw it under both. With Prism Glow's crystal, which refracts the raw recording, that order let the glass show what a privacy blur hides. The cursor now sits between the screen and the camera, as on the other platforms, and a test checks that a blur leaves no sharp edge of the crystal under it.
1 parent cf93ee5 commit 66304d9

1 file changed

Lines changed: 109 additions & 41 deletions

File tree

‎crates/compositor/src/compositor_linux.rs‎

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3105,9 +3105,9 @@ impl Compositor {
31053105
}
31063106
// Blur du fond (avant l'ecran), si active par la scene/l'inspector.
31073107
self.blur_bg(&mut encoder, cfg.bg_blur);
3108-
// Passe 2 : avant-plan (ecran + webcam), compose par-dessus le fond
3109-
// (eventuellement floute) avec `LoadOp::Load`. Les annotations sont dans
3110-
// une passe a part, cf. plus bas.
3108+
// Passe 2 : l'ecran, compose par-dessus le fond (eventuellement floute) avec
3109+
// `LoadOp::Load`. Puis le curseur, la camera et les annotations, dans cet ordre :
3110+
// celui de Windows et macOS.
31113111
{
31123112
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
31133113
label: Some("fg-pass"),
@@ -3140,40 +3140,12 @@ impl Compositor {
31403140
&device_frame,
31413141
),
31423142
}
3143-
if let Some((_buf, bind)) = &webcam_shadow {
3144-
rpass.set_bind_group(0, bind, &[]);
3145-
rpass.draw(0..4, 0..1);
3146-
}
3147-
// Fond personnalise : ENTRE l'ombre et la camera. C'est ce sandwich qui
3148-
// remplace la branche « mode 3 » du shader — la camera, decoupee, se
3149-
// fond dessus par alpha ; l'ombre reste dessous, elle appartient a la
3150-
// bulle et non a son contenu.
3151-
if let Some(bg) = &webcam_bg {
3152-
rpass.set_bind_group(0, &bg.bind, &[]);
3153-
rpass.draw(0..4, 0..1);
3154-
}
3155-
if let Some((_buf, bind)) = &webcam_draw {
3156-
rpass.set_bind_group(0, bind, &[]);
3157-
rpass.draw(0..4, 0..1);
3158-
}
3159-
}
3160-
// Fige la frame composee pour les annotations « flou ». ICI et nulle part
3161-
// ailleurs : apres l'ecran et la camera (sinon un flou masquerait du vide)
3162-
// et avant la premiere annotation (sinon deux flous qui se recouvrent
3163-
// s'echantillonnent l'un l'autre). Une passe de rendu ne peut pas lire sa
3164-
// propre cible, d'ou la copie -- et d'ou le fait que les annotations
3165-
// doivent avoir leur propre passe.
3166-
if needs_ann_copy {
3167-
self.generate_ann_mips(&mut encoder);
31683143
}
3169-
// Passe 3 : annotations puis curseur net, par-dessus tout le reste. Elle
3170-
// existe meme sans flou : deux passes consecutives sur la MEME cible avec
3171-
// `LoadOp::Load` ne coutent rien de plus qu'une seule sur un GPU
3172-
// desktop, et un seul chemin de code vaut mieux qu'un branchement qui ne
3173-
// serait exerce que dans un projet sur dix.
3144+
// Le curseur net, et l'impact des clics sous lui. La trainee, elle, a besoin de sa
3145+
// propre cible : elle suit.
31743146
{
31753147
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3176-
label: Some("ann-pass"),
3148+
label: Some("cursor-pass"),
31773149
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
31783150
view: &self.rt_view,
31793151
resolve_target: None,
@@ -3187,18 +3159,11 @@ impl Compositor {
31873159
occlusion_query_set: None,
31883160
});
31893161
rpass.set_pipeline(&self.pipeline);
3190-
for a in &ann_draws {
3191-
rpass.set_bind_group(0, &a.bind, &[]);
3192-
rpass.draw(0..4, 0..1);
3193-
}
31943162
// L'impact des clics, sous le curseur et sa trainee (dessinee plus bas).
31953163
for bind in cursor_draw.iter().flat_map(|c| &c.impacts) {
31963164
rpass.set_bind_group(0, bind, &[]);
31973165
rpass.draw(0..4, 0..1);
31983166
}
3199-
// Curseur en dernier : au-dessus de l'ecran et des annotations.
3200-
// Une seule copie = curseur net, il tient dans cette pass. La
3201-
// trainee, elle, a besoin de sa propre cible (voir plus bas).
32023167
if let Some(c) = cursor_draw.as_ref().filter(|c| c.binds.len() == 1) {
32033168
rpass.set_bind_group(0, &c.binds[0], &[]);
32043169
rpass.draw(0..4, 0..1);
@@ -3262,6 +3227,76 @@ impl Compositor {
32623227
rpass.set_bind_group(0, abind, &[]);
32633228
rpass.draw(0..3, 0..1);
32643229
}
3230+
// La camera, au-dessus de l'ecran et du curseur.
3231+
{
3232+
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3233+
label: Some("webcam-pass"),
3234+
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3235+
view: &self.rt_view,
3236+
resolve_target: None,
3237+
ops: wgpu::Operations {
3238+
load: wgpu::LoadOp::Load,
3239+
store: wgpu::StoreOp::Store,
3240+
},
3241+
})],
3242+
depth_stencil_attachment: None,
3243+
timestamp_writes: None,
3244+
occlusion_query_set: None,
3245+
});
3246+
rpass.set_pipeline(&self.pipeline);
3247+
if let Some((_buf, bind)) = &webcam_shadow {
3248+
rpass.set_bind_group(0, bind, &[]);
3249+
rpass.draw(0..4, 0..1);
3250+
}
3251+
// Fond personnalise : ENTRE l'ombre et la camera. C'est ce sandwich qui
3252+
// remplace la branche « mode 3 » du shader — la camera, decoupee, se
3253+
// fond dessus par alpha ; l'ombre reste dessous, elle appartient a la
3254+
// bulle et non a son contenu.
3255+
if let Some(bg) = &webcam_bg {
3256+
rpass.set_bind_group(0, &bg.bind, &[]);
3257+
rpass.draw(0..4, 0..1);
3258+
}
3259+
if let Some((_buf, bind)) = &webcam_draw {
3260+
rpass.set_bind_group(0, bind, &[]);
3261+
rpass.draw(0..4, 0..1);
3262+
}
3263+
}
3264+
// Fige la frame composee pour les annotations « flou ». ICI et nulle part
3265+
// ailleurs : apres l'ecran, le curseur et la camera (sinon un flou masquerait du vide)
3266+
// et avant la premiere annotation (sinon deux flous qui se recouvrent
3267+
// s'echantillonnent l'un l'autre). Une passe de rendu ne peut pas lire sa
3268+
// propre cible, d'ou la copie -- et d'ou le fait que les annotations
3269+
// doivent avoir leur propre passe.
3270+
if needs_ann_copy {
3271+
self.generate_ann_mips(&mut encoder);
3272+
}
3273+
// Passe 3 : les annotations, par-dessus tout le reste, curseur compris : un flou de
3274+
// confidentialite doit le couvrir, sans quoi le cristal de Prism Glow, qui refracte
3275+
// l'enregistrement brut, montrerait ce que le flou cache. Elle existe meme sans flou :
3276+
// deux passes consecutives sur la MEME cible avec `LoadOp::Load` ne coutent rien de plus
3277+
// qu'une seule sur un GPU desktop, et un seul chemin de code vaut mieux qu'un
3278+
// branchement qui ne serait exerce que dans un projet sur dix.
3279+
{
3280+
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3281+
label: Some("ann-pass"),
3282+
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3283+
view: &self.rt_view,
3284+
resolve_target: None,
3285+
ops: wgpu::Operations {
3286+
load: wgpu::LoadOp::Load,
3287+
store: wgpu::StoreOp::Store,
3288+
},
3289+
})],
3290+
depth_stencil_attachment: None,
3291+
timestamp_writes: None,
3292+
occlusion_query_set: None,
3293+
});
3294+
rpass.set_pipeline(&self.pipeline);
3295+
for a in &ann_draws {
3296+
rpass.set_bind_group(0, &a.bind, &[]);
3297+
rpass.draw(0..4, 0..1);
3298+
}
3299+
}
32653300
self.gpu.context.submit(std::iter::once(encoder.finish()));
32663301
Ok(())
32673302
}
@@ -5807,6 +5842,39 @@ mod tests {
58075842
assert!(failures.is_empty(), "{failures:#?}");
58085843
}
58095844

5845+
/// Un flou de confidentialité couvre le curseur, comme sous Windows et macOS. Le cristal de
5846+
/// Prism Glow réfracte l'enregistrement brut : dessiné par-dessus le flou, il montrerait ce
5847+
/// que le flou cache. Sous le flou, ce que change le curseur reste flou : aucun bord franc, là
5848+
/// où le serti du cristal, dessiné par-dessus, en ferait des centaines.
5849+
#[test]
5850+
fn a_privacy_blur_covers_the_crystal_cursor() {
5851+
let Some(gpu) = gpu() else { return };
5852+
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("Compositor::new_sized");
5853+
let (y, uv) = model_screen_planes(false);
5854+
let blue = FakeFrame::from_planes(&gpu, 640, 360, &y, &uv);
5855+
let blur = r#""annotations":[{"id":"b","startSec":0,"endSec":10,"kind":"blur","x":0.3,"y":0.3,"w":0.6,"h":0.7,"blur":{"style":"blur","shape":"rectangle","color":"white","intensity":24,"blockSize":16}}]"#;
5856+
let scene = |show: bool| {
5857+
model_scene_json("null", Some(true), "default", show, 5.0)
5858+
.replace(r#"/arrow.png","#, r#"/arrow.png","sculpt":"prism-glow/arrow","#)
5859+
.replace(r#""annotations":[]"#, blur)
5860+
};
5861+
let still = model_track("arrow", false, 0.5);
5862+
let with = compose_model(&comp, &blue, &scene(true), &still);
5863+
let without = compose_model(&comp, &blue, &scene(false), &still);
5864+
model_save("privacy-blur-over-crystal", &with);
5865+
// Ce que change le curseur, pixel par pixel ; un bord franc y est un saut de plus de 40
5866+
// entre deux voisins.
5867+
let diff = |i: usize, c: usize| with[i * 4 + c] as i32 - without[i * 4 + c] as i32;
5868+
let changed = (0..1280 * 720).filter(|&i| (0..3).any(|c| diff(i, c) != 0)).count();
5869+
let sharp = (0..1280 * 719)
5870+
.filter(|&i| i % 1280 < 1279)
5871+
.filter(|&i| (0..3).any(|c| (diff(i, c) - diff(i + 1, c)).abs() > 40 || (diff(i, c) - diff(i + 1280, c)).abs() > 40))
5872+
.count();
5873+
println!("sous le flou : {changed} px changés par le curseur, {sharp} bords francs");
5874+
assert!(changed > 0, "le curseur ne change rien : il manque, ou le flou l'a effacé");
5875+
assert_eq!(sharp, 0, "{sharp} bords francs sous le flou : le curseur est dessiné par-dessus");
5876+
}
5877+
58105878
/// Les thèmes cerclés gardent le trait de leur dessin (`design/cursors/<thème>`) : sur la
58115879
/// fleche comme sur la main, le modele est pour une bonne part de la couleur du trait (le
58125880
/// plateau, le jonc, les rainures entre les doigts), autour de la couleur du corps, et montre

0 commit comments

Comments
 (0)