Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-map/tests/fill_edge_aa.rs

//! **A filled area's OWN edge is anti-aliased with the outline layer OFF** — GG audit
//! row 32 (only with `--features wgpu`).
//!
//! # The claim this file was written against, and what the frame said
//!
//! `tests/fill_outline_aa.rs` proves the RING around a filled way carries a coverage
//! ramp. Audit #32 says that ring is the only anti-aliasing a filled area has — *"fill
//! triangles have NO AA; the 0.5 px outline IS the AA"* — and prescribes an SDF edge
//! distance per fill vertex, or 4× MSAA on the offscreen target. If the premise held, a
//! shoreline with no outline would be a HARD edge: water, paper, and nothing between.
//!
//! MEASURED on t14s / Radeon 890M (RADV, integrated, Vulkan), 2026-09-06, at facett
//! `05bd892f` with nothing in the lane changed: a diamond lake (every shore diagonal to
//! the pixel grid) rendered through the lane with a fully transparent ring colour:
//!
//! ```text
//! [GG32] outline OFF: 415 shore px, 3 distinct shore colours, 3 coverage levels
//!        (bins/16: [0,0,0,0, 103, 0,0,0, 207, 0,0,0, 105, 0,0,0,0])
//! ```
//!
//! **Three levels, at exactly ¼, ½ and ¾ water.** That is the offscreen target's
//! `MAP_MSAA_SAMPLES = 4` resolving the fill triangles — lane GG #2 put the whole
//! offscreen pair at 4× for per-sample depth, and the fill pipeline draws into it
//! through `map_msaa_state()`, so a triangle's true edge has been rasterised at four
//! samples since that commit. The audit's premise expired with GG #2, and its two cures
//! are not equivalent: the sample count is already there (GG #2's own row says 4× alone
//! changed nothing FOR ROADS, because `line.wgsl` rasterises a quad WIDER than the road
//! and hides the coverage in alpha — a fill triangle's edge IS its geometric edge, so
//! the rasteriser sees it directly and no `sample_mask` is needed). A per-vertex edge
//! distance would raise the ramp from 4 levels to 256, but an ear-clip triangle does not
//! know which of its edges is a boundary, so it is a new vertex format or a fringe
//! strip — not a cheaper cure, and not one this measurement asks for.
//!
//! # What is pinned, and why it is pixels
//!
//! The 4× floor: at least THREE distinct coverage levels along a diagonal shore with
//! the outline off. Nothing else in the suite looks at a fill edge without its ring, so
//! a future change that drops the target or the fill pipeline off `MAP_MSAA_SAMPLES` —
//! or hands the fill fragment a `sample_mask` — would only have been caught by the
//! road-lane oracles, indirectly. A control keeps the outline ON and checks the water
//! and the ring's ramp are what they were.
//!
//! # Why this is its own binary
//!
//! `facett_map::gpu::install_renderer` latches a PROCESS-GLOBAL `renderer_installed()`.
//! `fill_outline_aa.rs` renders a CPU arm through a plain kittest `RenderState` with no
//! renderer in it, relying on that latch being `false`; a test in the same binary that
//! installed first made the CPU arm hand its fills to a callback with no renderer and
//! draw nothing (ink 57 369 → 19 343 px, MEASURED). One binary per install policy.
//!
//! Run: `cargo test -p facett-map --features wgpu --test fill_edge_aa -- --nocapture`
#![cfg(feature = "wgpu")]

use facett_map::layer::{LatLon, MapFacet, MapTheme, MapView, WaysLayer};
use facett_map::Way;

mod common;

const CANVAS: egui::Vec2 = egui::vec2(600.0, 600.0);

/// The least number of distinct partial-coverage levels a diagonal shore must show with
/// the outline off: `MAP_MSAA_SAMPLES − 1`, the ¼ / ½ / ¾ steps a 4× resolve can put a
/// pixel at. Written as a number rather than the constant so a change to the constant
/// has to come here and re-measure.
const MSAA_FLOOR_LEVELS: usize = 3;

/// A square rotated 45° in `(lon, lat)`, tagged water so it classifies `area` and takes
/// the fill lane. Mercator stretches y by ~1.96x at this latitude, so the half-widths
/// below put the shore near 38° on screen: diagonal, and NOT the 45° a sample grid can
/// be symmetric about.
///
/// `outline == false` turns the ring layer OFF the one way the lane can be asked to: a
/// fully transparent ring colour. `line.wgsl` discards a fragment whose alpha is below
/// `MIN_COVERAGE_ALPHA`, so a transparent outline paints nothing, blends nothing and
/// claims no depth — the fill is alone on the shore. The fill colour is untouched.
fn diamond(outline: bool) -> Way {
    let (lon, lat) = (18.0620, 59.3272);
    let (w, h) = (0.010, 0.004);
    let r = vec![(lon, lat + h), (lon + w, lat), (lon, lat - h), (lon - w, lat), (lon, lat + h)];
    let tags = [("natural".to_string(), "water".to_string())];
    let mut way = Way::from_lonlat_with(1, &r, &tags, &facett_map::MapPalette::cartographic(false))
        .expect("a 5-vertex closed ring is a way");
    assert!(way.fill.is_some(), "fixture: the diamond must be a FILL, or there is no shore to measure");
    if !outline {
        way.style.color = egui::Color32::TRANSPARENT;
        way.stroke = egui::Color32::TRANSPARENT;
    }
    way
}

