Skip to content

Commit df7a3ab

Browse files
committed
test(compositor): share the D3D11 render-test harness
Five Windows render tests each carried their own copy of the same setup: an identical `gpu()` probe, and a synthetic NV12 source frame (FakeFrame, MockFrame, GridFrame, Source) whose texture creation, map/unmap and AVFrame wrapping were the same ~40 lines around a different pixel fill. Four tests also carried the same `write_ppm`. tests/common/ now holds them once: - `gpu()`, unchanged. - `Nv12Frame::new(gpu, size, luma, chroma)`: `luma(col, row)` fills the Y plane, `chroma(bx, by)` returns `[Cb, Cr]` for each 2x2 block. Each test keeps its own fill as a small builder next to its existing doc comment (`fake_frame`, `mock_frame`, `grid_frame`, `source`). - `write_ppm`, unchanged, with the doc comment it had in output_geometry_golden. The D3D pieces sit behind `#[cfg(windows)]`, so output_geometry_golden, which also runs on macOS, only pulls in `write_ppm`. The textures are byte-for-byte what the old loops wrote. The old constant-chroma loops wrote every column of the UV plane alternating U/V, which equals one write per 2x2 block because every source size these tests use is even (NV12 requires it, and the aspect-ratio cases already round with `& !1`). No test logic, assertion or threshold changes.
1 parent 46c304e commit df7a3ab

11 files changed

Lines changed: 264 additions & 479 deletions

‎crates/compositor/tests/animated_background.rs‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,15 @@ use openscreen_compositor::frame_geometry::live_params_from_scene;
2828
use openscreen_compositor::live::Player;
2929
use openscreen_compositor::scene::Scene;
3030

31+
mod common;
32+
use common::write_ppm;
33+
3134
const W: u32 = 960;
3235
const H: u32 = 540;
3336
/// Trois clips de 6 s du même fichier : le clip `k` lu à 3 s source tombe à `6k + 3` s de
3437
/// programme. La frame vidéo est la même, seul le temps programme change.
3538
const PROGRAMME_TIMES: [f32; 3] = [3.0, 9.0, 15.0];
3639

37-
fn write_ppm(path: &std::path::Path, rgba: &[u8], w: u32, h: u32) -> std::io::Result<()> {
38-
let mut out = Vec::with_capacity(rgba.len() / 4 * 3 + 32);
39-
out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
40-
for px in rgba.chunks_exact(4) {
41-
out.extend_from_slice(&px[..3]);
42-
}
43-
std::fs::write(path, out)
44-
}
45-
4640
/// Le fond, ouvert : `scene_json` y ajoute le mouvement puis referme l'accolade.
4741
const GRADIENT: &str = r##"{"kind":"gradient","angleDeg":135,"stops":["#2b3a67","#b8577f"]"##;
4842

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#![allow(dead_code)]
2+
3+
#[cfg(windows)]
4+
mod nv12;
5+
#[cfg(windows)]
6+
pub use nv12::{gpu, Nv12Frame};
7+
8+
/// PPM P6 — pas de dépendance à encoder, et ça s'ouvre dans n'importe quel
9+
/// visionneur. Permet l'inspection à l'œil en plus de la comparaison de hash.
10+
pub fn write_ppm(path: &std::path::Path, rgba: &[u8], w: u32, h: u32) -> std::io::Result<()> {
11+
let mut out = Vec::with_capacity(rgba.len() / 4 * 3 + 32);
12+
out.extend_from_slice(format!("P6\n{w} {h}\n255\n").as_bytes());
13+
for px in rgba.chunks_exact(4) {
14+
out.extend_from_slice(&px[..3]);
15+
}
16+
std::fs::write(path, out)
17+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use openscreen_compositor::d3d::Gpu;
2+
use openscreen_compositor::ffi::AVFrame;
3+
use windows::core::Interface;
4+
use windows::Win32::Graphics::Direct3D11::{
5+
ID3D11Texture2D, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE,
6+
D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_WRITE_DISCARD, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DYNAMIC,
7+
};
8+
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC};
9+
10+
pub fn gpu() -> Option<Gpu> {
11+
match Gpu::create(false) {
12+
Ok(g) => Some(g),
13+
Err(e) => {
14+
eprintln!("pas de device D3D11 matériel ({e:#}) — test saute");
15+
None
16+
}
17+
}
18+
}
19+
20+
pub struct Nv12Frame {
21+
frame: Box<AVFrame>,
22+
_tex: ID3D11Texture2D,
23+
}
24+
25+
impl Nv12Frame {
26+
pub fn new(
27+
gpu: &Gpu,
28+
(w, h): (u32, u32),
29+
luma: impl Fn(u32, u32) -> u8,
30+
chroma: impl Fn(u32, u32) -> [u8; 2],
31+
) -> Nv12Frame {
32+
let desc = D3D11_TEXTURE2D_DESC {
33+
Width: w,
34+
Height: h,
35+
MipLevels: 1,
36+
ArraySize: 1,
37+
Format: DXGI_FORMAT_NV12,
38+
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
39+
Usage: D3D11_USAGE_DYNAMIC,
40+
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
41+
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
42+
MiscFlags: 0,
43+
};
44+
unsafe {
45+
let mut tex: Option<ID3D11Texture2D> = None;
46+
gpu.device.CreateTexture2D(&desc, None, Some(&mut tex)).expect("texture NV12");
47+
let tex = tex.expect("texture NV12");
48+
let mut m = D3D11_MAPPED_SUBRESOURCE::default();
49+
gpu.context.Map(&tex, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut m)).expect("Map");
50+
let pitch = m.RowPitch as usize;
51+
let dst = m.pData as *mut u8;
52+
for row in 0..h {
53+
for col in 0..w {
54+
*dst.add(row as usize * pitch + col as usize) = luma(col, row);
55+
}
56+
}
57+
for by in 0..h / 2 {
58+
for bx in 0..w / 2 {
59+
let [cb, cr] = chroma(bx, by);
60+
let uv = (h + by) as usize * pitch + 2 * bx as usize;
61+
*dst.add(uv) = cb;
62+
*dst.add(uv + 1) = cr;
63+
}
64+
}
65+
gpu.context.Unmap(&tex, 0);
66+
let mut frame: Box<AVFrame> = Box::new(std::mem::zeroed());
67+
frame.data[0] = tex.as_raw() as *mut u8;
68+
frame.data[1] = std::ptr::null_mut();
69+
frame.width = w as i32;
70+
frame.height = h as i32;
71+
Nv12Frame { frame, _tex: tex }
72+
}
73+
}
74+
75+
pub fn as_ptr(&self) -> *const AVFrame {
76+
&*self.frame as *const AVFrame
77+
}
78+
}

