facett-demo/tests/cpu_raster_look.rs
//! THE wasm path, finally rasterized by hand. egui_kittest always uses wgpu on
//! native, so we've never actually SEEN the CPU painter's output. Here we call
//! `shade_faces` (the exact wasm/CPU path) and rasterize the returned triangles
//! ourselves (painter's order) into a PNG — the literal pixels the wasm demo draws.
//! Run: `cargo test -p facett-demo --test cpu_raster_look -- --nocapture`
use egui::Color32;
use facett_demo::osm_tabs;
use image::{Rgba, RgbaImage};
const W: u32 = 700;
const H: u32 = 700;
fn raster(name: &str, dist_mult: f32, elev_deg: f32) -> f32 {
let geo = osm_tabs::load();
let mut m = osm_tabs::osm3d_view(&geo).with_gpu(false);
m.spin = false;
let framed = m.render_camera().distance;
m.cam.distance = framed * dist_mult;
m.cam.goal_distance = m.cam.distance;
m.cam.elevation = elev_deg.to_radians();
m.cam.goal_elevation = m.cam.elevation;
m.cam.settle();
let bg = Color32::from_rgb(11, 13, 16);
let center = egui::Pos2::new(W as f32 * 0.5, H as f32 * 0.5);
let faces = m.shade_faces(center, H as f32 * 0.5, bg);
// Paint background, then fill every face in the returned (far→near) order.
let mut img = RgbaImage::from_pixel(W, H, Rgba([bg.r(), bg.g(), bg.b(), 255]));
for (_d, t, col) in &faces {
fill_tri(&mut img, t, *col);
}
let dir = std::env::temp_dir().join("facett-demo");
std::fs::create_dir_all(&dir).ok();
let png = dir.join(format!("cpuraster_{name}.png"));
img.save(&png).ok();
// How much of the frame is a SINGLE colour (a frame-filling terrain smear shows
// as one colour dominating) + max single-triangle screen coverage.
let mut max_cov = 0.0f32;
for (_d, t, _c) in &faces {
let a = tri_area(t);
max_cov = max_cov.max(a / (W as f32 * H as f32));
}
eprintln!(
"[cpuraster] {name}: wrote {} · faces={} · biggest single triangle covers {:.1}% of frame",
png.display(),
faces.len(),
max_cov * 100.0
);
// MATRIX EMIT: the wasm CPU render has no frame-filling smear triangle. A
// clipping smear drives a single projected triangle to fill the frame (>50%);
// a clean render keeps every face small (measured ~1-2%). Tight bound: 20%.
#[cfg(feature = "testmatrix")]
facett_core::testmatrix::emit(
"facett-demo::osm3d-cpu",
&format!("no_frame_filler[{name}]"),
max_cov < 0.20 && !faces.is_empty(),
&format!("max_single_tri_coverage={:.3} faces={}", max_cov, faces.len()),
);
max_cov
}
fn tri_area(t: &[egui::Pos2; 3]) -> f32 {
((t[1].x - t[0].x) * (t[2].y - t[0].y) - (t[2].x - t[0].x) * (t[1].y - t[0].y)).abs() * 0.5
}
fn fill_tri(img: &mut RgbaImage, t: &[egui::Pos2; 3], col: Color32) {
let minx = t.iter().map(|p| p.x).fold(f32::MAX, f32::min).floor().max(0.0) as i32;
let maxx = t.iter().map(|p| p.x).fold(f32::MIN, f32::max).ceil().min(W as f32 - 1.0) as i32;
let miny = t.iter().map(|p| p.y).fold(f32::MAX, f32::min).floor().max(0.0) as i32;
let maxy = t.iter().map(|p| p.y).fold(f32::MIN, f32::max).ceil().min(H as f32 - 1.0) as i32;
if maxx < minx || maxy < miny {
return;
}
let (a, b, c) = (t[0], t[1], t[2]);
let area = (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
if area.abs() < 1e-6 {
return;
}
for y in miny..=maxy {
for x in minx..=maxx {
let (px, py) = (x as f32 + 0.5, y as f32 + 0.5);
let w0 = ((b.x - px) * (c.y - py) - (c.x - px) * (b.y - py)) / area;
let w1 = ((c.x - px) * (a.y - py) - (a.x - px) * (c.y - py)) / area;
let w2 = 1.0 - w0 - w1;
if w0 >= -0.001 && w1 >= -0.001 && w2 >= -0.001 {
img.put_pixel(x as u32, y as u32, Rgba([col.r(), col.g(), col.b(), 255]));
}
}
}
}
/// Render the REAL wasm/CPU path through the **software z-buffer** (`raster_zbuffer`)
/// — the exact pixels `CpuPainterBackend::paint` now blits — and save the PNG so the
/// depth-tearing + tonemap fixes are visible. Also asserts the terracotta roofs read as
/// a warm, bright colour (not muddy) somewhere in the frame.
#[test]
fn zbuffer_render_of_the_real_cpu_path() {
let geo = osm_tabs::load();
let mut m = osm_tabs::osm3d_view(&geo).with_gpu(false);
m.spin = false;
let framed = m.render_camera().distance;
m.cam.distance = framed;
m.cam.goal_distance = m.cam.distance;
m.cam.elevation = 22.0_f32.to_radians();
m.cam.goal_elevation = m.cam.elevation;
m.cam.settle();
let bg = Color32::from_rgb(11, 13, 16);
let center = egui::Pos2::new(W as f32 * 0.5, H as f32 * 0.5);
let tris = facett_map3d::shade_mesh_tris(m.mesh(), &m.render_camera(), center, H as f32 * 0.5, bg);
let px = facett_map3d::raster_zbuffer(&tris, W as usize, H as usize, egui::Pos2::ZERO, 1.0);
// Composite over the bg (transparent pixels = sky/bg) and count warm-terracotta px.
let mut img = RgbaImage::from_pixel(W, H, Rgba([bg.r(), bg.g(), bg.b(), 255]));
let mut warm = 0u32;
for y in 0..H {
for x in 0..W {
let c = px[y as usize * W as usize + x as usize];
if c.a() > 0 {
img.put_pixel(x, y, Rgba([c.r(), c.g(), c.b(), 255]));
// A bright, warm roof pixel: strong red, clearly above blue.
if c.r() > 150 && i32::from(c.r()) - i32::from(c.b()) > 45 && c.r() > c.g() {
warm += 1;
}
}
}
}
let dir = std::env::temp_dir().join("facett-demo");
std::fs::create_dir_all(&dir).ok();
let png = dir.join("cpuraster_zbuffer.png");
img.save(&png).ok();
eprintln!("[zbuffer] wrote {} · tris={} · warm_roof_px={}", png.display(), tris.len(), warm);
assert!(warm > 200, "the tonemapped terracotta roofs read warm+bright ({warm} px), not muddy");
}
#[test]
fn rasterize_the_real_wasm_cpu_path() {
// The real wasm/CPU render across the poses the user reaches — assert NO
// frame-filling smear triangle at any of them (the clipping-bug guard, on the
// exact path the demo runs, with a viewport-tight bound).
for (name, dm, el) in [("default", 1.0, 22.0), ("pulled_back", 3.0, 40.0), ("street", 0.5, 8.0)] {
let cov = raster(name, dm, el);
assert!(cov < 0.20, "[{name}] a single triangle fills {:.1}% of the frame — clipping smear", cov * 100.0);
}
}
/// Reproduce the WASM render: the browser has no filesystem so it renders the
/// EMBEDDED Sigtuna data (osm_tabs::load_sigtuna), NOT the dev-box Liechtenstein.
/// Rasterize that through the CPU shade_faces path (the wasm/glow path) at the
/// natural from_geometry framing — the default pose the #OSM 3D deep-link lands on.
#[test]
fn reproduce_wasm_sigtuna_default() {
let geo = osm_tabs::load_sigtuna().expect("embedded sigtuna decodes");
eprintln!("[repro] sigtuna ways_3d={} pts={}", geo.ways_3d.len(), geo.points.len());
let mut m = osm_tabs::osm3d_view(&geo).with_gpu(false);
m.spin = false;
// NO pose override — the exact default framing the wasm opens with.
let bg = egui::Color32::from_rgb(11, 13, 16);
let center = egui::Pos2::new(W as f32 * 0.5, H as f32 * 0.5);
let faces = m.shade_faces(center, H as f32 * 0.5, bg);
let mut img = RgbaImage::from_pixel(W, H, Rgba([bg.r(), bg.g(), bg.b(), 255]));
let mut max_cov = 0.0f32;
for (_d, t, col) in &faces { fill_tri(&mut img, t, *col); max_cov = max_cov.max(tri_area(t) / (W as f32 * H as f32)); }
let png = std::env::temp_dir().join("facett-demo/repro_sigtuna.png");
std::fs::create_dir_all(png.parent().unwrap()).ok();
img.save(&png).ok();
eprintln!("[repro] faces={} biggest_tri={:.1}% → {}", faces.len(), max_cov*100.0, png.display());
}
/// Sweep the EMBEDDED sigtuna data (the wasm's data) on the CPU shade_faces path
/// across rotation×zoom×tilt — find the pose the live spinning wasm smears at.
#[test]
fn sweep_sigtuna_cpu_find_smear() {
let geo = osm_tabs::load_sigtuna().expect("sigtuna");
let mut m = osm_tabs::osm3d_view(&geo).with_gpu(false);
m.spin = false;
let framed = m.render_camera().distance;
let bg = Color32::from_rgb(11,13,16);
let center = egui::Pos2::new(W as f32*0.5, H as f32*0.5);
let mut worst = (0.0f32, String::new());
for az in (0..360).step_by(20) {
for &dm in &[0.3f32,0.6,1.0,2.0] {
for &el in &[5.0f32,20.0,45.0,75.0] {
m.cam.azimuth=(az as f32).to_radians(); m.cam.goal_azimuth=m.cam.azimuth;
m.cam.distance=framed*dm; m.cam.goal_distance=m.cam.distance;
m.cam.elevation=el.to_radians(); m.cam.goal_elevation=m.cam.elevation;
m.cam.settle();
let faces=m.shade_faces(center,H as f32*0.5,bg);
let mut mc=0.0f32; for (_d,t,_c) in &faces { mc=mc.max(tri_area(t)/(W as f32*H as f32)); }
if mc>worst.0 { worst=(mc, format!("az={az} dm={dm} el={el}"));
if mc>0.15 { // save smear frames
let mut img=RgbaImage::from_pixel(W,H,Rgba([bg.r(),bg.g(),bg.b(),255]));
for (_d,t,col) in &faces { fill_tri(&mut img,t,*col); }
img.save(std::env::temp_dir().join("facett-demo/sigtuna_smear.png")).ok();
}
}
}
}
}
eprintln!("[sweep-sigtuna] WORST biggest-tri={:.1}% at {}", worst.0*100.0, worst.1);
}