fn facet(ways: Vec<Way>) -> MapFacet {
    let view = MapView::new(LatLon::new(59.3272, 18.0620), 14.0, (CANVAS.x, CANVAS.y));
    MapFacet::new("fill-edge-aa", view, MapTheme::default())
        .with_layer(Box::new(WaysLayer::new("osm-ways", ways)))
}

/// One lane render plus the still-frame controls read beside it.
struct Probe {
    img: image::RgbaImage,
    /// Frames the GPU lane really encoded — 0 would mean the CPU painter drew this.
    lane_frames: u64,
    /// `offscreen_target_allocs()` after the first frame and after the last (GG #37).
    allocs: (u64, u64),
    /// `cull_dispatch_count()` after the first frame and after the last (GG #13).
    culls: (u64, u64),
    /// Harness wall time per frame over the still frames after the first — egui,
    /// the lane and the readback together, NOT the GPU pass alone.
    ms_per_frame: f64,
    /// The last blit's wall time (`last_blit_us`) and the lane clock's last cull pass
    /// where the device has timestamp queries.
    blit_us: u64,
    cull_pass_us: Option<u64>,
}

/// Render `frames` frames with `GpuMapRenderer` installed and read the controls off
/// the same `RenderState` the harness draws through. `RenderState` is `Arc`-backed, so
/// the clone kept here sees the renderer the callback installed.
fn probe_gpu(mut f: MapFacet, frames: usize) -> Probe {
    let _turn = common::one_gpu::hold();
    let before = facett_core::render::lane::lane_use().frames();
    let setup = facett_core::render::gpu::adapter_wgpu::facett_wgpu_options().wgpu_setup;
    let rs = egui_kittest::wgpu::create_render_state(setup, Default::default());
    facett_map::gpu::install_renderer(&rs);
    let seam = rs.clone();
    let culls = move || {
        seam.renderer
            .read()
            .callback_resources
            .get::<facett_map::gpu::GpuMapRenderer>()
            .map_or(0, facett_map::gpu::GpuMapRenderer::cull_dispatch_count)
    };
    let mut h = egui_kittest::Harness::builder()
        .with_size(CANVAS)
        .renderer(egui_kittest::wgpu::WgpuTestRenderer::from_render_state(rs))
        .build_ui(move |ui| f.ui(ui));
    let mut img = None;
    let mut first = (0, 0);
    let mut t0 = std::time::Instant::now();
    for i in 0..frames {
        img = Some(h.render().expect("the wgpu backend produced a frame"));
        h.step();
        if i == 0 {
            first = (facett_map::gpu::offscreen_target_allocs(), culls());
            t0 = std::time::Instant::now();
        }
    }
    let ms_per_frame = t0.elapsed().as_secs_f64() * 1000.0 / (frames.max(2) - 1) as f64;
    Probe {
        img: img.expect("at least one frame"),
        lane_frames: facett_core::render::lane::lane_use().frames() - before,
        allocs: (first.0, facett_map::gpu::offscreen_target_allocs()),
        culls: (first.1, culls()),
        ms_per_frame,
        blit_us: facett_map::gpu::last_blit_us(),
        cull_pass_us: facett_map::gpu::lane_clock_stats().last_gpu_pass_us,
    }
}

/// How many units of water a pixel carries, as the projection of `p − paper` onto
/// `water − paper`; `None` when the pixel is off that axis (the outline's own hue, an
/// overlay) by more than `OFF_AXIS` per channel.
fn water_share(p: [u8; 4], paper: [u8; 4], water: [u8; 4]) -> Option<f32> {
    const OFF_AXIS: f32 = 10.0;
    let d: [f32; 3] = std::array::from_fn(|i| f32::from(water[i]) - f32::from(paper[i]));
    let q: [f32; 3] = std::array::from_fn(|i| f32::from(p[i]) - f32::from(paper[i]));
    let dd: f32 = d.iter().map(|v| v * v).sum();
    let t = (0..3).map(|i| q[i] * d[i]).sum::<f32>() / dd;
    let residual = (0..3).map(|i| (q[i] - t * d[i]).abs()).fold(0.0_f32, f32::max);
    (residual <= OFF_AXIS).then_some(t)
}

