diff --git a/crates/compositor/tests/animated_background.rs b/crates/compositor/tests/animated_background.rs index 748e605f3..6ae4c214d 100644 --- a/crates/compositor/tests/animated_background.rs +++ b/crates/compositor/tests/animated_background.rs @@ -28,21 +28,15 @@ use openscreen_compositor::frame_geometry::live_params_from_scene; use openscreen_compositor::live::Player; use openscreen_compositor::scene::Scene; +mod common; +use common::write_ppm; + const W: u32 = 960; const H: u32 = 540; /// Trois clips de 6 s du même fichier : le clip `k` lu à 3 s source tombe à `6k + 3` s de /// programme. La frame vidéo est la même, seul le temps programme change. const PROGRAMME_TIMES: [f32; 3] = [3.0, 9.0, 15.0]; -fn write_ppm(path: &std::path::Path, rgba: &[u8], w: u32, h: u32) -> std::io::Result<()> { - let mut out = Vec::with_capacity(rgba.len() / 4 * 3 + 32); - out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes()); - for px in rgba.chunks_exact(4) { - out.extend_from_slice(&px[..3]); - } - std::fs::write(path, out) -} - /// Le fond, ouvert : `scene_json` y ajoute le mouvement puis referme l'accolade. const GRADIENT: &str = r##"{"kind":"gradient","angleDeg":135,"stops":["#2b3a67","#b8577f"]"##; diff --git a/crates/compositor/tests/common/mod.rs b/crates/compositor/tests/common/mod.rs new file mode 100644 index 000000000..48c8e3ac3 --- /dev/null +++ b/crates/compositor/tests/common/mod.rs @@ -0,0 +1,17 @@ +#![allow(dead_code)] + +#[cfg(windows)] +mod nv12; +#[cfg(windows)] +pub use nv12::{gpu, Nv12Frame}; + +/// PPM P6 — pas de dépendance à encoder, et ça s'ouvre dans n'importe quel +/// visionneur. Permet l'inspection à l'œil en plus de la comparaison de hash. +pub fn write_ppm(path: &std::path::Path, rgba: &[u8], w: u32, h: u32) -> std::io::Result<()> { + let mut out = Vec::with_capacity(rgba.len() / 4 * 3 + 32); + out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes()); + for px in rgba.chunks_exact(4) { + out.extend_from_slice(&px[..3]); + } + std::fs::write(path, out) +} diff --git a/crates/compositor/tests/common/nv12.rs b/crates/compositor/tests/common/nv12.rs new file mode 100644 index 000000000..66353350c --- /dev/null +++ b/crates/compositor/tests/common/nv12.rs @@ -0,0 +1,78 @@ +use openscreen_compositor::d3d::Gpu; +use openscreen_compositor::ffi::AVFrame; +use windows::core::Interface; +use windows::Win32::Graphics::Direct3D11::{ + ID3D11Texture2D, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE, + D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_WRITE_DISCARD, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DYNAMIC, +}; +use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC}; + +pub fn gpu() -> Option { + match Gpu::create(false) { + Ok(g) => Some(g), + Err(e) => { + eprintln!("pas de device D3D11 matériel ({e:#}) — test saute"); + None + } + } +} + +pub struct Nv12Frame { + frame: Box, + _tex: ID3D11Texture2D, +} + +impl Nv12Frame { + pub fn new( + gpu: &Gpu, + (w, h): (u32, u32), + luma: impl Fn(u32, u32) -> u8, + chroma: impl Fn(u32, u32) -> [u8; 2], + ) -> Nv12Frame { + let desc = D3D11_TEXTURE2D_DESC { + Width: w, + Height: h, + MipLevels: 1, + ArraySize: 1, + Format: DXGI_FORMAT_NV12, + SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, + Usage: D3D11_USAGE_DYNAMIC, + BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, + CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, + MiscFlags: 0, + }; + unsafe { + let mut tex: Option = None; + gpu.device.CreateTexture2D(&desc, None, Some(&mut tex)).expect("texture NV12"); + let tex = tex.expect("texture NV12"); + let mut m = D3D11_MAPPED_SUBRESOURCE::default(); + gpu.context.Map(&tex, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut m)).expect("Map"); + let pitch = m.RowPitch as usize; + let dst = m.pData as *mut u8; + for row in 0..h { + for col in 0..w { + *dst.add(row as usize * pitch + col as usize) = luma(col, row); + } + } + for by in 0..h / 2 { + for bx in 0..w / 2 { + let [cb, cr] = chroma(bx, by); + let uv = (h + by) as usize * pitch + 2 * bx as usize; + *dst.add(uv) = cb; + *dst.add(uv + 1) = cr; + } + } + gpu.context.Unmap(&tex, 0); + let mut frame: Box = Box::new(std::mem::zeroed()); + frame.data[0] = tex.as_raw() as *mut u8; + frame.data[1] = std::ptr::null_mut(); + frame.width = w as i32; + frame.height = h as i32; + Nv12Frame { frame, _tex: tex } + } + } + + pub fn as_ptr(&self) -> *const AVFrame { + &*self.frame as *const AVFrame + } +} diff --git a/crates/compositor/tests/cursor_model_render.rs b/crates/compositor/tests/cursor_model_render.rs index 70151158f..423686253 100644 --- a/crates/compositor/tests/cursor_model_render.rs +++ b/crates/compositor/tests/cursor_model_render.rs @@ -19,18 +19,14 @@ use openscreen_compositor::compositor::Compositor; use openscreen_compositor::config::Cfg; use openscreen_compositor::cursor::CursorTrack; use openscreen_compositor::d3d::Gpu; -use openscreen_compositor::ffi::AVFrame; use openscreen_compositor::frame_geometry::{ live_params_from_scene, plan_cursor, plan_frame, CursorPlacement, CursorPlanInput, FrameGeometryInput, }; use openscreen_compositor::scene::Scene; -use windows::core::Interface; -use windows::Win32::Graphics::Direct3D11::{ - ID3D11Texture2D, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE, - D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_WRITE_DISCARD, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DYNAMIC, -}; -use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC}; + +mod common; +use common::{gpu, Nv12Frame}; const SRC: (u32, u32) = (640, 360); /// Instant rendu : en plein palier de la région de zoom (0..10 s). @@ -67,16 +63,6 @@ fn is_centred(key: &str) -> bool { matches!(key, "text" | "resize-ew" | "not-allowed") } -fn gpu() -> Option { - match Gpu::create(false) { - Ok(g) => Some(g), - Err(e) => { - eprintln!("pas de device D3D11 matériel ({e:#}) — test saute"); - None - } - } -} - /// Deux teintes de contenu : un pixel identique sur les deux est du modèle opaque, un pixel qui /// change est le contenu, son ombre ou une frange antialiasée. #[derive(Clone, Copy)] @@ -89,63 +75,20 @@ enum Tint { /// D3D11VA (le seul contrat que `Compositor::nv12_srvs` lit). Une teinte moyenne striée de barres /// plus sombres : rien d'aussi clair ni d'aussi sombre que le curseur, et surtout rien de NEUTRE, /// si bien qu'un pixel gris est le curseur et un pixel teinté assombri, son ombre. -struct FakeFrame { - frame: Box, - _tex: ID3D11Texture2D, -} - -impl FakeFrame { - fn new(gpu: &Gpu, tint: Tint) -> FakeFrame { - let (w, h) = SRC; - let (u, v) = match tint { - Tint::Blue => (150, 120), - Tint::Orange => (100, 170), - }; - let desc = D3D11_TEXTURE2D_DESC { - Width: w, - Height: h, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_NV12, - SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - }; - unsafe { - let mut tex: Option = None; - gpu.device.CreateTexture2D(&desc, None, Some(&mut tex)).expect("texture NV12"); - let tex = tex.expect("texture NV12"); - let mut m = D3D11_MAPPED_SUBRESOURCE::default(); - gpu.context.Map(&tex, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut m)).expect("Map"); - let pitch = m.RowPitch as usize; - let dst = m.pData as *mut u8; - for row in 0..h as usize { - for col in 0..w as usize { - let bar = row % 24 >= 8 && row % 24 < 12 && (col / 40) % 3 != 2; - *dst.add(row * pitch + col) = if bar { 90 } else { 150 }; - } - } - for row in 0..(h / 2) as usize { - for col in 0..w as usize { - let uv = (h as usize + row) * pitch + col; - *dst.add(uv) = if col % 2 == 0 { u } else { v }; - } - } - gpu.context.Unmap(&tex, 0); - let mut frame: Box = Box::new(std::mem::zeroed()); - frame.data[0] = tex.as_raw() as *mut u8; - frame.data[1] = std::ptr::null_mut(); - frame.width = w as i32; - frame.height = h as i32; - FakeFrame { frame, _tex: tex } - } - } - - fn as_ptr(&self) -> *const AVFrame { - &*self.frame as *const AVFrame - } +fn fake_frame(gpu: &Gpu, tint: Tint) -> Nv12Frame { + let [u, v] = match tint { + Tint::Blue => [150, 120], + Tint::Orange => [100, 170], + }; + Nv12Frame::new( + gpu, + SRC, + |col, row| { + let bar = row % 24 >= 8 && row % 24 < 12 && (col / 40) % 3 != 2; + if bar { 90 } else { 150 } + }, + |_, _| [u, v], + ) } /// Les seize sprites livrés, au format `cursorSprites` de la scène. @@ -287,7 +230,7 @@ struct Probe { unit: f32, } -fn render(comp: &Compositor, screen: &FakeFrame, json: &str, track: &CursorTrack) -> (Vec, Probe) { +fn render(comp: &Compositor, screen: &Nv12Frame, json: &str, track: &CursorTrack) -> (Vec, Probe) { let (rgba, probe) = render_any(comp, screen, json, track); (rgba, probe.expect("un curseur à dessiner")) } @@ -295,7 +238,7 @@ fn render(comp: &Compositor, screen: &FakeFrame, json: &str, track: &CursorTrack /// `render`, sans exiger de curseur (scène au curseur masqué). fn render_any( comp: &Compositor, - screen: &FakeFrame, + screen: &Nv12Frame, json: &str, track: &CursorTrack, ) -> (Vec, Option) { @@ -517,7 +460,7 @@ fn save(name: &str, rgba: &[u8]) { fn the_modelled_arrow_stands_on_the_screen_and_casts_its_shadow() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let screen = FakeFrame::new(&gpu, Tint::Blue); + let screen = fake_frame(&gpu, Tint::Blue); let still = resting("still", false); let clicked = resting("clicked", true); @@ -580,7 +523,7 @@ fn the_modelled_arrow_stands_on_the_screen_and_casts_its_shadow() { fn every_state_keeps_its_art_and_its_footprint() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let (blue, orange) = (FakeFrame::new(&gpu, Tint::Blue), FakeFrame::new(&gpu, Tint::Orange)); + let (blue, orange) = (fake_frame(&gpu, Tint::Blue), fake_frame(&gpu, Tint::Orange)); const SIZE: f32 = 5.0; let bare = render_any(&comp, &blue, &hidden_json("null"), &resting("bare", false)).0; let mut failures = Vec::new(); @@ -647,7 +590,7 @@ fn every_state_keeps_its_art_and_its_footprint() { fn the_tilt_turns_every_state_with_the_screen() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let screen = FakeFrame::new(&gpu, Tint::Blue); + let screen = fake_frame(&gpu, Tint::Blue); for key in TESTED { let mask = |rotation: &str, x: f32, y: f32| -> (Vec, Probe) { let still = track_as(&format!("tilt-{key}"), Some(key), &[(0.0, x, y, false), (9.0, x, y, false)]); @@ -686,7 +629,7 @@ fn the_tilt_turns_every_state_with_the_screen() { fn without_the_model_the_cursor_renders_the_flat_sprite() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let screen = FakeFrame::new(&gpu, Tint::Blue); + let screen = fake_frame(&gpu, Tint::Blue); let still = resting("flat-sprite", false); let clicked = resting("flat-sprite-clicked", true); let text = resting_as("flat-sprite-text", Some("text"), false); @@ -727,7 +670,7 @@ fn without_the_model_the_cursor_renders_the_flat_sprite() { fn a_moving_pointer_turns_towards_its_motion_and_a_centred_cursor_does_not() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let screen = FakeFrame::new(&gpu, Tint::Blue); + let screen = fake_frame(&gpu, Tint::Blue); let json = scene_json(r#""iso""#, Some(true), "default", 0.0, 3.0); // En T, la piste mobile passe en x = 0,2 + 0,6 × T / (T + 1) = 0,6 : la piste immobile y est. let moving_as = |key: Option<&str>| { @@ -771,7 +714,7 @@ fn a_moving_pointer_turns_towards_its_motion_and_a_centred_cursor_does_not() { fn the_motion_blur_trail_draws_modelled_copies() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let screen = FakeFrame::new(&gpu, Tint::Blue); + let screen = fake_frame(&gpu, Tint::Blue); let moving = track("trail", &[(0.0, 0.2, 0.45, false), (T + 1.0, 0.8, 0.45, false)]); let (rgba, _) = render(&comp, &screen, &scene_json("null", Some(true), "default", 1.0, 3.0), &moving); let (flat, _) = render(&comp, &screen, &scene_json("null", Some(false), "default", 1.0, 3.0), &moving); @@ -788,7 +731,7 @@ fn the_motion_blur_trail_draws_modelled_copies() { fn the_sculpted_cursors_stand_at_the_hotspot() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let (blue, orange) = (FakeFrame::new(&gpu, Tint::Blue), FakeFrame::new(&gpu, Tint::Orange)); + let (blue, orange) = (fake_frame(&gpu, Tint::Blue), fake_frame(&gpu, Tint::Orange)); let bare = render_any(&comp, &blue, &hidden_json("null"), &resting("sculpt-bare", false)).0; let bare_orange = render_any(&comp, &orange, &hidden_json("null"), &resting("sculpt-bare", false)).0; let mut failures = Vec::new(); @@ -860,7 +803,7 @@ fn contact_sheets() { }; let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let screen = FakeFrame::new(&gpu, Tint::Blue); + let screen = fake_frame(&gpu, Tint::Blue); const CELL: u32 = 300; let crop = |rgba: &[u8], p: Probe| { let img = image::RgbaImage::from_raw(1280, 720, rgba.to_vec()).expect("readback"); @@ -928,7 +871,7 @@ fn bench_the_modelled_cursor_at_1080p() { gpu }; let comp = Compositor::new_sized(&gpu, 1920, 1080).expect("compositor"); - let screen = FakeFrame::new(&gpu, Tint::Blue); + let screen = fake_frame(&gpu, Tint::Blue); let still = resting("bench", false); let text = resting_as("bench-text", Some("text"), false); // Un geste rapide : la traînée prend ses 16 copies. diff --git a/crates/compositor/tests/cursor_tap_render.rs b/crates/compositor/tests/cursor_tap_render.rs index 537dd62a5..6d07519d2 100644 --- a/crates/compositor/tests/cursor_tap_render.rs +++ b/crates/compositor/tests/cursor_tap_render.rs @@ -21,18 +21,14 @@ use openscreen_compositor::compositor::Compositor; use openscreen_compositor::config::Cfg; use openscreen_compositor::cursor::CursorTrack; use openscreen_compositor::d3d::Gpu; -use openscreen_compositor::ffi::AVFrame; use openscreen_compositor::frame_geometry::{ live_params_from_scene, plan_cursor, plan_frame, CursorPlacement, CursorPlanInput, FrameGeometryInput, }; use openscreen_compositor::scene::Scene; -use windows::core::Interface; -use windows::Win32::Graphics::Direct3D11::{ - ID3D11Texture2D, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE, - D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_WRITE_DISCARD, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DYNAMIC, -}; -use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC}; + +mod common; +use common::{gpu, Nv12Frame}; const SRC: (u32, u32) = (1280, 720); const OUT: (u32, u32) = (1280, 720); @@ -44,111 +40,58 @@ const TARGETS: [([f32; 2], f32); 2] = /// Au creux du contact (`regions::tap`). const CONTACT_S: f32 = 0.0495; -fn gpu() -> Option { - match Gpu::create(false) { - Ok(g) => Some(g), - Err(e) => { - eprintln!("pas de device D3D11 matériel ({e:#}) — test saute"); - None - } - } -} - /// Une maquette d'interface en NV12 (barre de titre, carte, lignes de texte, deux boutons), avec /// une pastille rouge de 5 texels de rayon au centre exact de chaque cible. Présentée comme une /// frame D3D11VA (cf. `cursor_model_render.rs`). La luma est évaluée au centre de chaque texel, la /// chroma au centre de chaque bloc 2×2. -struct MockFrame { - frame: Box, - _tex: ID3D11Texture2D, -} - -impl MockFrame { - fn new(gpu: &Gpu) -> MockFrame { - let (w, h) = SRC; - // (Y, Cb, Cr) de chaque pixel, en BT.709 limité. - let pixel = |fx: f32, fy: f32| -> (u8, u8, u8) { - let (x, y) = (fx as u32, fy as u32); - for ([tx, ty], _) in TARGETS { - let (cx, cy) = (tx * w as f32, ty * h as f32); - if (fx - cx).hypot(fy - cy) <= 5.0 { - return (63, 102, 240); // rouge - } - } - let button = |c: [f32; 2], hw: f32, hh: f32| { - let (dx, dy) = ((fx - c[0] * w as f32).abs(), (fy - c[1] * h as f32).abs()); - (dx - hw + 10.0).max(0.0).hypot((dy - hh + 10.0).max(0.0)) <= 10.0 - }; - if button(TARGETS[0].0, 130.0, 70.0) { - return (92, 180, 110); // bouton bleu - } - if button(TARGETS[1].0, 90.0, 26.0) { - return (150, 100, 90); // bouton vert - } - if y < 56 { - return (48, 132, 126); // barre de titre sombre - } - if x < 220 { - let item = (y - 56) % 44; - return if (14..22).contains(&item) && (24..150).contains(&x) { (150, 128, 128) } else { (222, 129, 127) }; - } - let card = x > 250 && x < 1240 && y > 90 && y < 690; - if card && (x == 251 || x == 1239 || y == 91 || y == 689) { - return (200, 128, 128); - } - // Lignes de « texte » dans la carte, à l'écart des boutons. - let line = (y % 36) >= 14 && (y % 36) < 20 && x > 280 && x < 1200 && (x / 97) % 5 != 4; - if card && line { - return (135, 128, 128); +fn mock_frame(gpu: &Gpu) -> Nv12Frame { + let (w, h) = SRC; + // (Y, Cb, Cr) de chaque pixel, en BT.709 limité. + let pixel = |fx: f32, fy: f32| -> (u8, u8, u8) { + let (x, y) = (fx as u32, fy as u32); + for ([tx, ty], _) in TARGETS { + let (cx, cy) = (tx * w as f32, ty * h as f32); + if (fx - cx).hypot(fy - cy) <= 5.0 { + return (63, 102, 240); // rouge } - (if card { 245 } else { 232 }, 128, 128) - }; - let desc = D3D11_TEXTURE2D_DESC { - Width: w, - Height: h, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_NV12, - SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, + } + let button = |c: [f32; 2], hw: f32, hh: f32| { + let (dx, dy) = ((fx - c[0] * w as f32).abs(), (fy - c[1] * h as f32).abs()); + (dx - hw + 10.0).max(0.0).hypot((dy - hh + 10.0).max(0.0)) <= 10.0 }; - unsafe { - let mut tex: Option = None; - gpu.device.CreateTexture2D(&desc, None, Some(&mut tex)).expect("texture NV12"); - let tex = tex.expect("texture NV12"); - let mut m = D3D11_MAPPED_SUBRESOURCE::default(); - gpu.context.Map(&tex, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut m)).expect("Map"); - let pitch = m.RowPitch as usize; - let dst = m.pData as *mut u8; - for y in 0..h { - for x in 0..w { - *dst.add(y as usize * pitch + x as usize) = pixel(x as f32 + 0.5, y as f32 + 0.5).0; - } - } - for y in 0..h / 2 { - for x in 0..w / 2 { - let (_, cb, cr) = pixel(2.0 * x as f32 + 1.0, 2.0 * y as f32 + 1.0); - let uv = (h as usize + y as usize) * pitch + 2 * x as usize; - *dst.add(uv) = cb; - *dst.add(uv + 1) = cr; - } - } - gpu.context.Unmap(&tex, 0); - let mut frame: Box = Box::new(std::mem::zeroed()); - frame.data[0] = tex.as_raw() as *mut u8; - frame.data[1] = std::ptr::null_mut(); - frame.width = w as i32; - frame.height = h as i32; - MockFrame { frame, _tex: tex } + if button(TARGETS[0].0, 130.0, 70.0) { + return (92, 180, 110); // bouton bleu } - } - - fn as_ptr(&self) -> *const AVFrame { - &*self.frame as *const AVFrame - } + if button(TARGETS[1].0, 90.0, 26.0) { + return (150, 100, 90); // bouton vert + } + if y < 56 { + return (48, 132, 126); // barre de titre sombre + } + if x < 220 { + let item = (y - 56) % 44; + return if (14..22).contains(&item) && (24..150).contains(&x) { (150, 128, 128) } else { (222, 129, 127) }; + } + let card = x > 250 && x < 1240 && y > 90 && y < 690; + if card && (x == 251 || x == 1239 || y == 91 || y == 689) { + return (200, 128, 128); + } + // Lignes de « texte » dans la carte, à l'écart des boutons. + let line = (y % 36) >= 14 && (y % 36) < 20 && x > 280 && x < 1200 && (x / 97) % 5 != 4; + if card && line { + return (135, 128, 128); + } + (if card { 245 } else { 232 }, 128, 128) + }; + Nv12Frame::new( + gpu, + SRC, + |x, y| pixel(x as f32 + 0.5, y as f32 + 0.5).0, + |bx, by| { + let (_, cb, cr) = pixel(2.0 * bx as f32 + 1.0, 2.0 * by as f32 + 1.0); + [cb, cr] + }, + ) } /// `rotation` : valeur JSON du préset ; `hide` : le curseur masqué par la région (la caméra le @@ -232,7 +175,7 @@ fn cfg() -> Cfg { } /// La frame à `t`, et le pixel du contenu où le plan pose le curseur (la pointe), s'il y en a un. -fn render(comp: &Compositor, screen: &MockFrame, json: &str, track: &CursorTrack, t: f32) -> (Vec, Option<[f32; 2]>) { +fn render(comp: &Compositor, screen: &Nv12Frame, json: &str, track: &CursorTrack, t: f32) -> (Vec, Option<[f32; 2]>) { let scene = Scene::from_json(json).expect("scène valide"); let mut live = live_params_from_scene(&scene); live.has_webcam = false; @@ -308,7 +251,7 @@ fn marker(rgba: &[u8], near: [f32; 2]) -> [f32; 2] { fn the_tip_lands_on_the_marked_click_target() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, OUT.0, OUT.1).expect("compositor"); - let screen = MockFrame::new(&gpu); + let screen = mock_frame(&gpu); let track = gesture("contact"); for (name, rotation) in [("flat", "null"), ("iso", r#""iso""#), ("orbit", r#""orbit","focusMode":"auto""#)] { for (target, tc) in TARGETS { @@ -342,7 +285,7 @@ fn the_tip_lands_on_the_marked_click_target() { fn the_impact_ring_is_centred_on_the_click() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, OUT.0, OUT.1).expect("compositor"); - let screen = MockFrame::new(&gpu); + let screen = mock_frame(&gpu); let track = gesture("ring"); let (_, tc) = TARGETS[0]; let t = tc + 0.16; @@ -383,7 +326,7 @@ fn cursor_press_levels() { }; let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, OUT.0, OUT.1).expect("compositor"); - let screen = MockFrame::new(&gpu); + let screen = mock_frame(&gpu); let track = gesture("levels"); let frames = format!("{dir}/levels"); std::fs::create_dir_all(&frames).expect("dossier des frames"); @@ -447,7 +390,7 @@ fn cursor_tap_video() { }; let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, OUT.0, OUT.1).expect("compositor"); - let screen = MockFrame::new(&gpu); + let screen = mock_frame(&gpu); let track = gesture("video"); let frames = format!("{dir}/frames"); std::fs::create_dir_all(&frames).expect("dossier des frames"); diff --git a/crates/compositor/tests/device_frame_render.rs b/crates/compositor/tests/device_frame_render.rs index 23ababe5a..8780f1e91 100644 --- a/crates/compositor/tests/device_frame_render.rs +++ b/crates/compositor/tests/device_frame_render.rs @@ -19,16 +19,12 @@ use openscreen_compositor::compositor::Compositor; use openscreen_compositor::config::Cfg; use openscreen_compositor::cursor::CursorTrack; use openscreen_compositor::d3d::Gpu; -use openscreen_compositor::ffi::AVFrame; use openscreen_compositor::frame_geometry::{live_params_from_scene, plan_frame, FrameGeometry, FrameGeometryInput}; use openscreen_compositor::regions::TiltedQuad; use openscreen_compositor::scene::Scene; -use windows::core::Interface; -use windows::Win32::Graphics::Direct3D11::{ - ID3D11Texture2D, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE, D3D11_MAPPED_SUBRESOURCE, - D3D11_MAP_WRITE_DISCARD, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DYNAMIC, -}; -use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC}; + +mod common; +use common::{gpu, Nv12Frame}; const W: u32 = 1280; const H: u32 = 720; @@ -51,16 +47,6 @@ const SHAPES: [(&str, (u32, u32), (u32, u32)); 4] = [ ("21:9", (1260, 540), (630, 270)), ]; -fn gpu() -> Option { - match Gpu::create(false) { - Ok(g) => Some(g), - Err(e) => { - eprintln!("pas de device D3D11 matériel ({e:#}) — test saute"); - None - } - } -} - /// Deux teintes de contenu : un pixel identique sur les deux est de l'appareil ou du fond, un /// pixel qui change est le métrage. #[derive(Clone, Copy)] @@ -70,62 +56,20 @@ enum Tint { } /// Une frame NV12 synthétique, striée pour que le métrage se distingue d'un aplat. -struct FakeFrame { - frame: Box, - _tex: ID3D11Texture2D, -} - -impl FakeFrame { - fn new(gpu: &Gpu, (w, h): (u32, u32), tint: Tint) -> FakeFrame { - let (u, v) = match tint { - Tint::Blue => (150, 120), - Tint::Orange => (100, 170), - }; - let desc = D3D11_TEXTURE2D_DESC { - Width: w, - Height: h, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_NV12, - SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - }; - unsafe { - let mut tex: Option = None; - gpu.device.CreateTexture2D(&desc, None, Some(&mut tex)).expect("texture NV12"); - let tex = tex.expect("texture NV12"); - let mut m = D3D11_MAPPED_SUBRESOURCE::default(); - gpu.context.Map(&tex, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut m)).expect("Map"); - let pitch = m.RowPitch as usize; - let dst = m.pData as *mut u8; - for row in 0..h as usize { - for col in 0..w as usize { - let bar = row % 24 >= 8 && row % 24 < 12 && (col / 40) % 3 != 2; - *dst.add(row * pitch + col) = if bar { 90 } else { 170 }; - } - } - for row in 0..(h / 2) as usize { - for col in 0..w as usize { - let uv = (h as usize + row) * pitch + col; - *dst.add(uv) = if col % 2 == 0 { u } else { v }; - } - } - gpu.context.Unmap(&tex, 0); - let mut frame: Box = Box::new(std::mem::zeroed()); - frame.data[0] = tex.as_raw() as *mut u8; - frame.data[1] = std::ptr::null_mut(); - frame.width = w as i32; - frame.height = h as i32; - FakeFrame { frame, _tex: tex } - } - } - - fn as_ptr(&self) -> *const AVFrame { - &*self.frame as *const AVFrame - } +fn fake_frame(gpu: &Gpu, size: (u32, u32), tint: Tint) -> Nv12Frame { + let [u, v] = match tint { + Tint::Blue => [150, 120], + Tint::Orange => [100, 170], + }; + Nv12Frame::new( + gpu, + size, + |col, row| { + let bar = row % 24 >= 8 && row % 24 < 12 && (col / 40) % 3 != 2; + if bar { 90 } else { 170 } + }, + |_, _| [u, v], + ) } /// Une scène d'un seul écran : `frame` = la valeur du réglage (`""` = clé absente). @@ -177,7 +121,7 @@ fn cfg() -> Cfg { cfg } -fn render(comp: &Compositor, screen: &FakeFrame, json: &str, track: Option<&CursorTrack>, t: f32) -> Vec { +fn render(comp: &Compositor, screen: &Nv12Frame, json: &str, track: Option<&CursorTrack>, t: f32) -> Vec { let scene = Scene::from_json(json).expect("scène valide"); let mut live = live_params_from_scene(&scene); live.has_webcam = false; @@ -461,8 +405,8 @@ fn each_device_draws_around_untouched_footage() { let Some(gpu) = gpu() else { return }; for (shape, out, src) in SHAPES { let comp = Compositor::new_sized(&gpu, out.0, out.1).expect("compositor"); - let blue = FakeFrame::new(&gpu, src, Tint::Blue); - let orange = FakeFrame::new(&gpu, src, Tint::Orange); + let blue = fake_frame(&gpu, src, Tint::Blue); + let orange = fake_frame(&gpu, src, Tint::Orange); for &device in &DEVICES { for rotation in ["null", r#""iso""#, ORBIT] { let json = scene_json(device, rotation, 0.0, NO_CURSOR, out); @@ -536,7 +480,7 @@ fn each_device_draws_around_untouched_footage() { fn the_frame_theme_repaints_the_body_and_nothing_else() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); for &device in &["window", "laptop", "phone", "monitor"] { for rotation in ["null", r#""iso""#] { let light = @@ -569,7 +513,7 @@ fn the_frame_theme_repaints_the_body_and_nothing_else() { fn the_device_follows_the_tilt_and_the_orbit_camera() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); for &device in &["laptop", "window"] { let flat = render(&comp, &screen, &scene_json(device, "null", 0.0, NO_CURSOR, (W, H)), None, T); assert!( @@ -595,7 +539,7 @@ fn the_device_follows_the_tilt_and_the_orbit_camera() { fn the_device_trails_with_the_screen_under_motion_blur() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let blue = FakeFrame::new(&gpu, SRC, Tint::Blue); + let blue = fake_frame(&gpu, SRC, Tint::Blue); // Zoom ×1,6 de 2 à 6 s : sa rampe d'entrée court de 1,14 à 2 s, vitesse au plus fort vers 1,26 s. // Le petit rect garde l'appareil dans l'image pendant la rampe. let json = |device: &str, rotation: &str, blur: f32| { @@ -653,7 +597,7 @@ fn the_device_trails_with_the_screen_under_motion_blur() { fn the_frame_casts_the_contact_shadow() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); for &device in &DEVICES { let on = render(&comp, &screen, &scene_json(device, "null", 0.7, NO_CURSOR, (W, H)), None, T); let off = render(&comp, &screen, &scene_json(device, "null", 0.0, NO_CURSOR, (W, H)), None, T); @@ -689,7 +633,7 @@ fn no_seam_lets_the_wallpaper_through() { let Some(gpu) = gpu() else { return }; for (res, out) in [("1080p", (1920u32, 1080u32)), ("4K", (3840, 2160))] { let comp = Compositor::new_sized(&gpu, out.0, out.1).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); for frame in ["window", "laptop", "phone", "monitor"] { for roundness in [0.0f32, MAX_ROUND] { for rotation in ["null", r#""iso""#, ORBIT] { @@ -730,8 +674,8 @@ fn the_bezel_fills_the_corners_at_maximum_roundness() { let Some(gpu) = gpu() else { return }; let out = (1920u32, 1080u32); let comp = Compositor::new_sized(&gpu, out.0, out.1).expect("compositor"); - let blue = FakeFrame::new(&gpu, SRC, Tint::Blue); - let orange = FakeFrame::new(&gpu, SRC, Tint::Orange); + let blue = fake_frame(&gpu, SRC, Tint::Blue); + let orange = fake_frame(&gpu, SRC, Tint::Orange); for frame in ["window", "laptop", "phone", "monitor"] { for rotation in ["null", r#""iso""#, ORBIT] { let json = on_green(&with_roundness(&scene_json(frame, rotation, 0.0, NO_CURSOR, out), MAX_ROUND)); @@ -841,8 +785,8 @@ fn distance_to(mask: &[bool], w: usize, h: usize) -> Vec { fn the_device_shadow_follows_the_silhouette() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let blue = FakeFrame::new(&gpu, SRC, Tint::Blue); - let orange = FakeFrame::new(&gpu, SRC, Tint::Orange); + let blue = fake_frame(&gpu, SRC, Tint::Blue); + let orange = fake_frame(&gpu, SRC, Tint::Orange); let rect = CLIPS[0].1; // SCREEN_SHADOW_SPREAD_FRAC (40 px réglés contre un cadre 1080), en px de CETTE sortie. let spread = 40.0 / 1080.0 * W.min(H) as f32; @@ -904,7 +848,7 @@ fn contact_sheets() { }; let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); let all: [&str; 4] = ["window", "laptop", "phone", "monitor"]; // Une planche par theme : quatre cadres x (plat, iso). for theme in ["light", "dark"] { @@ -924,7 +868,7 @@ fn contact_sheets() { // Le telephone dans les deux orientations de clip : debout et couche. for (name, out, src) in [("portrait", (720u32, 1280u32), (360u32, 640u32)), ("landscape", (W, H), SRC)] { let comp = Compositor::new_sized(&gpu, out.0, out.1).expect("compositor"); - let f = FakeFrame::new(&gpu, src, Tint::Blue); + let f = fake_frame(&gpu, src, Tint::Blue); for (label, rotation) in [("flat", "null"), ("iso", r#""iso""#)] { let rgba = render(&comp, &f, &scene_json("phone", rotation, 0.6, NO_CURSOR, out), None, T); save(&dir, &format!("phone-{name}-{label}"), &rgba, out); @@ -933,7 +877,7 @@ fn contact_sheets() { // Ratios inhabituels : rien ne degenere. for (shape, out, src) in SHAPES { let comp = Compositor::new_sized(&gpu, out.0, out.1).expect("compositor"); - let f = FakeFrame::new(&gpu, src, Tint::Blue); + let f = fake_frame(&gpu, src, Tint::Blue); for &device in &["laptop", "monitor"] { let rgba = render(&comp, &f, &scene_json(device, "null", 0.6, NO_CURSOR, out), None, T); save(&dir, &format!("shape-{}-{device}", shape.replace(':', "x")), &rgba, out); @@ -1008,7 +952,7 @@ fn v3_renders() { // 1. Paysage et portrait, plat et iso, clair et sombre : une image par cas, une planche par // clip et par thème. for (clip, rect, src) in CLIPS { - let f = FakeFrame::new(&gpu, src, Tint::Blue); + let f = fake_frame(&gpu, src, Tint::Blue); for theme in ["light", "dark"] { let mut sheet = image::RgbaImage::new(W * 2, H * 4); for (row, &frame) in all.iter().enumerate() { @@ -1025,7 +969,7 @@ fn v3_renders() { // 2. Gros plans de coins : les quatre coins de l'écran du portable et de la fenêtre, pris au // sommet de l'arc du métrage. - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); let arc_points = |g: &Geo| { let [w, h] = g.s_px; let k = g.radius * (1.0 - std::f32::consts::FRAC_1_SQRT_2); @@ -1097,7 +1041,7 @@ fn orbit_clip_frames() { }; let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); // Le pointeur fait le tour de l'écran : l'œil de la caméra le suit sur son orbite. let mut samples = Vec::new(); for k in 0..=120 { @@ -1277,8 +1221,8 @@ fn the_border_keeps_its_thickness_around_every_corner() { let Some(gpu) = gpu() else { return }; let out = (1920u32, 1080u32); let comp = Compositor::new_sized(&gpu, out.0, out.1).expect("compositor"); - let blue = FakeFrame::new(&gpu, SRC, Tint::Blue); - let orange = FakeFrame::new(&gpu, SRC, Tint::Orange); + let blue = fake_frame(&gpu, SRC, Tint::Blue); + let orange = fake_frame(&gpu, SRC, Tint::Orange); let (mut worst, mut failures) = (0.0f32, Vec::new()); for frame in ["window", "phone"] { for (rlabel, roundness) in [("r0", 0.0f32), ("rdef", DEFAULT_ROUND), ("rmax", MAX_ROUND)] { @@ -1343,7 +1287,7 @@ fn only_the_inside_of_the_laptop_and_screen_bezel_follows_roundness() { let Some(gpu) = gpu() else { return }; let out = (1920u32, 1080u32); let comp = Compositor::new_sized(&gpu, out.0, out.1).expect("compositor"); - let blue = FakeFrame::new(&gpu, SRC, Tint::Blue); + let blue = fake_frame(&gpu, SRC, Tint::Blue); for frame in ["laptop", "monitor"] { let at = |r: f32| { let json = on_green(&with_roundness(&scene_json(frame, "null", 0.0, NO_CURSOR, out), r)); @@ -1386,8 +1330,8 @@ fn a_frame_looks_the_same_on_every_clip_ratio() { // La source a le ratio du clip : rien n'est recadré, rien n'est étiré. let src = if ar >= 1.0 { (w0, (w0 as f32 / ar).round() as u32) } else { ((h0 as f32 * ar).round() as u32, h0) }; let src = (src.0 & !1, src.1 & !1); - let blue = FakeFrame::new(&gpu, src, Tint::Blue); - let orange = FakeFrame::new(&gpu, src, Tint::Orange); + let blue = fake_frame(&gpu, src, Tint::Blue); + let orange = fake_frame(&gpu, src, Tint::Orange); let json = on_green(&with_roundness( &with_rect(&scene_json(frame, "null", 0.0, NO_CURSOR, out), contained_rect(ar)), DEFAULT_ROUND, @@ -1450,8 +1394,8 @@ fn orbit_json(frame: &str, zoom: f32, out: (u32, u32)) -> String { fn the_laptop_deck_never_covers_the_zoom_focus() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let blue = FakeFrame::new(&gpu, SRC, Tint::Blue); - let orange = FakeFrame::new(&gpu, SRC, Tint::Orange); + let blue = fake_frame(&gpu, SRC, Tint::Blue); + let orange = fake_frame(&gpu, SRC, Tint::Orange); let pointers = [ ("bas", (0.5f32, 0.98f32)), ("bas-gauche", (0.02, 0.98)), @@ -1550,7 +1494,7 @@ fn v4_renders() { let all: [&str; 4] = ["window", "laptop", "phone", "monitor"]; let out = (1920u32, 1080u32); let big = Compositor::new_sized(&gpu, out.0, out.1).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); let img = |rgba: Vec, o: (u32, u32)| image::RgbaImage::from_raw(o.0, o.1, rgba).expect("readback"); // 1. Coins, au Roundness maximal : les quatre coins, 40 px de côté agrandis six fois, clair et @@ -1578,7 +1522,7 @@ fn v4_renders() { for (row, &frame) in all.iter().enumerate() { for (col, (label, ar)) in RATIOS.iter().enumerate() { let src = if *ar >= 1.0 { (1280u32, (1280.0 / ar).round() as u32 & !1) } else { ((720.0 * ar).round() as u32 & !1, 720u32) }; - let f = FakeFrame::new(&gpu, src, Tint::Blue); + let f = fake_frame(&gpu, src, Tint::Blue); let json = with_rect(&scene_json_themed(frame, theme, "null", 0.6, NO_CURSOR, out), contained_rect(*ar)); let json = with_roundness(&json, DEFAULT_ROUND); let i = img(render(&big, &f, &json, None, T), out); @@ -1597,7 +1541,7 @@ fn v4_renders() { for (row, pct) in [0.0f32, 0.5, 1.0].into_iter().enumerate() { for (col, (label, ar)) in ratios4.iter().enumerate() { let src = if *ar >= 1.0 { (1280u32, (1280.0 / ar).round() as u32 & !1) } else { ((720.0 * ar).round() as u32 & !1, 720u32) }; - let f = FakeFrame::new(&gpu, src, Tint::Blue); + let f = fake_frame(&gpu, src, Tint::Blue); let json = with_rect(&scene_json(frame, "null", 0.6, NO_CURSOR, out), contained_rect(*ar)); let json = with_roundness(&json, pct * 64.0 / 1080.0); let i = img(render(&big, &f, &json, None, T), out); @@ -1624,7 +1568,7 @@ fn v4_near_clip() { }; let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); let track = parked_track("near", 0.5, 0.97); let json = with_rect(&scene_json("laptop", ORBIT, 0.6, SHOWN_CURSOR, (W, H)), CLIPS[0].1) .replace( @@ -1662,7 +1606,7 @@ fn bench_the_device_frame_at_1080p() { } let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1920, 1080).expect("compositor"); - let screen = FakeFrame::new(&gpu, SRC, Tint::Blue); + let screen = fake_frame(&gpu, SRC, Tint::Blue); let time = |json: &str| { let scene = Scene::from_json(json).expect("scène"); let mut live = live_params_from_scene(&scene); diff --git a/crates/compositor/tests/follow_camera_render.rs b/crates/compositor/tests/follow_camera_render.rs index 1dc245cbb..d4905087b 100644 --- a/crates/compositor/tests/follow_camera_render.rs +++ b/crates/compositor/tests/follow_camera_render.rs @@ -27,15 +27,11 @@ use openscreen_compositor::compositor::Compositor; use openscreen_compositor::config::Cfg; use openscreen_compositor::cursor::CursorTrack; use openscreen_compositor::d3d::Gpu; -use openscreen_compositor::ffi::AVFrame; use openscreen_compositor::frame_geometry::{live_params_from_scene, plan_frame, FrameGeometryInput}; use openscreen_compositor::scene::Scene; -use windows::core::Interface; -use windows::Win32::Graphics::Direct3D11::{ - ID3D11Texture2D, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE, - D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_WRITE_DISCARD, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DYNAMIC, -}; -use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC}; + +mod common; +use common::{gpu, Nv12Frame}; const SRC: (u32, u32) = (640, 360); /// Pas de la grille, en texels : des lignes sombres de 2 texels sur fond clair. @@ -43,70 +39,15 @@ const GRID: u32 = 32; /// En plein palier de la région (0..10 s), le cadreur posé. const T: f32 = 3.0; -fn gpu() -> Option { - match Gpu::create(false) { - Ok(g) => Some(g), - Err(e) => { - eprintln!("pas de device D3D11 matériel ({e:#}) — test saute"); - None - } - } -} - /// Une frame NV12 synthétique présentée comme une frame D3D11VA (cf. `cursor_model_render.rs`) : /// une grille sombre sur fond clair, chroma neutre. -struct GridFrame { - frame: Box, - _tex: ID3D11Texture2D, -} - -impl GridFrame { - fn new(gpu: &Gpu) -> GridFrame { - let (w, h) = SRC; - let desc = D3D11_TEXTURE2D_DESC { - Width: w, - Height: h, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_NV12, - SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - }; - unsafe { - let mut tex: Option = None; - gpu.device.CreateTexture2D(&desc, None, Some(&mut tex)).expect("texture NV12"); - let tex = tex.expect("texture NV12"); - let mut m = D3D11_MAPPED_SUBRESOURCE::default(); - gpu.context.Map(&tex, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut m)).expect("Map"); - let pitch = m.RowPitch as usize; - let dst = m.pData as *mut u8; - for row in 0..h { - for col in 0..w { - let line = col % GRID < 2 || row % GRID < 2; - *dst.add(row as usize * pitch + col as usize) = if line { 30 } else { 220 }; - } - } - for row in 0..(h / 2) as usize { - for col in 0..w as usize { - *dst.add((h as usize + row) * pitch + col) = 128; - } - } - gpu.context.Unmap(&tex, 0); - let mut frame: Box = Box::new(std::mem::zeroed()); - frame.data[0] = tex.as_raw() as *mut u8; - frame.data[1] = std::ptr::null_mut(); - frame.width = w as i32; - frame.height = h as i32; - GridFrame { frame, _tex: tex } - } - } - - fn as_ptr(&self) -> *const AVFrame { - &*self.frame as *const AVFrame - } +fn grid_frame(gpu: &Gpu) -> Nv12Frame { + Nv12Frame::new( + gpu, + SRC, + |col, row| if col % GRID < 2 || row % GRID < 2 { 30 } else { 220 }, + |_, _| [128, 128], + ) } /// Piste curseur écrite dans un sidecar temporaire, échantillonnée à 60 Hz sur `[0, dur]`. @@ -163,7 +104,7 @@ fn cfg() -> Cfg { /// La frame, et le quad que la géométrie partagée prévoit pour elle (centre compris). fn render( comp: &Compositor, - screen: &GridFrame, + screen: &Nv12Frame, scene: &Scene, track: &CursorTrack, ) -> (Vec, openscreen_compositor::regions::TiltedQuad, [f32; 2]) { @@ -236,7 +177,7 @@ fn bilinear(c: &[(f32, f32); 4], u: f32, v: f32) -> (f32, f32) { fn the_projective_warp_lands_every_line_where_the_camera_projects_it() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let screen = GridFrame::new(&gpu); + let screen = grid_frame(&gpu); // Pointeur en haut à droite : caméra tournée à fond, le cas le plus projectif. let tr = track("corner", 10.0, &|_| (0.93, 0.08), &[]); let (rgba, quad, center) = render(&comp, &screen, &grid_scene(2.2), &tr); @@ -309,7 +250,7 @@ fn measure_line(rgba: &[u8], quad: &openscreen_compositor::regions::TiltedQuad, fn the_vertical_through_the_view_axis_stays_vertical() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor"); - let screen = GridFrame::new(&gpu); + let screen = grid_frame(&gpu); for (name, at, scale) in [ ("tl", (0.1, 0.1), 1.8), ("br", (0.9, 0.92), 1.8), diff --git a/crates/compositor/tests/output_geometry_golden.rs b/crates/compositor/tests/output_geometry_golden.rs index 3e363b27a..723cbd8fb 100644 --- a/crates/compositor/tests/output_geometry_golden.rs +++ b/crates/compositor/tests/output_geometry_golden.rs @@ -36,6 +36,9 @@ use openscreen_compositor::live::Player; use openscreen_compositor::scene::Scene; use openscreen_compositor::config; +mod common; +use common::write_ppm; + /// Instant fixe dans la source. Un seek explicite (`present_frame`) plutôt que /// la lecture libre : le golden doit être reproductible à l'octet près. const AT_SEC: f64 = 2.0; @@ -85,17 +88,6 @@ fn gradient_energy(rgba: &[u8], w: usize, h: usize) -> (f64, f64) { (gx / nx.max(1) as f64, gy / ny.max(1) as f64) } -/// PPM P6 — pas de dépendance à encoder, et ça s'ouvre dans n'importe quel -/// visionneur. Permet l'inspection à l'œil en plus de la comparaison de hash. -fn write_ppm(path: &std::path::Path, rgba: &[u8], w: u32, h: u32) -> std::io::Result<()> { - let mut out = Vec::with_capacity(rgba.len() / 4 * 3 + 32); - out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes()); - for px in rgba.chunks_exact(4) { - out.extend_from_slice(&px[..3]); - } - std::fs::write(path, out) -} - fn scene_json(screen: &str, webcam: &str, w: u32, h: u32) -> String { // Chemins en slashes : le JSON n'échappe pas les backslashes Windows. let (s, c) = (screen.replace('\\', "/"), webcam.replace('\\', "/")); diff --git a/crates/compositor/tests/privacy_blur_under_zoom.rs b/crates/compositor/tests/privacy_blur_under_zoom.rs index 75d781890..da7b3b850 100644 --- a/crates/compositor/tests/privacy_blur_under_zoom.rs +++ b/crates/compositor/tests/privacy_blur_under_zoom.rs @@ -27,6 +27,9 @@ use openscreen_compositor::frame_geometry::live_params_from_scene; use openscreen_compositor::live::Player; use openscreen_compositor::scene::Scene; +mod common; +use common::write_ppm; + const W: u32 = 960; const H: u32 = 540; /// En plein palier du zoom : la région couvre 0..6 s. @@ -38,15 +41,6 @@ fn red_pixels(rgba: &[u8]) -> usize { rgba.chunks_exact(4).filter(|p| p[0] > 150 && p[1] < 80 && p[2] < 80).count() } -fn write_ppm(path: &std::path::Path, rgba: &[u8], w: u32, h: u32) -> std::io::Result<()> { - let mut out = Vec::with_capacity(rgba.len() / 4 * 3 + 32); - out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes()); - for px in rgba.chunks_exact(4) { - out.extend_from_slice(&px[..3]); - } - std::fs::write(path, out) -} - /// `zoom` : `None` = pas de région, sinon (échelle, focus x, focus y, rotation JSON). fn scene_json(secret: &str, masked: bool, zoom: Option<(f32, f32, f32, &str)>) -> String { let s = secret.replace('\\', "/"); diff --git a/crates/compositor/tests/screen_trail_render.rs b/crates/compositor/tests/screen_trail_render.rs index f06383456..88b659f99 100644 --- a/crates/compositor/tests/screen_trail_render.rs +++ b/crates/compositor/tests/screen_trail_render.rs @@ -12,15 +12,11 @@ use openscreen_compositor::compositor::Compositor; use openscreen_compositor::config::Cfg; use openscreen_compositor::d3d::Gpu; -use openscreen_compositor::ffi::AVFrame; use openscreen_compositor::frame_geometry::live_params_from_scene; use openscreen_compositor::scene::Scene; -use windows::core::Interface; -use windows::Win32::Graphics::Direct3D11::{ - ID3D11Texture2D, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE, D3D11_MAPPED_SUBRESOURCE, - D3D11_MAP_WRITE_DISCARD, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DYNAMIC, -}; -use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC}; + +mod common; +use common::{gpu, Nv12Frame}; const W: u32 = 1280; const H: u32 = 720; @@ -28,69 +24,18 @@ const SRC: (u32, u32) = (640, 360); /// Les rampes du zoom de `scene_json` (2 à 6 s) : l'entrée finit à 2 s, la sortie part de 6 s. const RAMPS: [f32; 8] = [1.3, 1.5, 1.7, 1.9, 6.1, 6.3, 6.5, 6.7]; -fn gpu() -> Option { - match Gpu::create(false) { - Ok(g) => Some(g), - Err(e) => { - eprintln!("pas de device D3D11 matériel ({e:#}) — test saute"); - None - } - } -} - /// Une frame NV12 synthétique : `yuv(colonne, rangée)` donne Y, Cb, Cr (BT.709 limité), la /// chroma étant prise au coin haut-gauche de chaque bloc 2×2. -struct Source { - frame: Box, - _tex: ID3D11Texture2D, -} - -impl Source { - fn new(gpu: &Gpu, yuv: impl Fn(u32, u32) -> [u8; 3]) -> Source { - let (w, h) = SRC; - let desc = D3D11_TEXTURE2D_DESC { - Width: w, - Height: h, - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_NV12, - SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 }, - Usage: D3D11_USAGE_DYNAMIC, - BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32, - CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32, - MiscFlags: 0, - }; - unsafe { - let mut tex: Option = None; - gpu.device.CreateTexture2D(&desc, None, Some(&mut tex)).expect("texture NV12"); - let tex = tex.expect("texture NV12"); - let mut m = D3D11_MAPPED_SUBRESOURCE::default(); - gpu.context.Map(&tex, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut m)).expect("Map"); - let pitch = m.RowPitch as usize; - let dst = m.pData as *mut u8; - for row in 0..h { - for col in 0..w { - *dst.add(row as usize * pitch + col as usize) = yuv(col, row)[0]; - } - } - for row in 0..h / 2 { - for col in 0..w { - let [_, u, v] = yuv(col & !1, row * 2); - *dst.add((h + row) as usize * pitch + col as usize) = if col % 2 == 0 { u } else { v }; - } - } - gpu.context.Unmap(&tex, 0); - let mut frame: Box = Box::new(std::mem::zeroed()); - frame.data[0] = tex.as_raw() as *mut u8; - frame.width = w as i32; - frame.height = h as i32; - Source { frame, _tex: tex } - } - } - - fn as_ptr(&self) -> *const AVFrame { - &*self.frame as *const AVFrame - } +fn source(gpu: &Gpu, yuv: impl Fn(u32, u32) -> [u8; 3]) -> Nv12Frame { + Nv12Frame::new( + gpu, + SRC, + |col, row| yuv(col, row)[0], + |bx, by| { + let [_, u, v] = yuv(2 * bx, 2 * by); + [u, v] + }, + ) } /// Un seul écran dans `rect` (fractions de la sortie `out`), zoomé ×`scale` sous `iso` de 2 à 6 s. @@ -112,7 +57,7 @@ fn scene_json(rect: [f32; 4], out: (u32, u32), scale: f32, roundness: f32, blur: ) } -fn render(comp: &Compositor, src: &Source, json: &str, t: f32) -> Vec { +fn render(comp: &Compositor, src: &Nv12Frame, json: &str, t: f32) -> Vec { let scene = Scene::from_json(json).expect("scène valide"); let mut live = live_params_from_scene(&scene); live.has_webcam = false; @@ -154,7 +99,7 @@ fn unsolvable_trail_taps_leave_the_background_alone() { let Some(gpu) = gpu() else { return }; let comp = Compositor::new_sized(&gpu, W, H).expect("compositor"); // Métrage rouge, repère vert dans son coin haut-gauche (sous l'arrondi de l'écran). - let src = Source::new(&gpu, |col, row| if col < 48 && row < 48 { [173, 42, 26] } else { [63, 102, 240] }); + let src = source(&gpu, |col, row| if col < 48 && row < 48 { [173, 42, 26] } else { [63, 102, 240] }); let json = |blur| scene_json([0.425, 0.425, 0.15, 0.15], (W, H), 1.6, 0.02, blur, false); let far = |x: u32, y: u32| x < W / 4 || x >= W * 3 / 4 || y < H / 4 || y >= H * 3 / 4; let mut leaks = 0; @@ -186,7 +131,7 @@ fn unsolvable_trail_taps_leave_the_background_alone() { fn off_canvas_trail_taps_keep_the_depth_of_field() { let Some(gpu) = gpu() else { return }; // Rayures noires et blanches de 2 px : la profondeur de champ les grise. - let src = Source::new(&gpu, |col, _| [if col / 2 % 2 == 0 { 16 } else { 235 }, 128, 128]); + let src = source(&gpu, |col, _| [if col / 2 % 2 == 0 { 16 } else { 235 }, 128, 128]); let normal = Compositor::new_sized(&gpu, W, H).expect("compositor"); let padded = Compositor::new_sized(&gpu, 2 * W, 2 * H).expect("compositor"); // La même boîte en px, centrée sur une sortie deux fois plus grande (le rayon se mesure sur diff --git a/crates/compositor/tests/window_frame_render.rs b/crates/compositor/tests/window_frame_render.rs index ea1a3b881..249705af6 100644 --- a/crates/compositor/tests/window_frame_render.rs +++ b/crates/compositor/tests/window_frame_render.rs @@ -19,19 +19,13 @@ use openscreen_compositor::frame_geometry::live_params_from_scene; use openscreen_compositor::live::Player; use openscreen_compositor::scene::Scene; +mod common; +use common::write_ppm; + const W: u32 = 1280; const H: u32 = 720; const AT_SEC: f64 = 3.0; -fn write_ppm(path: &std::path::Path, rgba: &[u8], w: u32, h: u32) -> std::io::Result<()> { - let mut out = Vec::with_capacity(rgba.len() / 4 * 3 + 32); - out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes()); - for px in rgba.chunks_exact(4) { - out.extend_from_slice(&px[..3]); - } - std::fs::write(path, out) -} - /// `frame` : `None` = clé absente du JSON (payload d'avant le cadre), sinon sa valeur. fn scene_json(source: &str, frame: Option<&str>, rotation: &str) -> String { let s = source.replace('\\', "/");