‎crates/compositor/tests/cursor_model_render.rs‎

Lines changed: 28 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,14 @@ use openscreen_compositor::compositor::Compositor;
1919
use openscreen_compositor::config::Cfg;
2020
use openscreen_compositor::cursor::CursorTrack;
2121
use openscreen_compositor::d3d::Gpu;
22-
use openscreen_compositor::ffi::AVFrame;
2322
use openscreen_compositor::frame_geometry::{
2423
live_params_from_scene, plan_cursor, plan_frame, CursorPlacement, CursorPlanInput,
2524
FrameGeometryInput,
2625
};
2726
use openscreen_compositor::scene::Scene;
28-
use windows::core::Interface;
29-
use windows::Win32::Graphics::Direct3D11::{
30-
ID3D11Texture2D, D3D11_BIND_SHADER_RESOURCE, D3D11_CPU_ACCESS_WRITE,
31-
D3D11_MAPPED_SUBRESOURCE, D3D11_MAP_WRITE_DISCARD, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DYNAMIC,
32-
};
33-
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_NV12, DXGI_SAMPLE_DESC};
27+
28+
mod common;
29+
use common::{gpu, Nv12Frame};
3430

3531
const SRC: (u32, u32) = (640, 360);
3632
/// Instant rendu : en plein palier de la région de zoom (0..10 s).
@@ -67,16 +63,6 @@ fn is_centred(key: &str) -> bool {
6763
matches!(key, "text" | "resize-ew" | "not-allowed")
6864
}
6965