/// What the shoreline census hands back.
struct Shore {
    /// Distinct coverage levels among ON-AXIS partial pixels (bins of a sixteenth).
    levels: usize,
    /// The bin histogram those levels were counted from.
    hist: [u32; 17],
    /// Distinct RGB triples over the whole shore, ring hue included.
    triples: usize,
    /// Shore pixels: neither paper nor water, inside the central window.
    px: usize,
}

/// **The shoreline census.** Every pixel in the central window that is neither paper
/// nor water is a shore pixel — the diamond is the only ink there, and the window
/// (60..540 both ways) is clear of the lane flag and badge overlays in the corners.
///
/// A hard edge has NO on-axis partial pixel: every pixel is paper or water, and
/// `levels` is 0. A 4× resolve can put a pixel at ¼, ½ or ¾ water — three levels.
fn shoreline(img: &image::RgbaImage) -> Shore {
    let paper = px_at(img, 0.05, 0.95);
    let water = px_at(img, 0.5, 0.5);
    assert!(!near(paper, water, 8), "fixture: the pane centre must be water, not paper ({water:?} vs {paper:?})");
    let mut hist = [0u32; 17];
    let mut triples = std::collections::HashSet::new();
    let mut px = 0usize;
    for y in 60..540u32 {
        for x in 60..540u32 {
            let p = img.get_pixel(x, y).0;
            if near(p, paper, 2) || near(p, water, 2) {
                continue;
            }
            px += 1;
            triples.insert([p[0], p[1], p[2]]);
            if let Some(t) = water_share(p, paper, water)
                && (0.03..=0.97).contains(&t)
            {
                hist[(t * 16.0).round() as usize] += 1;
            }
        }
    }
    Shore { levels: hist.iter().filter(|c| **c > 0).count(), hist, triples: triples.len(), px }
}

fn px_at(img: &image::RgbaImage, fx: f32, fy: f32) -> [u8; 4] {
    let x = ((fx * img.width() as f32) as u32).min(img.width() - 1);
    let y = ((fy * img.height() as f32) as u32).min(img.height() - 1);
    img.get_pixel(x, y).0
}

fn near(a: [u8; 4], b: [u8; 4], tol: i32) -> bool {
    (0..3).all(|i| (i32::from(a[i]) - i32::from(b[i])).abs() <= tol)
}

fn save(img: &image::RgbaImage, name: &str) {
    let path = format!("{}/scratch/cargo/fill_edge_aa_{name}.png", std::env::var("HOME").unwrap_or_default());
    let _ = image::DynamicImage::ImageRgba8(img.clone()).save(&path);
    eprintln!("LOOK: saved {path} ({}x{})", img.width(), img.height());
}

/// A synthetic 600×600 frame of the diamond, `paper` outside and `water` inside, with the
/// coverage of a pixel decided by `samples` points across it: 1 is a hard edge (the
/// pixel centre alone), 4 is a 4× resolve. Only for proving the census below can tell
/// the two apart — the lane's own frame is what the oracles measure.
fn synthetic(samples: u32, paper: [u8; 4], water: [u8; 4]) -> image::RgbaImage {
    let (cx, cy, hw, hh) = (300.0_f32, 300.0_f32, 116.0_f32, 91.0_f32);
    let inside = |x: f32, y: f32| ((x - cx) / hw).abs() + ((y - cy) / hh).abs() <= 1.0;
    let mut img = image::RgbaImage::new(600, 600);
    for (x, y, p) in img.enumerate_pixels_mut() {
        let mut hit = 0u32;
        for i in 0..samples {
            for j in 0..samples {
                let (sx, sy) = ((i as f32 + 0.5) / samples as f32, (j as f32 + 0.5) / samples as f32);
                hit += u32::from(inside(x as f32 + sx, y as f32 + sy));
            }
        }
        let t = hit as f32 / (samples * samples) as f32;
        *p = image::Rgba(std::array::from_fn(|k| {
            (f32::from(paper[k]) + t * (f32::from(water[k]) - f32::from(paper[k]))).round() as u8
        }));
    }
    img
}

