use facett_core::render::backend::{decide, Backend, GpuProbe};
use facett_core::render::camera::{Camera, Pos, V3};
use facett_core::render::cpu::scissor::{clip_poly_to_rect, ink_outside_rect};
use facett_core::render::cpu::sdf;
use facett_core::render::layer::{LayerKind, LayerStack};
use facett_core::render::prim::{
cap, shape, CircleInstance, LineInstance, MarkerInstance, QuadInstance, RingInstance,
};
use facett_core::render::{Canvas, CpuCanvas, Frame};
fn px(rgba: &[u8], w: u32, x: u32, y: u32) -> [u8; 4] {
let i = ((y * w + x) * 4) as usize;
[rgba[i], rgba[i + 1], rgba[i + 2], rgba[i + 3]]
}
fn frame_fingerprint(f: &Frame) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for &b in &f.rgba {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
h ^= (f.width as u64) << 32 | f.height as u64;
h
}
#[cfg(feature = "testmatrix")]
fn emit(component: &str, check: &str, ok: bool, detail: &str) {
facett_core::testmatrix::emit(component, check, ok, detail);
}
#[cfg(not(feature = "testmatrix"))]
fn emit(_component: &str, _check: &str, _ok: bool, _detail: &str) {}
#[test]
fn stage1_lowering_carries_shape_and_geometry() {
let mut ok = true;
for &r in &[2.0f32, 7.0, 19.0, 40.0] {
for &aa in &[0.0f32, 1.0, 3.0] {
let c = CircleInstance { center: [5.0, 6.0], radius: r, color: [1.0; 4], aa }.lower();
ok &= c.shape == shape::CIRCLE && c.inner == 0.0 && (c.half_extent() - (r + aa)).abs() < 1e-6;
let ring = RingInstance { center: [0.0; 2], radius: r, inner: r * 2.0, color: [1.0; 4], aa }.lower();
ok &= ring.shape == shape::RING && (ring.inner - r).abs() < 1e-6;
for &sh in &[shape::SQUARE, shape::TRIANGLE, shape::DIAMOND] {
let m = MarkerInstance { center: [1.0, 2.0], radius: r, corner: 1.5, color: [1.0; 4], aa, shape: sh }.lower();
ok &= m.shape == sh && (m.inner - 1.5).abs() < 1e-6;
}
let l = LineInstance::round([10.0, 10.0], [30.0, 14.0], r, aa, [1.0; 4]);
let (mnx, mny, mxx, mxy) = l.bounds();
ok &= (mnx - (10.0 - r - aa)).abs() < 1e-4
&& (mny - (10.0 - r - aa)).abs() < 1e-4
&& (mxx - (30.0 + r + aa)).abs() < 1e-4
&& (mxy - (14.0 + r + aa)).abs() < 1e-4;
ok &= LineInstance::butt([0.0; 2], [1.0, 0.0], r, aa, [0.0; 4]).cap == cap::BUTT;
ok &= l.cap == cap::ROUND;
}
}
assert!(ok, "lowering carries shape + clamped inner + inflated bounds");
emit("render::prim", "lower_shape_geometry_bounds", ok, "swept radius×aa over circle/ring/marker/line");
}
#[test]
fn stage2_camera_projection_matches_golden() {
let cam = Camera { pan_x: 30.0, pan_y: -12.0, zoom: 2.5, ..Camera::default() };
let (sx, sy) = cam.project2d(Pos::new(10.0, 4.0));
let affine_ok = (sx - (10.0 * 2.5 + 30.0)).abs() < 1e-6 && (sy - (4.0 * 2.5 - 12.0)).abs() < 1e-6;
let cam3 = Camera { distance: 5.0, fov_y: 50f32.to_radians(), ..Camera::default() };
let center = (400.0, 300.0);
let mid = cam3.project_view_world(V3::new(0.0, 0.0, 0.0), center, 300.0);
let near = cam3.project_view_world(V3::new(0.5, 0.0, 2.0), center, 300.0);
let far = cam3.project_view_world(V3::new(0.5, 0.0, -2.0), center, 300.0);
let behind = cam3.project_view_world(cam3.eye().add(cam3.basis().0.scale(-1.0)), center, 300.0);
let persp_ok = mid.visible
&& (mid.x - center.0).abs() < 1.0
&& (near.x - center.0).abs() > (far.x - center.0).abs()
&& !behind.visible;
let r = cam3.eye().sub(cam3.target).len();
let vs_ok = (r - 5.0).abs() < 1e-4;
let ok = affine_ok && persp_ok && vs_ok;
assert!(ok, "camera projection matches golden affine + perspective");
emit("render::camera", "project2d_view_proj_golden", ok, &format!("affine={affine_ok} persp={persp_ok} viewspace={vs_ok}"));
}
#[test]
fn stage3_layer_stack_orders_back_to_front() {
let mut s = LayerStack::new();
s.add(10.0, LayerKind::Graph);
s.add(0.0, LayerKind::Map3d);
s.add(5.0, LayerKind::Map2d);
let o = s.ordered();
let order_ok = o[0].kind == LayerKind::Map3d && o[1].kind == LayerKind::Map2d && o[2].kind == LayerKind::Graph;
let mut t = LayerStack::new();
t.add(1.0, LayerKind::Map2d);
t.add(2.0, LayerKind::Overlay);
t.add(2.0, LayerKind::Graph);
let to = t.ordered();
let tie_ok = to[1].kind == LayerKind::Overlay && to[2].kind == LayerKind::Graph;
let ok = order_ok && tie_ok && s.top().unwrap().kind == LayerKind::Graph;
assert!(ok, "layers ascending-z with stable ties");
emit("render::layer", "ordered_back_to_front_stable", ok, &format!("order={order_ok} tie={tie_ok}"));
}
#[test]
fn stage4_5_sdf_coverage_kernel_swept() {
let mut ok = true;
for &aa in &[0.5f32, 1.0, 2.0, 4.0] {
ok &= coverage_from_sd_monotone(aa);
}
ok &= sdf::coverage_from_sd(-0.01, 0.0) == 1.0 && sdf::coverage_from_sd(0.01, 0.0) == 0.0;
for &r in &[5.0f32, 12.0, 30.0] {
let circle = CircleInstance { center: [0.0; 2], radius: r, color: [1.0; 4], aa: 1.0 }.lower();
ok &= sdf::quad_coverage(&circle, 0.0, 0.0) > 0.99 && sdf::quad_coverage(&circle, r * 2.0, 0.0) < 0.01;
let ring = RingInstance { center: [0.0; 2], radius: r, inner: r * 0.5, color: [1.0; 4], aa: 1.0 }.lower();
ok &= sdf::quad_coverage(&ring, 0.0, 0.0) < 0.01 && sdf::quad_coverage(&ring, r * 0.75, 0.0) > 0.9;
ok &= sdf::square_coverage(r * 0.8, r * 0.8, r, 0.0, 0.5) > 0.9;
ok &= sdf::diamond_coverage(r * 0.8, r * 0.8, r, 0.5) < 0.1;
ok &= sdf::triangle_coverage(0.0, r * 0.7, r, 0.5) > 0.9;
let hw = 3.0;
let l = LineInstance::round([10.0, 50.0], [90.0, 50.0], hw, 1.0, [1.0; 4]);
ok &= sdf::line_coverage(&l, [50.0, 50.0]) > 0.99 && sdf::line_coverage(&l, [50.0, 50.0 + hw + 4.0]) < 0.05;
}
assert!(ok, "SDF coverage kernel + dispatch correct across the sweep");
emit("render::cpu::sdf", "coverage_kernel_and_dispatch", ok, "monotone AA + per-shape dispatch + line width");
}
fn coverage_from_sd_monotone(aa: f32) -> bool {
if sdf::coverage_from_sd(-aa - 1.0, aa) < 0.999 {
return false;
}
if sdf::coverage_from_sd(aa + 1.0, aa) > 0.001 {
return false;
}
let mut prev = 1.0f32;
let mut d = -aa;
while d <= aa {
let c = sdf::coverage_from_sd(d, aa);
if c > prev + 1e-6 {
return false;
}
prev = c;
d += aa / 16.0;
}
true
}
#[test]
fn stage8_scissor_confines_and_leaves_no_ink_outside() {
let rect = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(100.0, 100.0));
let sprawl = [egui::pos2(-500.0, 50.0), egui::pos2(50.0, -500.0), egui::pos2(600.0, 600.0)];
let poly = clip_poly_to_rect(&sprawl, rect);
let mut inside = poly.len() >= 3;
for p in &poly {
inside &= p.x >= -1e-3 && p.x <= 100.0 + 1e-3 && p.y >= -1e-3 && p.y <= 100.0 + 1e-3;
}
let area = shoelace(&poly);
let area_ok = area <= 100.0 * 100.0 + 1.0 && area > 0.0;
let oracle_ok = ink_outside_rect(&[sprawl], rect) == 0
&& clip_poly_to_rect(&[egui::pos2(200.0, 200.0), egui::pos2(300.0, 200.0), egui::pos2(250.0, 300.0)], rect).len() < 3;
let ok = inside && area_ok && oracle_ok;
assert!(ok, "scissor confines geometry + leaves no ink outside the rect");
emit("render::cpu::scissor", "clip_area_and_zero_ink_outside", ok, &format!("inside={inside} area={area:.0} oracle={oracle_ok}"));
}
fn shoelace(poly: &[egui::Pos2]) -> f32 {
if poly.len() < 3 {
return 0.0;
}
let mut a = 0.0f32;
for i in 0..poly.len() {
let p = poly[i];
let q = poly[(i + 1) % poly.len()];
a += p.x * q.y - q.x * p.y;
}
(a * 0.5).abs()
}
#[test]
fn stage9_backend_decide_maps_probe_to_backend() {
let cpu_only = decide(GpuProbe::cpu_only()) == Backend::CpuVello;
let forced = decide(GpuProbe { usable_gpu: true, force_cpu: true }) == Backend::CpuVello;
let gpu = decide(GpuProbe::gpu());
let gpu_ok = if cfg!(feature = "wgpu") { gpu == Backend::GpuVello } else { gpu == Backend::CpuVello };
let ok = cpu_only && forced && gpu_ok;
assert!(ok, "backend::decide maps probe → backend");
emit("render::backend", "decide_probe_to_backend", ok, &format!("cpu_only={cpu_only} forced={forced} gpu={gpu_ok}"));
}
#[test]
fn raster_matrix_sweep_emits_one_row_per_case() {
let shapes = [shape::CIRCLE, shape::RING, shape::SQUARE, shape::TRIANGLE, shape::DIAMOND];
let radii = [4.0f32, 8.0, 16.0, 24.0, 32.0];
let aas = [0.0f32, 0.75, 1.5, 3.0];
let canvases = [(48u32, 48u32), (128, 96), (300, 200), (320, 320)];
let inst_counts = [1usize, 8, 64];
let workers = [1usize, 0];
let mut cases = 0usize;
let mut passed = 0usize;
for &sh in &shapes {
for &radius in &radii {
for &aa in &aas {
for &(w, h) in &canvases {
for &n in &inst_counts {
let mut fps = [0u64; 2];
let mut lit = [0usize; 2];
let mut pixel_ok = true;
for (wi, &wk) in workers.iter().enumerate() {
let frame = render_case(sh, radius, aa, w, h, n, wk);
fps[wi] = frame_fingerprint(&frame);
lit[wi] = frame.lit_px();
pixel_ok &= centre_pixel_correct(&frame, w, h, sh);
pixel_ok &= frame.rgba.len() == (w * h * 4) as usize;
}
let parity_ok = fps[0] == fps[1] && lit[0] == lit[1];
let drew = lit[0] > 0; cases += 1;
let case_ok = parity_ok && drew && pixel_ok;
if case_ok {
passed += 1;
}
emit(
"render::cpu::raster",
"matrix_case",
case_ok,
&format!(
"shape={sh} r={radius} aa={aa} canvas={w}x{h} n={n} lit={} fp={:#018x} parity={parity_ok}",
lit[0], fps[0]
),
);
assert!(
case_ok,
"raster case failed: shape={sh} r={radius} aa={aa} canvas={w}x{h} n={n} parity={parity_ok} drew={drew} pixel_ok={pixel_ok}"
);
}
}
}
}
}
assert!(cases >= 1000, "matrix has at least ~1000 cases, got {cases}");
assert_eq!(passed, cases, "every matrix case green");
emit(
"render::cpu::raster",
"matrix_sweep_complete",
passed == cases,
&format!("cases={cases} passed={passed} (gatling N-vs-1 byte-identical, pixels correct)"),
);
eprintln!("[render_callchain_matrix] raster matrix: {passed}/{cases} cases green");
}
fn render_case(sh: u32, radius: f32, aa: f32, w: u32, h: u32, n: usize, workers: usize) -> Frame {
let mut canvas = CpuCanvas::new(w, h, Camera::default(), [8, 10, 16, 255]);
let cx = w as f32 / 2.0;
let cy = h as f32 / 2.0;
let mut quads: Vec<QuadInstance> = Vec::with_capacity(n);
for i in 0..n {
let (px, py) = if i == 0 {
(cx, cy)
} else {
(((i * 37) % (w as usize).max(1)) as f32, ((i * 53) % (h as usize).max(1)) as f32)
};
let col = [
(i % 7) as f32 / 7.0 + 0.2,
(i % 5) as f32 / 5.0 + 0.2,
(i % 3) as f32 / 3.0 + 0.2,
0.9,
];
let q = match sh {
shape::CIRCLE => CircleInstance { center: [px, py], radius, color: col, aa }.lower(),
shape::RING => RingInstance { center: [px, py], radius, inner: radius * 0.5, color: col, aa }.lower(),
_ => MarkerInstance { center: [px, py], radius, corner: radius * 0.2, color: col, aa, shape: sh }.lower(),
};
quads.push(q);
}
let lines = vec![
LineInstance::round([2.0, 2.0], [w as f32 - 2.0, h as f32 - 2.0], 2.0, aa.max(0.5), [0.9, 0.9, 0.3, 0.8]),
LineInstance::butt([2.0, h as f32 - 2.0], [w as f32 - 2.0, 2.0], 1.5, aa.max(0.5), [0.3, 0.8, 0.9, 0.8]),
];
canvas.push_lines(&lines);
canvas.push_quads(&quads);
canvas.rasterize_with_workers(workers)
}
fn centre_pixel_correct(frame: &Frame, w: u32, h: u32, sh: u32) -> bool {
let bg = [8i32, 10, 16];
let far_from_bg = |p: [u8; 4]| {
(p[0] as i32 - bg[0]).abs() + (p[1] as i32 - bg[1]).abs() + (p[2] as i32 - bg[2]).abs() > 24
};
let cx = w / 2;
let cy = h / 2;
if sh == shape::RING {
for d in [3u32, 4, 5, 6, 8, 10] {
if cx + d < w && far_from_bg(px(&frame.rgba, w, cx + d, cy)) {
return true;
}
}
return frame.lit_px() > 0;
}
far_from_bg(px(&frame.rgba, w, cx, cy))
}
#[cfg(feature = "wgpu")]
#[test]
fn stage10_gpu_offscreen_nonblank_and_cpu_parity() {
use facett_core::render::gpu::offscreen_render;
let (w, h) = (256u32, 256u32);
let bg = [0.05f32, 0.06, 0.08, 1.0];
let quads = vec![
CircleInstance { center: [60.0, 60.0], radius: 24.0, color: [1.0, 0.2, 0.1, 1.0], aa: 1.5 }.lower(),
RingInstance { center: [180.0, 60.0], radius: 26.0, inner: 14.0, color: [0.1, 0.9, 0.3, 1.0], aa: 1.5 }.lower(),
MarkerInstance { center: [60.0, 180.0], radius: 22.0, corner: 0.0, color: [0.3, 0.5, 1.0, 1.0], aa: 1.0, shape: shape::DIAMOND }.lower(),
];
let lines = vec![LineInstance::round([120.0, 130.0], [230.0, 220.0], 5.0, 1.5, [1.0, 1.0, 0.2, 1.0])];
let Some(gpu) = offscreen_render(w, h, bg, &quads, &lines) else {
eprintln!("[render_callchain_matrix] no wgpu adapter — GPU surface row skipped (CPU pixmap asserted instead)");
let mut canvas = CpuCanvas::new(w, h, Camera::default(), [13, 15, 20, 255]);
canvas.push_lines(&lines);
canvas.push_quads(&quads);
let cpu = canvas.rasterize();
let lit = cpu.lit_px();
assert!(lit > 1000, "CPU pixmap non-blank ({lit})");
emit("render::cpu::surface", "headless_nonblank", lit > 1000, &format!("lit_px={lit} (no gpu adapter)"));
return;
};
let bg8 = [(bg[0] * 255.0) as u8, (bg[1] * 255.0) as u8, (bg[2] * 255.0) as u8];
let ink = |rgba: &[u8]| -> usize {
rgba.chunks_exact(4)
.filter(|c| (c[0] as i32 - bg8[0] as i32).abs() + (c[1] as i32 - bg8[1] as i32).abs() + (c[2] as i32 - bg8[2] as i32).abs() > 24)
.count()
};
let gpu_ink = ink(&gpu.rgba);
let nonblank = gpu_ink > 1000;
assert!(nonblank, "GPU offscreen non-blank ({gpu_ink})");
let mut canvas = CpuCanvas::new(w, h, Camera::default(), [bg8[0], bg8[1], bg8[2], 255]);
canvas.push_lines(&lines);
canvas.push_quads(&quads);
let cpu = canvas.rasterize();
let mut disagree = 0usize;
for y in 0..h {
for x in 0..w {
let gi = !near_bg(&gpu.rgba, w, x, y, bg8);
let ci = !near_bg(&cpu.rgba, w, x, y, bg8);
if gi != ci {
disagree += 1;
}
}
}
let frac = disagree as f64 / (w * h) as f64;
let parity = frac < 0.04;
assert!(parity, "GPU/CPU footprints agree (disagreement {frac:.4})");
emit("render::gpu::offscreen", "nonblank_and_cpu_parity", nonblank && parity, &format!("gpu_ink={gpu_ink} disagreement={frac:.4}"));
}
#[cfg(feature = "wgpu")]
fn near_bg(rgba: &[u8], w: u32, x: u32, y: u32, bg8: [u8; 3]) -> bool {
let p = px(rgba, w, x, y);
(p[0] as i32 - bg8[0] as i32).abs() + (p[1] as i32 - bg8[1] as i32).abs() + (p[2] as i32 - bg8[2] as i32).abs() < 24
}