70-
fn gpu() -> Option<Gpu> {
71-
match Gpu::create(false) {
72-
Ok(g) => Some(g),
73-
Err(e) => {
74-
eprintln!("pas de device D3D11 matériel ({e:#}) — test saute");
75-
None
76-
}
77-
}
78-
}
79-
8066
/// Deux teintes de contenu : un pixel identique sur les deux est du modèle opaque, un pixel qui
8167
/// change est le contenu, son ombre ou une frange antialiasée.
8268
#[derive(Clone, Copy)]
@@ -89,63 +75,20 @@ enum Tint {
8975
/// D3D11VA (le seul contrat que `Compositor::nv12_srvs` lit). Une teinte moyenne striée de barres
9076
/// plus sombres : rien d'aussi clair ni d'aussi sombre que le curseur, et surtout rien de NEUTRE,
9177
/// si bien qu'un pixel gris est le curseur et un pixel teinté assombri, son ombre.
92-
struct FakeFrame {
93-
frame: Box<AVFrame>,
94-
_tex: ID3D11Texture2D,
95-
}
96-
97-
impl FakeFrame {
98-
fn new(gpu: &Gpu, tint: Tint) -> FakeFrame {
99-
let (w, h) = SRC;
100-
let (u, v) = match tint {
101-
Tint::Blue => (150, 120),
102-
Tint::Orange => (100, 170),
103-
};
104-
let desc = D3D11_TEXTURE2D_DESC {
105-
Width: w,
106-
Height: h,
107-
MipLevels: 1,
108-
ArraySize: 1,
109-
Format: DXGI_FORMAT_NV12,
110-
SampleDesc: DXGI_SAMPLE_DESC { Count: 1, Quality: 0 },
111-
Usage: D3D11_USAGE_DYNAMIC,
112-
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
113-
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
114-
MiscFlags: 0,
115-
};
116-
unsafe {
117-
let mut tex: Option<ID3D11Texture2D> = None;
118-
gpu.device.CreateTexture2D(&desc, None, Some(&mut tex)).expect("texture NV12");
119-
let tex = tex.expect("texture NV12");
120-
let mut m = D3D11_MAPPED_SUBRESOURCE::default();
121-
gpu.context.Map(&tex, 0, D3D11_MAP_WRITE_DISCARD, 0, Some(&mut m)).expect("Map");
122-
let pitch = m.RowPitch as usize;
123-
let dst = m.pData as *mut u8;
124-
for row in 0..h as usize {
125-
for col in 0..w as usize {
126-
let bar = row % 24 >= 8 && row % 24 < 12 && (col / 40) % 3 != 2;
127-
*dst.add(row * pitch + col) = if bar { 90 } else { 150 };
128-
}
129-
}
130-
for row in 0..(h / 2) as usize {
131-
for col in 0..w as usize {
132-
let uv = (h as usize + row) * pitch + col;
133-
*dst.add(uv) = if col % 2 == 0 { u } else { v };
134-
}
135-
}
136-
gpu.context.Unmap(&tex, 0);
137-
let mut frame: Box<AVFrame> = Box::new(std::mem::zeroed());
138-
frame.data[0] = tex.as_raw() as *mut u8;
139-
frame.data[1] = std::ptr::null_mut();
140-
frame.width = w as i32;
141-
frame.height = h as i32;
142-
FakeFrame { frame, _tex: tex }
143-
}
144-
}
145-
146-
fn as_ptr(&self) -> *const AVFrame {
147-
&*self.frame as *const AVFrame
148-
}
78+
fn fake_frame(gpu: &Gpu, tint: Tint) -> Nv12Frame {
79+
let [u, v] = match tint {
80+
Tint::Blue => [150, 120],
81+
Tint::Orange => [100, 170],
82+
};
83+
Nv12Frame::new(
84+
gpu,
85+
SRC,
86+
|col, row| {
87+
let bar = row % 24 >= 8 && row % 24 < 12 && (col / 40) % 3 != 2;
88+
if bar { 90 } else { 150 }
89+
},
90+
|_, _| [u, v],
91+
)
14992
}
15093

15194
/// Les seize sprites livrés, au format `cursorSprites` de la scène.
@@ -287,15 +230,15 @@ struct Probe {
287230
unit: f32,
288231
}
289232

290-
fn render(comp: &Compositor, screen: &FakeFrame, json: &str, track: &CursorTrack) -> (Vec<u8>, Probe) {
233+
fn render(comp: &Compositor, screen: &Nv12Frame, json: &str, track: &CursorTrack) -> (Vec<u8>, Probe) {
291234
let (rgba, probe) = render_any(comp, screen, json, track);
292235
(rgba, probe.expect("un curseur à dessiner"))
293236
}
294237

295238
/// `render`, sans exiger de curseur (scène au curseur masqué).
296239
fn render_any(
297240
comp: &Compositor,
298-
screen: &FakeFrame,
241+
screen: &Nv12Frame,
299242
json: &str,
300243
track: &CursorTrack,
301244
) -> (Vec<u8>, Option<Probe>) {
@@ -517,7 +460,7 @@ fn save(name: &str, rgba: &[u8]) {
517460
fn the_modelled_arrow_stands_on_the_screen_and_casts_its_shadow() {
518461
let Some(gpu) = gpu() else { return };
519462
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor");
520-
let screen = FakeFrame::new(&gpu, Tint::Blue);
463+
let screen = fake_frame(&gpu, Tint::Blue);
521464
let still = resting("still", false);
522465
let clicked = resting("clicked", true);
523466

@@ -580,7 +523,7 @@ fn the_modelled_arrow_stands_on_the_screen_and_casts_its_shadow() {
580523
fn every_state_keeps_its_art_and_its_footprint() {
581524
let Some(gpu) = gpu() else { return };
582525
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor");
583-
let (blue, orange) = (FakeFrame::new(&gpu, Tint::Blue), FakeFrame::new(&gpu, Tint::Orange));
526+
let (blue, orange) = (fake_frame(&gpu, Tint::Blue), fake_frame(&gpu, Tint::Orange));
584527
const SIZE: f32 = 5.0;
585528
let bare = render_any(&comp, &blue, &hidden_json("null"), &resting("bare", false)).0;
586529
let mut failures = Vec::new();
@@ -647,7 +590,7 @@ fn every_state_keeps_its_art_and_its_footprint() {
647590
fn the_tilt_turns_every_state_with_the_screen() {
648591
let Some(gpu) = gpu() else { return };
649592
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor");
650-
let screen = FakeFrame::new(&gpu, Tint::Blue);
593+
let screen = fake_frame(&gpu, Tint::Blue);
651594
for key in TESTED {
652595
let mask = |rotation: &str, x: f32, y: f32| -> (Vec<bool>, Probe) {
653596
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() {
686629
fn without_the_model_the_cursor_renders_the_flat_sprite() {
687630
let Some(gpu) = gpu() else { return };
688631
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor");
689-
let screen = FakeFrame::new(&gpu, Tint::Blue);
632+
let screen = fake_frame(&gpu, Tint::Blue);
690633
let still = resting("flat-sprite", false);
691634
let clicked = resting("flat-sprite-clicked", true);
692635
let text = resting_as("flat-sprite-text", Some("text"), false);
@@ -727,7 +670,7 @@ fn without_the_model_the_cursor_renders_the_flat_sprite() {
727670
fn a_moving_pointer_turns_towards_its_motion_and_a_centred_cursor_does_not() {
728671
let Some(gpu) = gpu() else { return };
729672
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor");
730-
let screen = FakeFrame::new(&gpu, Tint::Blue);
673+
let screen = fake_frame(&gpu, Tint::Blue);
731674
let json = scene_json(r#""iso""#, Some(true), "default", 0.0, 3.0);
732675
// En T, la piste mobile passe en x = 0,2 + 0,6 × T / (T + 1) = 0,6 : la piste immobile y est.
733676
let moving_as = |key: Option<&str>| {
@@ -771,7 +714,7 @@ fn a_moving_pointer_turns_towards_its_motion_and_a_centred_cursor_does_not() {
771714
fn the_motion_blur_trail_draws_modelled_copies() {
772715
let Some(gpu) = gpu() else { return };
773716
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor");
774-
let screen = FakeFrame::new(&gpu, Tint::Blue);
717+
let screen = fake_frame(&gpu, Tint::Blue);
775718
let moving = track("trail", &[(0.0, 0.2, 0.45, false), (T + 1.0, 0.8, 0.45, false)]);
776719
let (rgba, _) = render(&comp, &screen, &scene_json("null", Some(true), "default", 1.0, 3.0), &moving);
777720
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() {
788731
fn the_sculpted_cursors_stand_at_the_hotspot() {
789732
let Some(gpu) = gpu() else { return };
790733
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor");
791-
let (blue, orange) = (FakeFrame::new(&gpu, Tint::Blue), FakeFrame::new(&gpu, Tint::Orange));
734+
let (blue, orange) = (fake_frame(&gpu, Tint::Blue), fake_frame(&gpu, Tint::Orange));
792735
let bare = render_any(&comp, &blue, &hidden_json("null"), &resting("sculpt-bare", false)).0;
793736
let bare_orange = render_any(&comp, &orange, &hidden_json("null"), &resting("sculpt-bare", false)).0;
794737
let mut failures = Vec::new();
@@ -860,7 +803,7 @@ fn contact_sheets() {
860803
};
861804
let Some(gpu) = gpu() else { return };
862805
let comp = Compositor::new_sized(&gpu, 1280, 720).expect("compositor");
863-
let screen = FakeFrame::new(&gpu, Tint::Blue);
806+
let screen = fake_frame(&gpu, Tint::Blue);
864807
const CELL: u32 = 300;
865808
let crop = |rgba: &[u8], p: Probe| {
866809
let img = image::RgbaImage::from_raw(1280, 720, rgba.to_vec()).expect("readback");
@@ -928,7 +871,7 @@ fn bench_the_modelled_cursor_at_1080p() {
928871
gpu
929872
};
930873
let comp = Compositor::new_sized(&gpu, 1920, 1080).expect("compositor");
931-
let screen = FakeFrame::new(&gpu, Tint::Blue);
874+
let screen = fake_frame(&gpu, Tint::Blue);
932875
let still = resting("bench", false);
933876
let text = resting_as("bench-text", Some("text"), false);
934877
// Un geste rapide : la traînée prend ses 16 copies.

0 commit comments

Comments
 (0)