/// **The census is not vacuous**: on a HARD diagonal edge it counts zero levels, and on
/// a multi-sample one it counts the steps. Without this the oracle's `>= 3` could be
/// met by a census that mis-bins paper.
#[test]
fn the_census_sees_a_hard_edge_as_zero_levels_and_a_resolved_one_as_steps() {
    let (paper, water) = ([246, 242, 233, 255], [18, 58, 90, 255]);
    // A hard edge has NO pixel that is neither paper nor water — the census must see an
    // empty shore, not a shore of two colours.
    let hard = shoreline(&synthetic(1, paper, water));
    assert_eq!((hard.px, hard.levels), (0, 0), "a hard edge binned a shore: {} px, {:?}", hard.px, hard.hist);
    let msaa = shoreline(&synthetic(2, paper, water));
    assert!(msaa.px > 200, "the resolved diamond has a shore ({} px)", msaa.px);
    assert_eq!(msaa.levels, MSAA_FLOOR_LEVELS, "a 2×2 resolve has three steps: {:?}", msaa.hist);
}

/// **THE ORACLE (GG audit row 32): a diagonal shoreline is anti-aliased with the
/// outline layer OFF.** The fill triangles carry their own edge coverage; the ring
/// stroke is not the only thing that does.
#[test]
fn a_fill_edge_is_anti_aliased_with_the_outline_layer_off() {
    if common::headless_device("fill_edge_aa").is_none() {
        return;
    }
    let off = probe_gpu(facet(vec![diamond(false)]), 4);
    save(&off.img, "outline_off");
    let s = shoreline(&off.img);
    eprintln!(
        "[GG32] outline OFF: {} shore px, {} distinct shore colours, {} coverage levels (bins/16: {:?}) | \
         lane frames {} | culls {:?} allocs {:?} | harness {:.2} ms/frame, blit {} us, cull pass {:?} us",
        s.px, s.triples, s.levels, s.hist, off.lane_frames, off.culls, off.allocs, off.ms_per_frame, off.blit_us,
        off.cull_pass_us
    );

    // NON-VACUITY: the GPU lane drew it, and a shore exists to be measured.
    assert!(off.lane_frames > 0, "the GPU lane encoded no frame — this is the CPU painter, not the device");
    assert!(s.px > 200, "only {} shore pixels: the diamond is not on the pane where the census looks", s.px);

    // The still-frame controls did not move: one cull per lane frame (the whole-payload
    // path culls every frame it encodes; GG #13's skip is the tile lane's) and NO
    // offscreen target allocated after the first frame (GG #37).
    assert_eq!(off.culls.1 - off.culls.0, off.lane_frames - 1, "extra cull dispatches on still frames");
    assert_eq!(off.allocs.0, off.allocs.1, "an offscreen target was allocated on a still frame");

    // THE CLAIM. A hard edge is water and paper and nothing between: zero levels. The
    // 4× target puts a shore pixel at ¼, ½ or ¾ — three, and that floor is what is held.
    assert!(
        s.levels >= MSAA_FLOOR_LEVELS,
        "the shoreline has {} partial-coverage level(s) with the outline off (bins/16 {:?}); {} shore \
         pixels in {} colours. The fill triangles are no longer resolving through the 4× offscreen \
         target (`MAP_MSAA_SAMPLES`, `map_msaa_state()` on `fill_pipeline`) — audit #32's hard edge \
         is back.",
        s.levels, s.hist, s.px, s.triples
    );
}

/// **CONTROL: the same shore with the outline ON is what it was.** The interior is the
/// same water, the ring still carries its own ramp (many more colours than the fill's
/// four steps), and it can only WIDEN the shore over the fill's own edge, never narrow it.
#[test]
fn the_shore_with_the_outline_on_keeps_its_ramp_and_its_water() {
    if common::headless_device("fill_edge_aa_control").is_none() {
        return;
    }
    let on = probe_gpu(facet(vec![diamond(true)]), 4);
    let off = probe_gpu(facet(vec![diamond(false)]), 4);
    save(&on.img, "outline_on");
    let a = shoreline(&on.img);
    let b = shoreline(&off.img);
    eprintln!(
        "[GG32] outline ON: {} shore px, {} distinct shore colours, {} on-axis coverage levels (bins/16: {:?}) \
         | OFF: {} px, {} levels | harness {:.2} vs {:.2} ms/frame | blit {} vs {} us",
        a.px, a.triples, a.levels, a.hist, b.px, b.levels, on.ms_per_frame, off.ms_per_frame, on.blit_us, off.blit_us
    );
    assert!(on.lane_frames > 0 && off.lane_frames > 0, "both renders must be the GPU lane");
    assert_eq!(px_at(&on.img, 0.5, 0.5), px_at(&off.img, 0.5, 0.5), "the outline must not change the water itself");
    assert!(
        a.triples >= 8,
        "the ring lost its coverage ramp: only {} distinct colours along the shore with the outline on",
        a.triples
    );
    assert!(a.px >= b.px, "the shore is NARROWER with the outline on ({} < {} px)", a.px, b.px);
    assert_eq!(on.allocs.0, on.allocs.1, "an offscreen target was allocated on a still frame");
}