Skip to content
Draft
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
120 changes: 106 additions & 14 deletions crates/compositor/src/compositor_linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub use crate::frame_geometry::{
};
use crate::frame_geometry::{
cursor_sprite_dst, parse_hex, plan_cursor, plan_frame, CursorPlacement, CursorPlanInput,
FrameGeometryInput,
FrameGeometryInput, ShadowCaster,
};
use crate::scene::{Scene, SceneBackground};

Expand Down Expand Up @@ -1976,6 +1976,9 @@ impl Compositor {
// `plane_px` dans `dst_prev`). Les deux sens ne peuvent pas cohabiter dans
// un meme draw. macOS et Windows sautent egalement le flou sur le chemin
// incline, pour la meme raison.
// Sous un cadre de fenetre, l'ecran garde ses coins HAUTS carres (`mb.w` au mode 0,
// `dst_prev.z` au mode 8) ; 0 sans cadre, soit le rendu d'avant.
let square_top = g.screen_square_top();
let screen_layer = match tilt.as_ref() {
None => LayerCB {
dst: g.s_dst,
Expand All @@ -1986,10 +1989,14 @@ impl Compositor {
color: [1.0, 1.0, 1.0, 1.0],
src_prev: g.cut,
dst_prev: g.s_dst_prev,
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
mb: [g.mb_taps, g.mb_amount, 1.0, square_top],
..Default::default()
},
Some(quad) => self.tilted_screen_cb(quad, s_px, quad_center_px, g.cut, g.s_radius),
Some(quad) => {
let mut cb = self.tilted_screen_cb(quad, s_px, quad_center_px, g.cut, g.s_radius);
cb.dst_prev[2] = square_top;
cb
}
};
// Bind group construit AVANT le pass (doit vivre pendant tout le pass) ;
// `_screen_uniform` garde le buffer uniforme en vie (reference par le bind).
Expand All @@ -2007,24 +2014,26 @@ impl Compositor {
// plateformes quelle que soit la resolution de sortie.
//
// L'ombre suit la silhouette REELLEMENT affichee : rect arrondi (mode 2)
// quand l'ecran est droit, quadrilatere projete (mode 12) quand il penche.
// quand l'ecran est droit, quadrilatere projete (mode 12) quand il penche. Avec un
// cadre de fenetre, c'est le CADRE qui la porte (`shadow_caster`), sinon la barre de
// titre flotterait au-dessus de l'ombre.
let screen_shadow = cfg.shadow.then(|| {
let spread = crate::frame_geometry::SCREEN_SHADOW_SPREAD_FRAC * g.frame_min_px;
let offset = [0.0, crate::frame_geometry::SCREEN_SHADOW_OFFSET_FRAC * g.frame_min_px];
let opacity = 0.45 * lp.shadow_scale;
let cb = match tilt.as_ref() {
None => self.shadow_cb(g.s_dst, s_px, g.s_radius, spread, offset, opacity),
Some(quad) => self.quad_shadow_cb(
&quad.corners,
quad_center_px,
g.s_radius * quad.scale,
spread,
offset,
opacity,
),
let cb = match g.shadow_caster([rw, rh]) {
ShadowCaster::Upright { dst, size_px, radius } => {
self.shadow_cb(dst, size_px, radius, spread, offset, opacity)
}
ShadowCaster::Tilted { corners, center_px, radius } => {
self.quad_shadow_cb(&corners, center_px, radius, spread, offset, opacity)
}
};
self.make_bind(&cb, None, &dummy)
});
// Le cadre (mode 14), dessine entre l'ombre et l'ecran : l'ecran le recouvre et ne
// laisse voir que la barre de titre et le filet.
let window_frame = g.window_frame_cb([rw, rh]).map(|cb| self.make_bind(&cb, None, &dummy));

// Fond (gradient mode 5 OU image mode 6), dessine dans la passe de fond.
let bg_draw = bg_layer.and_then(|bl| match bl {
Expand Down Expand Up @@ -2780,6 +2789,10 @@ impl Compositor {
rpass.set_bind_group(0, bind, &[]);
rpass.draw(0..4, 0..1);
}
if let Some((_buf, bind)) = &window_frame {
rpass.set_bind_group(0, bind, &[]);
rpass.draw(0..4, 0..1);
}
rpass.set_bind_group(0, &screen_bind, &[]);
rpass.draw(0..4, 0..1);
if let Some((_buf, bind)) = &webcam_shadow {
Expand Down Expand Up @@ -4532,6 +4545,85 @@ mod tests {
rgba
}
}

/// Ecran gris sur fond gris moyen, avec ou sans cadre de fenetre (`frame` est insere tel
/// quel dans `effects`), droit ou incline (`rotation`, JSON).
fn compose_framed(comp: &Compositor, gpu: &Gpu, frame: &str, rotation: &str) -> Vec<u8> {
let json = format!(
r##"{{"clips":[{{"screenPath":"/s.mp4","webcamPath":"","sourceStartSec":0,"sourceEndSec":10,"webcamOffsetSec":0,"hasAudio":false}}],
"layout":{{"preset":"no-webcam","webcamSize":1,"webcamShape":"rounded",
"webcamMirror":false,"webcamPosition":null,"webcamReactiveZoom":false,
"screenRect":{{"x":0.1,"y":0.1,"width":0.8,"height":0.8}}}},
"effects":{{"padding":0.2,"blur":false,"shadow":0.6,"roundnessFrac":0.03,"motionBlur":0{frame}}},
"background":{{"kind":"color","color":"#6070a0"}},
"zoomRegions":[{{"clipIndex":0,"startSec":0,"endSec":10,"scale":1,"focusX":0.5,"focusY":0.5,"rotation":{rotation}}}],
"annotations":[],
"cursor":{{"show":false,"size":1,"smoothing":0,"motionBlur":0,"clickBounce":0,
"clipToBounds":false,"theme":"default"}},
"cropByClip":[null],
"output":{{"width":1280,"height":720,"fps":30}}}}"##
);
let scene = Scene::from_json(&json).expect("scene json");
comp.set_live_params(live_params_from_scene(&scene));
comp.set_has_webcam(false);
comp.set_scene(Some(scene));
let screen = FakeFrame::new(gpu, 640, 360, |_, _| 60);
let webcam = FakeFrame::new(gpu, 64, 64, |_, _| 60);
let mut cfg = Cfg::c8();
cfg.bg_blur = false;
cfg.zoom = false;
cfg.layout_anim = false;
cfg.cursor = false;
cfg.mblur_n = 1;
cfg.shadow = true;
unsafe {
comp.set_timeline_time(Some(2.0));
comp.compose_frame(screen.as_ptr(), webcam.as_ptr(), 0.0, &cfg)
.expect("compose_frame");
let (_, _, rgba) = comp.readback_direct().expect("readback_direct");
rgba
}
}

/// Le cadre de fenetre (mode 14) se dessine sur Linux comme ailleurs : `"none"` rend
/// l'image d'avant a l'octet, le theme clair ajoute une barre claire, droite comme
/// inclinee, et les deux themes different. `OPENSCREEN_FRAME_OUT` recoit les PNG.
#[test]
fn the_window_frame_draws_its_title_bar_flat_and_tilted() {
let Some(gpu) = gpu() else { return };
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("Compositor::new_sized");
let out_dir = std::env::var("OPENSCREEN_FRAME_OUT").ok();
let bright = |rgba: &[u8]| {
rgba.chunks_exact(4).filter(|p| p[0] > 215 && p[1] > 215 && p[2] > 215).count()
};
let differing = |a: &[u8], b: &[u8]| {
a.chunks_exact(4)
.zip(b.chunks_exact(4))
.filter(|(p, q)| p.iter().zip(q.iter()).take(3).any(|(x, y)| x.abs_diff(*y) > 8))
.count()
};
for (name, rotation) in [("flat", "null"), ("iso", r#""iso""#)] {
let absent = compose_framed(&comp, &gpu, "", rotation);
let none = compose_framed(&comp, &gpu, r#","frame":"none""#, rotation);
let light = compose_framed(&comp, &gpu, r#","frame":"window-light""#, rotation);
let dark = compose_framed(&comp, &gpu, r#","frame":"window-dark""#, rotation);
if let Some(dir) = &out_dir {
for (theme, rgba) in [("light", &light), ("dark", &dark)] {
let path = format!("{dir}/linux-{name}-{theme}.png");
image::RgbaImage::from_raw(1280, 720, rgba.clone())
.expect("dimensions du readback")
.save(&path)
.unwrap_or_else(|e| panic!("ecriture {path} : {e}"));
}
}
assert_eq!(absent, none, "{name}: frame none a change des pixels");
assert_eq!(bright(&none), 0, "{name}: le temoin a deja des pixels clairs");
// Barre de titre : ~0.04 x 576 px de haut sur ~1000 px de large, moins l'inclinaison.
assert!(bright(&light) > 10_000, "{name}: barre claire absente ({} px)", bright(&light));
assert!(differing(&none, &dark) > 10_000, "{name}: cadre sombre absent");
assert!(differing(&light, &dark) > 10_000, "{name}: les deux themes se confondent");
}
}
}

// ---------------------------------------------------------------------------
Expand Down
35 changes: 20 additions & 15 deletions crates/compositor/src/compositor_macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use crate::ffi::AVFrame;
pub use crate::frame_geometry::{
live_params_from_scene, webcam_shape_code, FIXTURE_FRAMES, LayerCB, LiveParams, OUT_H, OUT_W,
};
use crate::frame_geometry::{parse_hex, FrameGeometryInput, SCREEN_SHADOW_OFFSET_FRAC,
use crate::frame_geometry::{parse_hex, FrameGeometryInput, ShadowCaster, SCREEN_SHADOW_OFFSET_FRAC,
SCREEN_SHADOW_SPREAD_FRAC, WEBCAM_SHADOW_OFFSET_FRAC, WEBCAM_SHADOW_OPACITY,
WEBCAM_SHADOW_SPREAD_FRAC};
use crate::scene::{Scene, SceneBackground};
Expand Down Expand Up @@ -1132,6 +1132,7 @@ impl Compositor {

/// Écran incliné (mode 8) : warp bilinéaire inverse dans la bbox du quad projeté.
/// Pas de motion blur sur ce chemin — le tilt est bref, la simplification ne se voit pas.
#[allow(clippy::too_many_arguments)]
unsafe fn draw_tilted_screen(
&self,
enc: &metal::RenderCommandEncoderRef,
Expand All @@ -1140,6 +1141,8 @@ impl Compositor {
center_px: [f32; 2],
cut: [f32; 4],
radius: f32,
// 1 : coins hauts carrés, l'écran est sous la barre d'un cadre (`dst_prev.z`).
square_top: f32,
y: &metal::Texture,
uv: &metal::Texture,
) {
Expand Down Expand Up @@ -1175,7 +1178,7 @@ impl Compositor {
mode: 8.0,
fx: [tl0, tl1, tr0, tr1],
src_prev: [br0, br1, bl0, bl1],
dst_prev: [plane_px[0], plane_px[1], 0.0, 0.0],
dst_prev: [plane_px[0], plane_px[1], square_top, 0.0],
..Default::default()
},
y,
Expand Down Expand Up @@ -2084,20 +2087,22 @@ impl Compositor {
let opacity = 0.45 * lp.shadow_scale;
// L'ombre suit la silhouette réellement affichée : rect arrondi quand l'écran est
// droit, quadrilatère projeté quand il est penché. Un rect droit derrière un écran
// incliné se lit comme une seconde surface, pas comme son ombre.
match tilt.as_ref() {
None => self.draw_shadow(enc, g.s_dst, s_px, g.s_radius, spread, offset, opacity),
Some(quad) => self.draw_quad_shadow(
enc,
&quad.corners,
quad_center_px,
g.s_radius * quad.scale,
spread,
offset,
opacity,
// incliné se lit comme une seconde surface, pas comme son ombre. Avec un cadre de
// fenêtre, c'est le CADRE qui la porte (`shadow_caster`).
match g.shadow_caster([rw, rh]) {
ShadowCaster::Upright { dst, size_px, radius } => {
self.draw_shadow(enc, dst, size_px, radius, spread, offset, opacity)
}
ShadowCaster::Tilted { corners, center_px, radius } => self.draw_quad_shadow(
enc, &corners, center_px, radius, spread, offset, opacity,
),
}
}
// Le cadre (mode 14) passe SOUS l'écran, qui ne laisse voir que la barre et le filet.
if let Some(cb) = g.window_frame_cb([rw, rh]) {
self.draw_solid(enc, &cb);
}
let square_top = g.screen_square_top();
let [su0, sv0, su1, sv1] = g.cut;
match tilt.as_ref() {
None => self.draw_video(
Expand All @@ -2111,14 +2116,14 @@ impl Compositor {
color: [0.0, 0.0, 0.0, 1.0],
src_prev: [su0, sv0, su1, sv1],
dst_prev: g.s_dst_prev,
mb: [g.mb_taps, g.mb_amount, 1.0, 0.0],
mb: [g.mb_taps, g.mb_amount, 1.0, square_top],
..Default::default()
},
&sy,
&suv,
),
Some(quad) => self.draw_tilted_screen(
enc, quad, s_px, quad_center_px, g.cut, g.s_radius, &sy, &suv,
enc, quad, s_px, quad_center_px, g.cut, g.s_radius, square_top, &sy, &suv,
),
}

Expand Down
34 changes: 20 additions & 14 deletions crates/compositor/src/compositor_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub use crate::frame_geometry::{live_params_from_scene, webcam_shape_code, Layer
use crate::frame_geometry::{
cover_crop_uv, cover_uv_rect, cursor_sprite_dst, decode_data_uri, ease_in_out_cubic, lerp,
lerp4, parse_hex, preset_placements, remap_box, screen_source_rect, timeline, CursorPlacement,
FrameParams, Placement, CURSOR_BASE_SIZE_FRAC, FPS, SCREEN_SHADOW_OFFSET_FRAC,
FrameParams, Placement, ShadowCaster, CURSOR_BASE_SIZE_FRAC, FPS, SCREEN_SHADOW_OFFSET_FRAC,
SCREEN_SHADOW_SPREAD_FRAC, SHADOW_TUNING_REF_PX, WEBCAM_SHADOW_OFFSET_FRAC,
WEBCAM_SHADOW_OPACITY, WEBCAM_SHADOW_SPREAD_FRAC,
};
Expand Down Expand Up @@ -1776,23 +1776,29 @@ impl Compositor {
// penché ne se lisait pas comme son ombre mais comme une seconde surface. Elle suit
// aussi la croissance de la boîte pendant un zoom (issue #179) : quand la boîte sort
// du cadre, l'ombre en sort avec elle, sans jamais se lire comme une bande noire.
// Avec un cadre de fenêtre, c'est le CADRE qui porte l'ombre (`shadow_caster`), sinon
// elle tomberait sous l'écran seul et la barre de titre flotterait au-dessus.
let render_px = [self.rw(), self.rh()];
if cfg.shadow {
let spread = SCREEN_SHADOW_SPREAD_FRAC * frame_min_px;
let offset = [0.0, SCREEN_SHADOW_OFFSET_FRAC * frame_min_px];
let opacity = 0.45 * lp.shadow_scale;
match tilt.as_ref() {
None => self.draw_shadow(s_dst, s_px, s_radius, spread, offset, opacity),
Some(quad) => self.draw_quad_shadow(
&quad.corners,
quad_center_px,
// Même rayon que le plan incliné lui-même (cf. le dessin du mode 8).
s_radius * quad.scale,
spread,
offset,
opacity,
),
match g.shadow_caster(render_px) {
ShadowCaster::Upright { dst, size_px, radius } => {
self.draw_shadow(dst, size_px, radius, spread, offset, opacity)
}
// Même rayon que le plan incliné lui-même (cf. le dessin du mode 8).
ShadowCaster::Tilted { corners, center_px, radius } => {
self.draw_quad_shadow(&corners, center_px, radius, spread, offset, opacity)
}
}
}
// Le cadre (mode 14) passe SOUS l'écran : l'écran le recouvre, ne laissant voir que la
// barre de titre, le filet et les coins bas entre les deux arrondis.
if let Some(cb) = g.window_frame_cb(render_px) {
self.draw_solid(&cb);
}
let square_top = g.screen_square_top();
if crate::regions::is_identity_rotation(zoom_rotation) {
self.draw_video(
&LayerCB {
Expand All @@ -1804,7 +1810,7 @@ impl Compositor {
color: [0.0, 0.0, 0.0, 1.0],
src_prev: [su0_p, sv0_p, su0_p + 2.0 * hu_p, sv0_p + 2.0 * hv_p],
dst_prev: s_dst_prev,
mb: [mb_taps, mb_amount, 1.0, 0.0],
mb: [mb_taps, mb_amount, 1.0, square_top],
..Default::default()
},
&sy,
Expand Down Expand Up @@ -1857,7 +1863,7 @@ impl Compositor {
mode: 8.0,
fx: [tl0, tl1, tr0, tr1],
src_prev: [br0, br1, bl0, bl1],
dst_prev: [plane_px[0], plane_px[1], 0.0, 0.0],
dst_prev: [plane_px[0], plane_px[1], square_top, 0.0],
..Default::default()
},
&sy,
Expand Down
Loading
Loading